From 2ea5e5d6ef8fa5c574d0a752c33674423e41f163 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Wed, 27 Sep 2017 15:17:22 +0200
Subject: [PATCH] Implemented Account Setup UI

---
 app/build.gradle.kts                          |   5 +
 app/src/main/AndroidManifest.xml              |   8 +
 .../ui/setup/AccountSetupActivity.kt          |  17 ++
 .../quasseldroid_ng/ui/setup/SetupActivity.kt | 172 ++++++++++++++++++
 .../slides/AccountSetupConnectionSlide.kt     |  64 +++++++
 .../ui/setup/slides/AccountSetupNameSlide.kt  |  55 ++++++
 .../ui/setup/slides/AccountSetupUserSlide.kt  |  63 +++++++
 .../ui/setup/slides/SlideFragment.kt          |  72 ++++++++
 .../ui/setup/slides/ValidityChangeCallback.kt |   5 +
 .../layout-sw600dp-land/activity_setup.xml    |  25 +++
 .../res/layout-sw600dp-land/setup_slide.xml   |  81 +++++++++
 .../res/layout-sw600dp/activity_setup.xml     |  32 ++++
 .../main/res/layout-sw600dp/setup_slide.xml   |  46 +++++
 app/src/main/res/layout/activity_setup.xml    |  25 +++
 .../res/layout/setup_account_connection.xml   |  53 ++++++
 .../main/res/layout/setup_account_name.xml    |  40 ++++
 .../main/res/layout/setup_account_user.xml    |  52 ++++++
 app/src/main/res/layout/setup_slide.xml       |  44 +++++
 app/src/main/res/values-sw600dp/styles.xml    |   6 +
 app/src/main/res/values/colors.xml            |   4 +-
 app/src/main/res/values/defaults.xml          |   3 +
 app/src/main/res/values/strings_setup.xml     |  33 ++++
 app/src/main/res/values/styles.xml            |  14 +-
 build.gradle.kts                              |   3 +
 24 files changed, 916 insertions(+), 6 deletions(-)
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/AccountSetupActivity.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/SetupActivity.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/AccountSetupConnectionSlide.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/AccountSetupNameSlide.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/AccountSetupUserSlide.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/SlideFragment.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/ValidityChangeCallback.kt
 create mode 100644 app/src/main/res/layout-sw600dp-land/activity_setup.xml
 create mode 100644 app/src/main/res/layout-sw600dp-land/setup_slide.xml
 create mode 100644 app/src/main/res/layout-sw600dp/activity_setup.xml
 create mode 100644 app/src/main/res/layout-sw600dp/setup_slide.xml
 create mode 100644 app/src/main/res/layout/activity_setup.xml
 create mode 100644 app/src/main/res/layout/setup_account_connection.xml
 create mode 100644 app/src/main/res/layout/setup_account_name.xml
 create mode 100644 app/src/main/res/layout/setup_account_user.xml
 create mode 100644 app/src/main/res/layout/setup_slide.xml
 create mode 100644 app/src/main/res/values-sw600dp/styles.xml
 create mode 100644 app/src/main/res/values/defaults.xml
 create mode 100644 app/src/main/res/values/strings_setup.xml

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index bb47d3bf5..865e508b7 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -51,6 +51,8 @@ android {
         )
       }
     }
+
+    vectorDrawables.useSupportLibrary = true
   }
 
   buildTypes {
@@ -75,6 +77,9 @@ dependencies {
   implementation(appCompat("customtabs"))
   implementation(appCompat("cardview-v7"))
   implementation(appCompat("recyclerview-v7"))
+  implementation("com.android.support.constraint:constraint-layout:1.0.2")
+
+  implementation("com.github.apl-devs:appintro:v4.2.2")
 
   implementation("io.reactivex.rxjava2:rxjava:2.1.3")
 
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1c13407cc..4434e368d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -19,9 +19,17 @@
       android:theme="@style/SplashTheme">
       <intent-filter>
         <action android:name="android.intent.action.MAIN" />
+        <action android:name="android.intent.action.VIEW" />
+
         <category android:name="android.intent.category.LAUNCHER" />
       </intent-filter>
     </activity>
+    <activity
+      android:name=".ui.setup.AccountSetupActivity"
+      android:exported="true"
+      android:label="AccountSetup"
+      android:theme="@style/SplashTheme" />
+
     <service
       android:name=".service.QuasselService"
       android:description="@string/connection_service_description"
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/AccountSetupActivity.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/AccountSetupActivity.kt
new file mode 100644
index 000000000..8a537a205
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/AccountSetupActivity.kt
@@ -0,0 +1,17 @@
+package de.kuschku.quasseldroid_ng.ui.setup
+
+import android.os.Bundle
+import de.kuschku.quasseldroid_ng.ui.setup.slides.AccountSetupConnectionSlide
+import de.kuschku.quasseldroid_ng.ui.setup.slides.AccountSetupNameSlide
+import de.kuschku.quasseldroid_ng.ui.setup.slides.AccountSetupUserSlide
+
+class AccountSetupActivity : SetupActivity() {
+  override fun onDone(data: Bundle) {
+  }
+
+  override val fragments = listOf(
+    AccountSetupConnectionSlide(),
+    AccountSetupUserSlide(),
+    AccountSetupNameSlide()
+  )
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/SetupActivity.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/SetupActivity.kt
new file mode 100644
index 000000000..734561b6e
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/SetupActivity.kt
@@ -0,0 +1,172 @@
+package de.kuschku.quasseldroid_ng.ui.setup
+
+import android.arch.lifecycle.MutableLiveData
+import android.arch.lifecycle.Observer
+import android.os.Bundle
+import android.os.Parcelable
+import android.support.design.widget.FloatingActionButton
+import android.support.v4.app.FragmentManager
+import android.support.v4.app.FragmentStatePagerAdapter
+import android.support.v4.view.ViewPager
+import android.support.v7.app.AppCompatActivity
+import android.util.SparseArray
+import android.view.ViewGroup
+import butterknife.BindView
+import butterknife.ButterKnife
+import de.kuschku.quasseldroid_ng.R
+import de.kuschku.quasseldroid_ng.ui.setup.slides.SlideFragment
+import de.kuschku.quasseldroid_ng.util.helper.stickySwitchMapNotNull
+
+abstract class SetupActivity : AppCompatActivity() {
+  @BindView(R.id.view_pager)
+  lateinit var viewPager: ViewPager
+
+  @BindView(R.id.next_button)
+  lateinit var button: FloatingActionButton
+
+  private lateinit var adapter: SlidePagerAdapter
+
+  protected abstract val fragments: List<SlideFragment>
+
+  private val currentPage = MutableLiveData<SlideFragment?>()
+  private val isValid = currentPage.stickySwitchMapNotNull(false, SlideFragment::valid)
+
+  private val pageChangeListener = object : ViewPager.OnPageChangeListener {
+    override fun onPageScrollStateChanged(state: Int) {
+      when (state) {
+        ViewPager.SCROLL_STATE_SETTLING -> pageChanged()
+      }
+    }
+
+    override fun onPageScrolled(position: Int, positionOffset: Float,
+                                positionOffsetPixels: Int) = Unit
+
+    override fun onPageSelected(position: Int) = Unit
+  }
+
+  private fun pageChanged() {
+    currentPage.value = adapter.getItem(viewPager.currentItem)
+    val drawable = if (viewPager.currentItem == adapter.totalCount - 1)
+      R.drawable.ic_check
+    else
+      R.drawable.ic_arrow_right
+    button.setImageResource(drawable)
+  }
+
+  override fun onCreate(savedInstanceState: Bundle?) {
+    setTheme(R.style.SetupTheme)
+    super.onCreate(savedInstanceState)
+    setContentView(R.layout.activity_setup)
+    ButterKnife.bind(this)
+
+    adapter = SlidePagerAdapter(supportFragmentManager)
+    fragments.forEach(adapter::addFragment)
+    viewPager.adapter = adapter
+
+    button.setOnClickListener {
+      if (viewPager.currentItem == adapter.totalCount - 1)
+        onDoneInternal()
+      else
+        viewPager.setCurrentItem(viewPager.currentItem + 1, true)
+    }
+    isValid.observe(this, Observer {
+      if (it == true) {
+        button.show()
+        adapter.lastValidItem = viewPager.currentItem
+      } else {
+        button.hide()
+        adapter.lastValidItem = viewPager.currentItem - 1
+      }
+    })
+    viewPager.addOnPageChangeListener(pageChangeListener)
+    pageChanged()
+  }
+
+  private fun onDoneInternal() {
+    onDone(adapter.result)
+  }
+
+  abstract fun onDone(data: Bundle)
+
+  override fun onSaveInstanceState(outState: Bundle) {
+    outState.putInt("currentItem", viewPager.currentItem)
+    outState.putInt("lastValidItem", adapter.lastValidItem)
+    outState.putBundle("result", adapter.result)
+    super.onSaveInstanceState(outState)
+  }
+
+  override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
+    super.onRestoreInstanceState(savedInstanceState)
+    if (savedInstanceState != null) {
+      if (savedInstanceState.containsKey("result"))
+        adapter.result.putAll(savedInstanceState.getBundle("result"))
+      if (savedInstanceState.containsKey("lastValidItem"))
+        adapter.lastValidItem = savedInstanceState.getInt("lastValidItem")
+      if (savedInstanceState.containsKey("currentItem"))
+        viewPager.currentItem = savedInstanceState.getInt("currentItem")
+      currentPage.value = adapter.getItem(viewPager.currentItem)
+    }
+    pageChanged()
+  }
+
+  companion object {
+    private class SlidePagerAdapter(private val fragmentManager: FragmentManager) :
+      FragmentStatePagerAdapter(fragmentManager) {
+      private val retainedFragments = SparseArray<SlideFragment>()
+
+      val result = Bundle()
+        get() {
+          (0 until retainedFragments.size()).map(retainedFragments::valueAt).forEach {
+            it.getData(field)
+          }
+          return field
+        }
+
+      var lastValidItem = -1
+        set(value) {
+          field = value
+          notifyDataSetChanged()
+        }
+      private val list = mutableListOf<SlideFragment>()
+
+      override fun getItem(position: Int): SlideFragment {
+        return retainedFragments.get(position) ?: list[position]
+      }
+
+      override fun getCount() = Math.min(list.size, lastValidItem + 2)
+      val totalCount get() = list.size
+      fun addFragment(fragment: SlideFragment) {
+        list.add(fragment)
+      }
+
+      override fun instantiateItem(container: ViewGroup?, position: Int): Any {
+        val fragment = super.instantiateItem(container, position)
+        retainedFragments.put(position, fragment as SlideFragment)
+        return fragment
+      }
+
+      override fun destroyItem(container: ViewGroup?, position: Int, `object`: Any?) {
+        retainedFragments.get(position)?.getData(result)
+        retainedFragments.remove(position)
+        super.destroyItem(container, position, `object`)
+      }
+
+      override fun restoreState(state: Parcelable?, loader: ClassLoader?) {
+        super.restoreState(state, loader)
+        if (state != null) {
+          val bundle = state as Bundle
+          val keys = bundle.keySet()
+          for (key in keys) {
+            if (key.startsWith("f")) {
+              val index = Integer.parseInt(key.substring(1))
+              val f = fragmentManager.getFragment(bundle, key)
+              if (f != null && f is SlideFragment) {
+                retainedFragments.put(index, f)
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/AccountSetupConnectionSlide.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/AccountSetupConnectionSlide.kt
new file mode 100644
index 000000000..7e6cc8352
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/AccountSetupConnectionSlide.kt
@@ -0,0 +1,64 @@
+package de.kuschku.quasseldroid_ng.ui.setup.slides
+
+import android.os.Bundle
+import android.support.design.widget.TextInputEditText
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import butterknife.BindView
+import butterknife.ButterKnife
+import de.kuschku.quasseldroid_ng.R
+
+class AccountSetupConnectionSlide : SlideFragment() {
+  @BindView(R.id.host)
+  lateinit var hostField: TextInputEditText
+
+  @BindView(R.id.port)
+  lateinit var portField: TextInputEditText
+
+  private val textWatcher = object : TextWatcher {
+    override fun afterTextChanged(p0: Editable?) = updateValidity()
+    override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) = Unit
+    override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) = Unit
+  }
+
+  override fun isValid(): Boolean {
+    return validHost() && validPort()
+  }
+
+  override val title = R.string.slideAccountConnectionTitle
+  override val descripion = R.string.slideAccountConnectionDescription
+
+  override fun setData(data: Bundle) {
+    if (data.containsKey("host"))
+      hostField.setText(data.getString("host"))
+    if (data.containsKey("port"))
+      portField.setText(data.getInt("port").toString())
+    updateValidity()
+  }
+
+  override fun getData(data: Bundle) {
+    data.putString("host", hostField.text.toString())
+    data.putInt("port", portField.text.toString().toIntOrNull() ?: -1)
+  }
+
+  override fun onCreateContent(inflater: LayoutInflater, container: ViewGroup?,
+                               savedInstanceState: Bundle?): View {
+    val view = inflater.inflate(R.layout.setup_account_connection, container, false)
+    ButterKnife.bind(this, view)
+    hostField.addTextChangedListener(textWatcher)
+    portField.addTextChangedListener(textWatcher)
+    return view
+  }
+
+  override fun onDestroyView() {
+    hostField.removeTextChangedListener(textWatcher)
+    portField.removeTextChangedListener(textWatcher)
+    super.onDestroyView()
+  }
+
+  private fun validHost() = hostField.text.isNotEmpty()
+  private fun validPort() = (0 until 65536).contains(portField.text.toString().toIntOrNull())
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/AccountSetupNameSlide.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/AccountSetupNameSlide.kt
new file mode 100644
index 000000000..d3be3155c
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/AccountSetupNameSlide.kt
@@ -0,0 +1,55 @@
+package de.kuschku.quasseldroid_ng.ui.setup.slides
+
+import android.os.Bundle
+import android.support.design.widget.TextInputEditText
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import butterknife.BindView
+import butterknife.ButterKnife
+import de.kuschku.quasseldroid_ng.R
+
+class AccountSetupNameSlide : SlideFragment() {
+  @BindView(R.id.name)
+  lateinit var nameField: TextInputEditText
+
+  private val textWatcher = object : TextWatcher {
+    override fun afterTextChanged(p0: Editable?) = updateValidity()
+    override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) = Unit
+    override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) = Unit
+  }
+
+  override fun isValid(): Boolean {
+    return validName()
+  }
+
+  override val title = R.string.slideAccountNameTitle
+  override val descripion = R.string.slideAccountNameDescription
+
+  override fun setData(data: Bundle) {
+    if (data.containsKey("name"))
+      nameField.setText(data.getString("name"))
+    updateValidity()
+  }
+
+  override fun getData(data: Bundle) {
+    data.putString("name", nameField.text.toString())
+  }
+
+  override fun onCreateContent(inflater: LayoutInflater, container: ViewGroup?,
+                               savedInstanceState: Bundle?): View {
+    val view = inflater.inflate(R.layout.setup_account_name, container, false)
+    ButterKnife.bind(this, view)
+    nameField.addTextChangedListener(textWatcher)
+    return view
+  }
+
+  override fun onDestroyView() {
+    nameField.removeTextChangedListener(textWatcher)
+    super.onDestroyView()
+  }
+
+  private fun validName() = nameField.text.isNotEmpty()
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/AccountSetupUserSlide.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/AccountSetupUserSlide.kt
new file mode 100644
index 000000000..9efc94cc7
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/AccountSetupUserSlide.kt
@@ -0,0 +1,63 @@
+package de.kuschku.quasseldroid_ng.ui.setup.slides
+
+import android.os.Bundle
+import android.support.design.widget.TextInputEditText
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import butterknife.BindView
+import butterknife.ButterKnife
+import de.kuschku.quasseldroid_ng.R
+
+class AccountSetupUserSlide : SlideFragment() {
+  @BindView(R.id.user)
+  lateinit var userField: TextInputEditText
+
+  @BindView(R.id.pass)
+  lateinit var passField: TextInputEditText
+
+  private val textWatcher = object : TextWatcher {
+    override fun afterTextChanged(p0: Editable?) = updateValidity()
+    override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) = Unit
+    override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) = Unit
+  }
+
+  override fun isValid(): Boolean {
+    return validUser() && validPass()
+  }
+
+  override val title = R.string.slideAccountUserTitle
+  override val descripion = R.string.slideAccountUserDescription
+
+  override fun setData(data: Bundle) {
+    if (data.containsKey("user"))
+      userField.setText(data.getString("user"))
+    if (data.containsKey("pass"))
+      passField.setText(data.getString("pass"))
+  }
+
+  override fun getData(data: Bundle) {
+    data.putString("user", userField.text.toString())
+    data.putString("pass", passField.text.toString())
+  }
+
+  override fun onCreateContent(inflater: LayoutInflater, container: ViewGroup?,
+                               savedInstanceState: Bundle?): View {
+    val view = inflater.inflate(R.layout.setup_account_user, container, false)
+    ButterKnife.bind(this, view)
+    userField.addTextChangedListener(textWatcher)
+    passField.addTextChangedListener(textWatcher)
+    return view
+  }
+
+  override fun onDestroyView() {
+    userField.removeTextChangedListener(textWatcher)
+    passField.removeTextChangedListener(textWatcher)
+    super.onDestroyView()
+  }
+
+  private fun validUser() = userField.text.isNotEmpty()
+  private fun validPass() = passField.text.isNotEmpty()
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/SlideFragment.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/SlideFragment.kt
new file mode 100644
index 000000000..d6e1a76fd
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/SlideFragment.kt
@@ -0,0 +1,72 @@
+package de.kuschku.quasseldroid_ng.ui.setup.slides
+
+import android.arch.lifecycle.LifecycleOwner
+import android.arch.lifecycle.MutableLiveData
+import android.arch.lifecycle.Observer
+import android.os.Bundle
+import android.support.annotation.StringRes
+import android.support.v4.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import de.kuschku.quasseldroid_ng.R
+
+abstract class SlideFragment : Fragment() {
+  @get:StringRes
+  protected abstract val title: Int
+  @get:StringRes
+  protected abstract val descripion: Int
+
+  protected abstract fun isValid(): Boolean
+
+  val valid = object : MutableLiveData<Boolean>() {
+    override fun observe(owner: LifecycleOwner?, observer: Observer<Boolean>?) {
+      super.observe(owner, observer)
+      observer?.onChanged(value)
+    }
+
+    override fun observeForever(observer: Observer<Boolean>?) {
+      super.observeForever(observer)
+      observer?.onChanged(value)
+    }
+  }
+
+  protected fun updateValidity() {
+    val valid1 = isValid()
+    println("Updating validity: ${this::class.java.simpleName}@${hashCode()} $valid1")
+    valid.value = valid1
+  }
+
+  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+                            savedInstanceState: Bundle?): View {
+    val view = inflater.inflate(R.layout.setup_slide, container, false)
+    val viewGroup = view.findViewById<View>(R.id.content_host) as ViewGroup
+    viewGroup.addView(onCreateContent(inflater, viewGroup, savedInstanceState))
+
+    view.findViewById<TextView>(R.id.title).setText(title)
+    view.findViewById<TextView>(R.id.description).setText(descripion)
+
+    if (savedInstanceState != null)
+      setData(savedInstanceState)
+    updateValidity()
+
+    return view
+  }
+
+  override fun onSaveInstanceState(outState: Bundle) {
+    super.onSaveInstanceState(outState)
+    getData(outState)
+  }
+
+  override fun onViewStateRestored(savedInstanceState: Bundle?) {
+    super.onViewStateRestored(savedInstanceState)
+    updateValidity()
+  }
+
+  abstract fun setData(data: Bundle)
+  abstract fun getData(data: Bundle)
+
+  protected abstract fun onCreateContent(inflater: LayoutInflater, container: ViewGroup?,
+                                         savedInstanceState: Bundle?): View
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/ValidityChangeCallback.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/ValidityChangeCallback.kt
new file mode 100644
index 000000000..2fa49ef4b
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/slides/ValidityChangeCallback.kt
@@ -0,0 +1,5 @@
+package de.kuschku.quasseldroid_ng.ui.setup.slides
+
+interface ValidityChangeCallback {
+  fun invoke(isValid: Boolean)
+}
diff --git a/app/src/main/res/layout-sw600dp-land/activity_setup.xml b/app/src/main/res/layout-sw600dp-land/activity_setup.xml
new file mode 100644
index 000000000..c97d7bc4e
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-land/activity_setup.xml
@@ -0,0 +1,25 @@
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:app="http://schemas.android.com/apk/res-auto"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent">
+
+  <android.support.v4.view.ViewPager
+    android:id="@+id/view_pager"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" />
+
+  <android.support.design.widget.FloatingActionButton
+    android:id="@+id/next_button"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="end|bottom"
+    android:layout_marginBottom="16dp"
+    android:layout_marginEnd="80dp"
+    android:layout_marginRight="80dp"
+    android:tint="#ffffff"
+    app:backgroundTint="#8A000000"
+    app:elevation="0dip"
+    app:fabSize="normal"
+    app:pressedTranslationZ="0dip" />
+
+</merge>
diff --git a/app/src/main/res/layout-sw600dp-land/setup_slide.xml b/app/src/main/res/layout-sw600dp-land/setup_slide.xml
new file mode 100644
index 000000000..ed8ebc7b2
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-land/setup_slide.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ QuasselDroid - Quassel client for Android
+  ~ Copyright (C) 2016 Janne Koschinski
+  ~ Copyright (C) 2016 Ken Børge Viktil
+  ~ Copyright (C) 2016 Magnus Fjell
+  ~ Copyright (C) 2016 Martin Sandsmark <martin.sandsmark@kde.org>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify it
+  ~ under the terms of the GNU General Public License as published by the Free
+  ~ Software Foundation, either version 3 of the License, or (at your option)
+  ~ any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License along
+  ~ with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:tools="http://schemas.android.com/tools"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  android:paddingLeft="64dp"
+  android:paddingRight="64dp">
+
+  <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/title"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_above="@+id/view"
+    android:layout_toLeftOf="@+id/scrollView"
+    android:layout_toStartOf="@+id/scrollView"
+    android:gravity="end"
+    android:paddingEnd="64dp"
+    android:paddingRight="64dp"
+    android:textSize="28sp"
+    android:theme="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse"
+    tools:text="Connection" />
+
+  <android.support.v4.widget.Space
+    android:id="@+id/view"
+    android:layout_width="match_parent"
+    android:layout_height="8dp"
+    android:layout_centerVertical="true"
+    android:layout_toLeftOf="@+id/scrollView"
+    android:layout_toStartOf="@+id/scrollView" />
+
+  <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/description"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_below="@+id/view"
+    android:layout_toLeftOf="@+id/scrollView"
+    android:layout_toStartOf="@+id/scrollView"
+    android:gravity="end"
+    android:paddingEnd="64dp"
+    android:paddingRight="64dp"
+    android:textSize="16sp"
+    android:theme="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle.Inverse"
+    tools:text="First, please choose which server your core is hosted on." />
+
+  <ScrollView
+    android:id="@+id/scrollView"
+    android:layout_width="400dp"
+    android:layout_height="wrap_content"
+    android:layout_alignParentBottom="true"
+    android:layout_alignParentEnd="true"
+    android:layout_alignParentRight="true">
+
+    <android.support.v7.widget.CardView
+      android:id="@+id/content_host"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:minHeight="400dp" />
+
+  </ScrollView>
+
+</RelativeLayout>
diff --git a/app/src/main/res/layout-sw600dp/activity_setup.xml b/app/src/main/res/layout-sw600dp/activity_setup.xml
new file mode 100644
index 000000000..dd0d8803e
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp/activity_setup.xml
@@ -0,0 +1,32 @@
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:app="http://schemas.android.com/apk/res-auto"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent">
+
+  <android.support.v4.view.ViewPager
+    android:id="@+id/view_pager"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" />
+
+  <FrameLayout
+    android:layout_width="400dp"
+    android:layout_height="wrap_content"
+    android:layout_gravity="bottom|center">
+
+    <android.support.design.widget.FloatingActionButton
+      android:id="@+id/next_button"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_gravity="end|bottom"
+      android:layout_marginBottom="16dp"
+      android:layout_marginEnd="16dp"
+      android:layout_marginRight="16dp"
+      android:tint="#ffffff"
+      app:backgroundTint="#8A000000"
+      app:elevation="0dip"
+      app:fabSize="normal"
+      app:pressedTranslationZ="0dip" />
+
+  </FrameLayout>
+
+</merge>
diff --git a/app/src/main/res/layout-sw600dp/setup_slide.xml b/app/src/main/res/layout-sw600dp/setup_slide.xml
new file mode 100644
index 000000000..7f40113c1
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp/setup_slide.xml
@@ -0,0 +1,46 @@
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:tools="http://schemas.android.com/tools"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent">
+
+  <LinearLayout
+    android:layout_width="400dp"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center_horizontal|bottom"
+    android:orientation="vertical">
+
+    <LinearLayout
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:layout_marginTop="64dp"
+      android:orientation="vertical"
+      android:padding="32dp">
+
+      <TextView
+        android:id="@+id/title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="8dp"
+        android:text="Connection"
+        android:textSize="28sp"
+        android:theme="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse" />
+
+      <TextView
+        android:id="@+id/description"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textSize="16sp"
+        android:theme="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle.Inverse"
+        tools:text="First, please choose which server your core is hosted on." />
+
+    </LinearLayout>
+
+    <android.support.v7.widget.CardView
+      android:id="@+id/content_host"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:minHeight="400dp" />
+
+  </LinearLayout>
+
+</ScrollView>
diff --git a/app/src/main/res/layout/activity_setup.xml b/app/src/main/res/layout/activity_setup.xml
new file mode 100644
index 000000000..6778c9ebc
--- /dev/null
+++ b/app/src/main/res/layout/activity_setup.xml
@@ -0,0 +1,25 @@
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:app="http://schemas.android.com/apk/res-auto"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent">
+
+  <android.support.v4.view.ViewPager
+    android:id="@+id/view_pager"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" />
+
+  <android.support.design.widget.FloatingActionButton
+    android:id="@+id/next_button"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="end|bottom"
+    android:layout_marginBottom="16dp"
+    android:layout_marginEnd="16dp"
+    android:layout_marginRight="16dp"
+    android:tint="#ffffff"
+    app:backgroundTint="#8A808080"
+    app:elevation="0dip"
+    app:fabSize="normal"
+    app:pressedTranslationZ="0dip" />
+
+</merge>
diff --git a/app/src/main/res/layout/setup_account_connection.xml b/app/src/main/res/layout/setup_account_connection.xml
new file mode 100644
index 000000000..71cab8049
--- /dev/null
+++ b/app/src/main/res/layout/setup_account_connection.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ QuasselDroid - Quassel client for Android
+  ~ Copyright (C) 2016 Janne Koschinski
+  ~ Copyright (C) 2016 Ken Børge Viktil
+  ~ Copyright (C) 2016 Magnus Fjell
+  ~ Copyright (C) 2016 Martin Sandsmark <martin.sandsmark@kde.org>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify it
+  ~ under the terms of the GNU General Public License as published by the Free
+  ~ Software Foundation, either version 3 of the License, or (at your option)
+  ~ any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License along
+  ~ with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  android:orientation="vertical"
+  android:padding="32dp">
+
+  <android.support.design.widget.TextInputLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:hint="@string/labelConnectionHostname">
+
+    <android.support.design.widget.TextInputEditText
+      android:id="@+id/host"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:inputType="textUri" />
+  </android.support.design.widget.TextInputLayout>
+
+  <android.support.design.widget.TextInputLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:hint="@string/labelConnectionPort">
+
+    <android.support.design.widget.TextInputEditText
+      android:id="@+id/port"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:inputType="number"
+      android:text="@string/defaultConnectionPort" />
+  </android.support.design.widget.TextInputLayout>
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/setup_account_name.xml b/app/src/main/res/layout/setup_account_name.xml
new file mode 100644
index 000000000..86c8f1026
--- /dev/null
+++ b/app/src/main/res/layout/setup_account_name.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ QuasselDroid - Quassel client for Android
+  ~ Copyright (C) 2016 Janne Koschinski
+  ~ Copyright (C) 2016 Ken Børge Viktil
+  ~ Copyright (C) 2016 Magnus Fjell
+  ~ Copyright (C) 2016 Martin Sandsmark <martin.sandsmark@kde.org>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify it
+  ~ under the terms of the GNU General Public License as published by the Free
+  ~ Software Foundation, either version 3 of the License, or (at your option)
+  ~ any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License along
+  ~ with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  android:orientation="vertical"
+  android:padding="32dp">
+
+  <android.support.design.widget.TextInputLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:hint="@string/labelAccountName">
+
+    <android.support.design.widget.TextInputEditText
+      android:id="@+id/name"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:inputType="text" />
+  </android.support.design.widget.TextInputLayout>
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/setup_account_user.xml b/app/src/main/res/layout/setup_account_user.xml
new file mode 100644
index 000000000..dd2394f82
--- /dev/null
+++ b/app/src/main/res/layout/setup_account_user.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ QuasselDroid - Quassel client for Android
+  ~ Copyright (C) 2016 Janne Koschinski
+  ~ Copyright (C) 2016 Ken Børge Viktil
+  ~ Copyright (C) 2016 Magnus Fjell
+  ~ Copyright (C) 2016 Martin Sandsmark <martin.sandsmark@kde.org>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify it
+  ~ under the terms of the GNU General Public License as published by the Free
+  ~ Software Foundation, either version 3 of the License, or (at your option)
+  ~ any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License along
+  ~ with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  android:orientation="vertical"
+  android:padding="32dp">
+
+  <android.support.design.widget.TextInputLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:hint="@string/labelAccountUsername">
+
+    <android.support.design.widget.TextInputEditText
+      android:id="@+id/user"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:inputType="textVisiblePassword|textNoSuggestions" />
+  </android.support.design.widget.TextInputLayout>
+
+  <android.support.design.widget.TextInputLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:hint="@string/labelAccountPassword">
+
+    <android.support.design.widget.TextInputEditText
+      android:id="@+id/pass"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:inputType="textPassword" />
+  </android.support.design.widget.TextInputLayout>
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/setup_slide.xml b/app/src/main/res/layout/setup_slide.xml
new file mode 100644
index 000000000..3cc29475b
--- /dev/null
+++ b/app/src/main/res/layout/setup_slide.xml
@@ -0,0 +1,44 @@
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:tools="http://schemas.android.com/tools"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent">
+
+  <LinearLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <LinearLayout
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:background="?attr/colorPrimary"
+      android:orientation="vertical"
+      android:padding="32dp">
+
+      <TextView
+        android:id="@+id/title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="32dp"
+        android:textSize="28sp"
+        android:theme="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse"
+        tools:text="Connection" />
+
+      <TextView
+        android:id="@+id/description"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textSize="16sp"
+        android:theme="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle.Inverse"
+        tools:text="First, please choose which server your core is hosted on." />
+
+    </LinearLayout>
+
+    <FrameLayout
+      android:id="@+id/content_host"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content" />
+
+  </LinearLayout>
+
+</ScrollView>
diff --git a/app/src/main/res/values-sw600dp/styles.xml b/app/src/main/res/values-sw600dp/styles.xml
new file mode 100644
index 000000000..62a617628
--- /dev/null
+++ b/app/src/main/res/values-sw600dp/styles.xml
@@ -0,0 +1,6 @@
+<resources>
+
+  <style name="SetupTheme" parent="AppTheme.NoActionBar">
+    <item name="android:windowBackground">?attr/colorPrimary</item>
+  </style>
+</resources>
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 3d2127148..04b6d213e 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,6 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-  <color name="colorPrimary">#0a70c0</color>
+  <color name="colorPrimary">#ca241f</color>
   <color name="colorPrimaryDark">#105a94</color>
   <color name="colorAccent">#ffaf3b</color>
+
+  <attr name="backgroundSetup">?attr/color</attr>
 </resources>
diff --git a/app/src/main/res/values/defaults.xml b/app/src/main/res/values/defaults.xml
new file mode 100644
index 000000000..1a915421c
--- /dev/null
+++ b/app/src/main/res/values/defaults.xml
@@ -0,0 +1,3 @@
+<resources>
+  <string name="defaultConnectionPort" translatable="false">4242</string>
+</resources>
diff --git a/app/src/main/res/values/strings_setup.xml b/app/src/main/res/values/strings_setup.xml
new file mode 100644
index 000000000..4cd5b5f9b
--- /dev/null
+++ b/app/src/main/res/values/strings_setup.xml
@@ -0,0 +1,33 @@
+<resources>
+  <!-- Account Connection -->
+  <string name="slideAccountConnectionTitle">Connection</string>
+  <string name="slideAccountConnectionDescription">First, please choose which server your core is hosted on.</string>
+
+  <string name="labelConnectionHostname">Host</string>
+  <string name="labelConnectionPort">Port</string>
+
+  <!-- Account User -->
+  <string name="slideAccountUserTitle">Your Account</string>
+  <string name="slideAccountUserDescription">Now, please enter the username and password for your account on the core. If you just created that core, we’ll set up this account for you</string>
+
+  <string name="labelAccountUsername">Username</string>
+  <string name="labelAccountPassword">Password</string>
+
+  <!-- Account Name -->
+  <string name="slideAccountNameTitle">Customize Account</string>
+  <string name="slideAccountNameDescription">Give this account a name and icon</string>
+
+  <string name="labelAccountName">Account name</string>
+
+  <!-- Core Authenticator Select -->
+  <string name="slideCoreAuthenticatorSelectTitle">Select Authentication Backend</string>
+  <string name="slideCoreAuthenticatorSelectDescription">Please select an authentication backend for the Quassel Core to use for authenticating users.</string>
+
+  <!-- Core Backend Select -->
+  <string name="slideCoreBackendSelectTitle">Select Storage Backend</string>
+  <string name="slideCoreBackendSelectDescription">Please select a database backend for the Quassel Core storage to store the backlog and other data in.</string>
+
+  <!-- Core Backend Config -->
+  <string name="slideCoreBackendSetupTitle">Configure Storage Backend</string>
+  <string name="slideCoreBackendSetupDescription">Please configure the selected database backend.</string>
+</resources>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 3c8869669..965fd7437 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -6,6 +6,14 @@
     <item name="colorAccent">@color/colorAccent</item>
   </style>
 
+  <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.Light.NoActionBar">
+    <item name="colorPrimary">@color/colorPrimary</item>
+    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+    <item name="colorAccent">@color/colorAccent</item>
+  </style>
+
+  <style name="SetupTheme" parent="AppTheme.NoActionBar" />
+
   <style name="RaisedButton" parent="AppTheme">
     <item name="colorButtonNormal">?attr/background</item>
   </style>
@@ -14,11 +22,7 @@
     <item name="colorButtonNormal">@color/colorAccent</item>
   </style>
 
-  <style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">
-    <item name="colorPrimary">@color/colorPrimaryDark</item>
-    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
-    <item name="colorAccent">@color/colorAccent</item>
-
+  <style name="SplashTheme" parent="AppTheme.NoActionBar">
     <item name="android:windowBackground">@drawable/bg_splash</item>
   </style>
 </resources>
diff --git a/build.gradle.kts b/build.gradle.kts
index 6228f732b..ad56ba870 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -13,5 +13,8 @@ allprojects {
   repositories {
     google()
     jcenter()
+    maven {
+      url = uri("https://jitpack.io")
+    }
   }
 }
-- 
GitLab