From ea8f96b97ad94fd5b7b02acaa04a7a3b848cb9bc Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Thu, 28 Sep 2017 13:13:09 +0200
Subject: [PATCH] Fixed problems with setup UI scrolling and validation

---
 app/build.gradle.kts                          |   2 +
 .../java/de/kuschku/quasseldroid_ng/Keys.kt   |  10 ++
 .../kuschku/quasseldroid_ng/QuasseldroidNG.kt |   6 +-
 .../quasseldroid_ng/service/QuasselService.kt |   2 +-
 .../quasseldroid_ng/ui/ChatActivity.kt        |  17 +--
 .../quasseldroid_ng/ui/setup/SetupActivity.kt |   7 +
 .../ui/setup/accounts/AccountAdapter.kt       |  90 ++++++-----
 .../ui/setup/accounts/AccountEditActivity.kt  | 129 ++++++++++++----
 .../accounts/AccountSelectionActivity.kt      |  31 ++--
 .../setup/accounts/AccountSelectionSlide.kt   |  22 ++-
 .../accounts/AccountSetupConnectionSlide.kt   |  50 ++++---
 .../setup/accounts/AccountSetupNameSlide.kt   |  31 ++--
 .../setup/accounts/AccountSetupUserSlide.kt   |  36 ++---
 .../kuschku/quasseldroid_ng/util/Patterns.kt  |  64 ++++++++
 .../quasseldroid_ng/util/TextValidator.kt     |  22 +++
 .../AndroidCompatibilityUtils.kt              |   2 +-
 .../AndroidHandlerService.kt                  |   2 +-
 .../AndroidLoggingHandler.kt                  |   9 +-
 .../AndroidStreamChannelFactory.kt            |   2 +-
 app/src/main/res/layout/activity_main.xml     |   6 +-
 app/src/main/res/layout/activity_setup.xml    |   5 +-
 .../res/layout/setup_account_connection.xml   |   4 +-
 .../main/res/layout/setup_account_edit.xml    | 140 ++++++++----------
 .../main/res/layout/setup_account_name.xml    |   1 +
 .../main/res/layout/setup_account_user.xml    |   6 +-
 app/src/main/res/layout/setup_slide.xml       |  11 +-
 app/src/main/res/menu/setup_edit_account.xml  |   6 +-
 app/src/main/res/values/strings_setup.xml     |  13 +-
 app/src/main/res/values/styles.xml            |   7 +-
 29 files changed, 469 insertions(+), 264 deletions(-)
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/Keys.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/Patterns.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/TextValidator.kt
 rename app/src/main/java/de/kuschku/quasseldroid_ng/util/{ => compatibility}/AndroidCompatibilityUtils.kt (95%)
 rename app/src/main/java/de/kuschku/quasseldroid_ng/util/{ => compatibility}/AndroidHandlerService.kt (96%)
 rename app/src/main/java/de/kuschku/quasseldroid_ng/util/{ => compatibility}/AndroidLoggingHandler.kt (80%)
 rename app/src/main/java/de/kuschku/quasseldroid_ng/util/{ => compatibility}/AndroidStreamChannelFactory.kt (92%)

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 4c7351c1c..414ed3f05 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -79,6 +79,8 @@ dependencies {
   implementation(appCompat("recyclerview-v7"))
   implementation("com.android.support.constraint:constraint-layout:1.0.2")
 
+  implementation("com.github.StephenVinouze.AdvancedRecyclerView:core:1.1.6")
+
   implementation("io.reactivex.rxjava2:rxjava:2.1.3")
 
   implementation(appArch("lifecycle", "runtime", version = "1.0.0"))
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/Keys.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/Keys.kt
new file mode 100644
index 000000000..4fa19496a
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/Keys.kt
@@ -0,0 +1,10 @@
+package de.kuschku.quasseldroid_ng
+
+object Keys {
+  object Status {
+    const val NAME = "status"
+
+    const val selectedAccount = "selectedAccount"
+    const val reconnect = "reconnect"
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/QuasseldroidNG.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/QuasseldroidNG.kt
index 684888bc8..f6e4c396a 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/QuasseldroidNG.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/QuasseldroidNG.kt
@@ -6,9 +6,9 @@ import android.content.pm.ShortcutInfo
 import android.content.pm.ShortcutManager
 import android.graphics.drawable.Icon
 import android.os.Build
-import de.kuschku.quasseldroid_ng.util.AndroidCompatibilityUtils
-import de.kuschku.quasseldroid_ng.util.AndroidLoggingHandler
-import de.kuschku.quasseldroid_ng.util.AndroidStreamChannelFactory
+import de.kuschku.quasseldroid_ng.util.compatibility.AndroidCompatibilityUtils
+import de.kuschku.quasseldroid_ng.util.compatibility.AndroidLoggingHandler
+import de.kuschku.quasseldroid_ng.util.compatibility.AndroidStreamChannelFactory
 import de.kuschku.quasseldroid_ng.util.helper.systemService
 import org.acra.ACRA
 import org.acra.ReportingInteractionMode
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt
index 250be610d..ed212b2e0 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt
@@ -13,7 +13,7 @@ import de.kuschku.libquassel.session.SocketAddress
 import de.kuschku.quasseldroid_ng.BuildConfig
 import de.kuschku.quasseldroid_ng.R
 import de.kuschku.quasseldroid_ng.persistence.QuasselDatabase
-import de.kuschku.quasseldroid_ng.util.AndroidHandlerService
+import de.kuschku.quasseldroid_ng.util.compatibility.AndroidHandlerService
 import org.threeten.bp.Instant
 import java.security.cert.X509Certificate
 import javax.net.ssl.X509TrustManager
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/ChatActivity.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/ChatActivity.kt
index 9e198fc6f..8cf87b3f5 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/ChatActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/ChatActivity.kt
@@ -20,6 +20,7 @@ import de.kuschku.libquassel.session.Backend
 import de.kuschku.libquassel.session.ConnectionState
 import de.kuschku.libquassel.session.SocketAddress
 import de.kuschku.libquassel.util.compatibility.LoggingHandler
+import de.kuschku.quasseldroid_ng.Keys
 import de.kuschku.quasseldroid_ng.R
 import de.kuschku.quasseldroid_ng.persistence.AccountDatabase
 import de.kuschku.quasseldroid_ng.service.QuasselService
@@ -97,15 +98,15 @@ class ChatActivity : ServiceBoundActivity() {
 
     val database = AccountDatabase.Creator.init(this)
     handler.post {
-      val accountId = getSharedPreferences("status", Context.MODE_PRIVATE)
-        ?.getLong(selectedAccountKey, -1) ?: -1
+      val accountId = getSharedPreferences(Keys.Status.NAME, Context.MODE_PRIVATE)
+        ?.getLong(Keys.Status.selectedAccount, -1) ?: -1
       if (accountId == -1L) {
-        setResult(Activity.RESULT_CANCELED)
+        setResult(Activity.RESULT_OK)
         finish()
       }
       val it = database.accounts().findById(accountId)
       if (it == null) {
-        setResult(Activity.RESULT_CANCELED)
+        setResult(Activity.RESULT_OK)
         finish()
       }
       account = it
@@ -149,8 +150,8 @@ class ChatActivity : ServiceBoundActivity() {
 
   override fun onOptionsItemSelected(item: MenuItem?) = when (item?.itemId) {
     R.id.disconnect -> {
-      getSharedPreferences("status", Context.MODE_PRIVATE).editApply {
-        putBoolean("reconnect", false)
+      getSharedPreferences(Keys.Status.NAME, Context.MODE_PRIVATE).editApply {
+        putBoolean(Keys.Status.reconnect, false)
       }
       finish()
       true
@@ -167,8 +168,4 @@ class ChatActivity : ServiceBoundActivity() {
     LoggingHandler.loggingHandlers.remove(logHandler)
     super.onStop()
   }
-
-  companion object {
-    private const val selectedAccountKey = "selectedAccount"
-  }
 }
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
index 202ff734e..8991a7e38 100644
--- 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
@@ -176,6 +176,13 @@ abstract class SetupActivity : AppCompatActivity() {
     }
   }
 
+  override fun onBackPressed() {
+    if (viewPager.currentItem == 0)
+      super.onBackPressed()
+    else
+      viewPager.currentItem -= 1
+  }
+
   companion object {
     private const val currentItemKey = ":setupActivity:currentItem"
     private const val lastValidItemKey = ":setupActivity:lastValidItem"
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountAdapter.kt
index cd2214cc0..dc2a18205 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountAdapter.kt
@@ -1,6 +1,5 @@
 package de.kuschku.quasseldroid_ng.ui.setup.accounts
 
-import android.arch.lifecycle.MutableLiveData
 import android.arch.paging.PagedListAdapter
 import android.support.v7.recyclerview.extensions.DiffCallback
 import android.support.v7.widget.AppCompatImageButton
@@ -19,18 +18,12 @@ class AccountAdapter :
   PagedListAdapter<AccountDatabase.Account, AccountAdapter.AccountViewHolder>(DIFF_CALLBACK) {
   private val actionListeners = mutableSetOf<(Long) -> Unit>()
   private val addListeners = mutableSetOf<() -> Unit>()
-  val selectedItemId: Long
-    get() {
-      val position = selectedPos.value
-      return if (position != null && position > -1 && position < super.getItemCount())
-        getItem(position)?.id ?: -1
-      else
-        -1
-    }
+  private val selectionListeners = mutableSetOf<(Long) -> Unit>()
 
   private val clickListener = object : ItemListener {
     override fun onAction(id: Long, pos: Int) {
-      changeSelection(id, pos)
+      notifySelectionChanged(selectedItemView, pos)
+      selectionListener.invoke(id)
     }
   }
 
@@ -42,6 +35,17 @@ class AccountAdapter :
     }
   }
 
+  private val selectionListener = { id: Long ->
+    selectedItemId = id
+    for (selectionListener in selectionListeners) {
+      selectionListener.invoke(id)
+    }
+  }
+
+  private var selectedItemView = -1
+  var selectedItemId = -1L
+    private set
+
   private val addListener = {
     for (addListener in addListeners) {
       addListener.invoke()
@@ -64,6 +68,14 @@ class AccountAdapter :
     addListeners.remove(f)
   }
 
+  fun addSelectionListener(f: (Long) -> Unit) {
+    selectionListeners.add(f)
+  }
+
+  fun removeSelectionListener(f: (Long) -> Unit) {
+    selectionListeners.remove(f)
+  }
+
   override fun onBindViewHolder(holder: AccountViewHolder, position: Int) {
     when (holder) {
       is AccountViewHolder.Item -> {
@@ -71,12 +83,11 @@ class AccountAdapter :
         if (account == null) {
           holder.clear()
         } else {
-          val selected = selectedId == account.id
-          holder.bind(account, position, selected)
-          if (selected && position != selectedPos.value)
-            holder.itemView.post {
-              changeSelection(account.id, position)
-            }
+          val selected = account.id == selectedItemId
+          if (selected) {
+            selectedItemView = position
+          }
+          holder.bind(account, selected)
         }
       }
       is AccountViewHolder.Add  -> {
@@ -123,37 +134,23 @@ class AccountAdapter :
     }
   }
 
-  private fun notifySelectionChanged(from: Int?, to: Int?) {
-    if (from != null && from != -1)
-      notifyItemChanged(from)
-
-    val real_to = to ?: -1
-    selectedPos.value = real_to
-
-    if (to != null && to != -1)
-      notifyItemChanged(to)
+  fun selectAccount(id: Long) {
+    selectedItemView = -1
+    selectionListener(id)
   }
 
-  fun changeSelection(id: Long, position: Int) {
-    notifySelectionChanged(selectedPos.value, position)
-    selectedId = id
-  }
+  fun notifySelectionChanged(from: Int?, to: Int?) {
+    val _from = from ?: -1
+    val _to = to ?: -1
 
-  private fun indexOf(id: Long) = (0 until itemCount).lastOrNull {
-    getItemViewType(it) == TYPE_ACCOUNT && getItem(it)?.id == id
-  } ?: -1
+    if (_from != -1)
+      notifyItemChanged(_from)
 
-  fun selectAccount(id: Long) {
-    val index = indexOf(id)
-    if (index != -1) {
-      changeSelection(id, index)
-    } else {
-      selectedId = id
-    }
-  }
+    selectedItemView = _to
 
-  private var selectedId = -1L
-  var selectedPos = MutableLiveData<Int>()
+    if (_to != -1)
+      notifyItemChanged(_to)
+  }
 
   interface ItemListener {
     fun onAction(id: Long, pos: Int)
@@ -175,20 +172,18 @@ class AccountAdapter :
       lateinit var accountEdit: AppCompatImageButton
 
       private var id = -1L
-      private var index = -1
 
       init {
         ButterKnife.bind(this, itemView)
         accountEdit.setOnClickListener {
-          actionListener.onAction(id, index)
+          actionListener.onAction(id, adapterPosition)
         }
         itemView.setOnClickListener {
-          clickListener.onAction(id, index)
+          clickListener.onAction(id, adapterPosition)
         }
       }
 
-      fun bind(account: AccountDatabase.Account, position: Int, selected: Boolean) {
-        index = position
+      fun bind(account: AccountDatabase.Account, selected: Boolean) {
         id = account.id
         accountName.text = account.name
         accountDescription.text = itemView.context.resources.getString(
@@ -198,7 +193,6 @@ class AccountAdapter :
       }
 
       fun clear() {
-        index = -1
         id = -1L
         accountName.text = ""
         accountDescription.text = ""
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountEditActivity.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountEditActivity.kt
index 8992f3c05..d6500de75 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountEditActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountEditActivity.kt
@@ -1,38 +1,51 @@
 package de.kuschku.quasseldroid_ng.ui.setup.accounts
 
 import android.app.Activity
+import android.content.Context
 import android.os.Bundle
 import android.os.Handler
 import android.os.HandlerThread
-import android.support.design.widget.FloatingActionButton
+import android.support.design.widget.TextInputEditText
+import android.support.design.widget.TextInputLayout
 import android.support.v7.app.AlertDialog
 import android.support.v7.app.AppCompatActivity
+import android.text.Editable
 import android.view.Menu
 import android.view.MenuItem
-import android.widget.TextView
 import butterknife.BindView
 import butterknife.ButterKnife
+import de.kuschku.quasseldroid_ng.Keys
 import de.kuschku.quasseldroid_ng.R
 import de.kuschku.quasseldroid_ng.persistence.AccountDatabase
+import de.kuschku.quasseldroid_ng.util.Patterns
+import de.kuschku.quasseldroid_ng.util.TextValidator
+import de.kuschku.quasseldroid_ng.util.helper.editCommit
 
 class AccountEditActivity : AppCompatActivity() {
+  @BindView(R.id.nameWrapper)
+  lateinit var nameWrapper: TextInputLayout
   @BindView(R.id.name)
-  lateinit var name: TextView
+  lateinit var name: TextInputEditText
 
+  @BindView(R.id.hostWrapper)
+  lateinit var hostWrapper: TextInputLayout
   @BindView(R.id.host)
-  lateinit var host: TextView
+  lateinit var host: TextInputEditText
 
+  @BindView(R.id.portWrapper)
+  lateinit var portWrapper: TextInputLayout
   @BindView(R.id.port)
-  lateinit var port: TextView
+  lateinit var port: TextInputEditText
 
+  @BindView(R.id.userWrapper)
+  lateinit var userWrapper: TextInputLayout
   @BindView(R.id.user)
-  lateinit var user: TextView
+  lateinit var user: TextInputEditText
 
+  @BindView(R.id.passWrapper)
+  lateinit var passWrapper: TextInputLayout
   @BindView(R.id.pass)
-  lateinit var pass: TextView
-
-  @BindView(R.id.save_button)
-  lateinit var saveButton: FloatingActionButton
+  lateinit var pass: TextInputEditText
 
   private var accountId: Long = -1
   private var account: AccountDatabase.Account? = null
@@ -50,22 +63,6 @@ class AccountEditActivity : AppCompatActivity() {
     setContentView(R.layout.setup_account_edit)
     ButterKnife.bind(this)
 
-    saveButton.setOnClickListener {
-      val it = account
-      if (it != null) {
-        it.name = name.text.toString()
-        it.host = host.text.toString()
-        it.port = port.text.toString().toIntOrNull() ?: 4242
-        it.user = user.text.toString()
-        it.pass = pass.text.toString()
-        handler.post {
-          database.accounts().save(it)
-          setResult(Activity.RESULT_OK)
-          finish()
-        }
-      }
-    }
-
     database = AccountDatabase.Creator.init(this)
     handler.post {
       accountId = intent.getLongExtra("account", -1)
@@ -79,14 +76,60 @@ class AccountEditActivity : AppCompatActivity() {
         finish()
       }
 
-      name.text = account?.name
-      host.text = account?.host
-      port.text = account?.port?.toString()
-      user.text = account?.user
-      pass.text = account?.pass
+      name.setText(account?.name)
+      host.setText(account?.host)
+      port.setText(account?.port?.toString())
+      user.setText(account?.user)
+      pass.setText(account?.pass)
+    }
+
+    nameValidator = object : TextValidator(
+      nameWrapper::setError, resources.getString(R.string.hintInvalidName)
+    ) {
+      override fun validate(text: Editable)
+        = text.isNotBlank()
+    }
+
+    hostValidator = object : TextValidator(
+      hostWrapper::setError, resources.getString(R.string.hintInvalidHost)
+    ) {
+      override fun validate(text: Editable)
+        = text.toString().matches(Patterns.DOMAIN_NAME.toRegex())
     }
+
+    portValidator = object : TextValidator(
+      portWrapper::setError, resources.getString(R.string.hintInvalidPort)
+    ) {
+      override fun validate(text: Editable)
+        = text.toString().toIntOrNull() in (0 until 65536)
+    }
+
+    userValidator = object : TextValidator(
+      userWrapper::setError, resources.getString(R.string.hintInvalidUser)
+    ) {
+      override fun validate(text: Editable)
+        = text.isNotBlank()
+    }
+
+    name.addTextChangedListener(nameValidator)
+    host.addTextChangedListener(hostValidator)
+    port.addTextChangedListener(portValidator)
+    user.addTextChangedListener(userValidator)
+    nameValidator.afterTextChanged(name.text)
+    hostValidator.afterTextChanged(host.text)
+    portValidator.afterTextChanged(port.text)
+    userValidator.afterTextChanged(user.text)
   }
 
+  private lateinit var nameValidator: TextValidator
+  private lateinit var hostValidator: TextValidator
+  private lateinit var portValidator: TextValidator
+  private lateinit var userValidator: TextValidator
+
+  private val isValid
+    get() = nameValidator.isValid && hostValidator.isValid && portValidator.isValid
+      && userValidator.isValid
+
   override fun onCreateOptionsMenu(menu: Menu?): Boolean {
     menuInflater.inflate(R.menu.setup_edit_account, menu)
     return super.onCreateOptionsMenu(menu)
@@ -106,6 +149,12 @@ class AccountEditActivity : AppCompatActivity() {
           val it = account
           if (it != null)
             handler.post {
+              val preferences = getSharedPreferences(Keys.Status.NAME, Context.MODE_PRIVATE)
+              if (preferences.getLong(Keys.Status.selectedAccount, -1) == it.id) {
+                preferences.editCommit {
+                  remove(Keys.Status.selectedAccount)
+                }
+              }
               database.accounts().delete(it)
             }
           setResult(Activity.RESULT_OK)
@@ -117,6 +166,24 @@ class AccountEditActivity : AppCompatActivity() {
         .show()
       true
     }
+    R.id.save   -> {
+      if (isValid) {
+        val it = account
+        if (it != null) {
+          it.name = name.text.toString()
+          it.host = host.text.toString()
+          it.port = port.text.toString().toIntOrNull() ?: 4242
+          it.user = user.text.toString()
+          it.pass = pass.text.toString()
+          handler.post {
+            database.accounts().save(it)
+            setResult(Activity.RESULT_OK)
+            finish()
+          }
+        }
+      }
+      true
+    }
     else        -> super.onOptionsItemSelected(item)
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSelectionActivity.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSelectionActivity.kt
index 437f31369..e44638799 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSelectionActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSelectionActivity.kt
@@ -1,41 +1,52 @@
 package de.kuschku.quasseldroid_ng.ui.setup.accounts
 
+import android.app.Activity
 import android.content.Context
 import android.content.Intent
 import android.content.SharedPreferences
 import android.os.Bundle
+import de.kuschku.quasseldroid_ng.Keys
 import de.kuschku.quasseldroid_ng.ui.ChatActivity
 import de.kuschku.quasseldroid_ng.ui.setup.SetupActivity
 import de.kuschku.quasseldroid_ng.util.helper.editCommit
 
 class AccountSelectionActivity : SetupActivity() {
+  companion object {
+    const val REQUEST_CHAT = 0
+    const val REQUEST_CREATE_FIRST = 1
+    const val REQUEST_CREATE_NEW = 2
+  }
+
   override val fragments = listOf(
     AccountSelectionSlide()
   )
 
-  lateinit var statusPreferences: SharedPreferences
+  private lateinit var statusPreferences: SharedPreferences
   override fun onDone(data: Bundle) {
     statusPreferences.editCommit {
-      putLong(selectedAccountKey, data.getLong(selectedAccountKey, -1))
-      putBoolean("reconnect", true)
+      putLong(Keys.Status.selectedAccount, data.getLong(Keys.Status.selectedAccount, -1))
+      putBoolean(Keys.Status.reconnect, true)
     }
     startActivity(Intent(this, ChatActivity::class.java))
   }
 
   override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
-    statusPreferences = this.getSharedPreferences("status", Context.MODE_PRIVATE)
+    statusPreferences = this.getSharedPreferences(Keys.Status.NAME, Context.MODE_PRIVATE)
     val data = Bundle()
-    val selectedAccount = statusPreferences.getLong(selectedAccountKey, -1)
-    data.putLong(selectedAccountKey, selectedAccount)
+    val selectedAccount = statusPreferences.getLong(Keys.Status.selectedAccount, -1)
+    data.putLong(Keys.Status.selectedAccount, selectedAccount)
     setInitData(data)
 
-    if (statusPreferences.getBoolean("reconnect", false) && selectedAccount != -1L) {
-      startActivity(Intent(this, ChatActivity::class.java))
+    if (statusPreferences.getBoolean(Keys.Status.reconnect, false) && selectedAccount != -1L) {
+      startActivityForResult(Intent(this, ChatActivity::class.java), REQUEST_CHAT)
     }
   }
 
-  companion object {
-    private const val selectedAccountKey = "selectedAccount"
+  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+    super.onActivityResult(requestCode, resultCode, data)
+    if (requestCode == REQUEST_CHAT && resultCode == Activity.RESULT_CANCELED) {
+      finish()
+    }
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSelectionSlide.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSelectionSlide.kt
index 094c72ba7..7e5718e59 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSelectionSlide.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSelectionSlide.kt
@@ -1,5 +1,6 @@
 package de.kuschku.quasseldroid_ng.ui.setup.accounts
 
+import android.app.Activity
 import android.arch.lifecycle.Observer
 import android.arch.lifecycle.ViewModelProviders
 import android.arch.paging.PagedList
@@ -16,12 +17,14 @@ import butterknife.ButterKnife
 import de.kuschku.quasseldroid_ng.R
 import de.kuschku.quasseldroid_ng.persistence.AccountDatabase
 import de.kuschku.quasseldroid_ng.ui.setup.SlideFragment
+import de.kuschku.quasseldroid_ng.ui.setup.accounts.AccountSelectionActivity.Companion.REQUEST_CREATE_FIRST
+import de.kuschku.quasseldroid_ng.ui.setup.accounts.AccountSelectionActivity.Companion.REQUEST_CREATE_NEW
 
 class AccountSelectionSlide : SlideFragment() {
   @BindView(R.id.account_list)
   lateinit var accountList: RecyclerView
 
-  override fun isValid() = adapter.selectedPos.value ?: -1 != -1
+  override fun isValid() = adapter.selectedItemId != -1L
 
   override val title = R.string.slideAccountSelectTitle
   override val description = R.string.slideAccountSelectDescription
@@ -45,7 +48,8 @@ class AccountSelectionSlide : SlideFragment() {
     val firstObserver = object : Observer<PagedList<AccountDatabase.Account>?> {
       override fun onChanged(t: PagedList<AccountDatabase.Account>?) {
         if (t?.isEmpty() != false)
-          startActivityForResult(Intent(context, AccountSetupActivity::class.java), -1)
+          startActivityForResult(Intent(context, AccountSetupActivity::class.java),
+                                 REQUEST_CREATE_FIRST)
         accountViewmodel.accounts.removeObserver(this)
       }
     }
@@ -54,19 +58,25 @@ class AccountSelectionSlide : SlideFragment() {
     accountList.layoutManager = LinearLayoutManager(context)
     accountList.itemAnimator = DefaultItemAnimator()
     accountList.adapter = adapter
-    adapter.selectedPos.observe(this, Observer {
-      updateValidity()
-    })
     adapter.addAddListener {
       startActivityForResult(Intent(context, AccountSetupActivity::class.java), -1)
     }
     adapter.addEditListener { id ->
       val intent = Intent(context, AccountEditActivity::class.java)
       intent.putExtra("account", id)
-      startActivityForResult(intent, -1)
+      startActivityForResult(intent, REQUEST_CREATE_NEW)
+    }
+    adapter.addSelectionListener {
+      updateValidity()
     }
 
     return view
   }
 
+  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+    super.onActivityResult(requestCode, resultCode, data)
+    if (requestCode == REQUEST_CREATE_FIRST && resultCode == Activity.RESULT_CANCELED) {
+      activity.finish()
+    }
+  }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSetupConnectionSlide.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSetupConnectionSlide.kt
index dd16c6ddf..28d100c6b 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSetupConnectionSlide.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSetupConnectionSlide.kt
@@ -2,8 +2,8 @@ package de.kuschku.quasseldroid_ng.ui.setup.accounts
 
 import android.os.Bundle
 import android.support.design.widget.TextInputEditText
+import android.support.design.widget.TextInputLayout
 import android.text.Editable
-import android.text.TextWatcher
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -11,22 +11,23 @@ import butterknife.BindView
 import butterknife.ButterKnife
 import de.kuschku.quasseldroid_ng.R
 import de.kuschku.quasseldroid_ng.ui.setup.SlideFragment
+import de.kuschku.quasseldroid_ng.util.Patterns
+import de.kuschku.quasseldroid_ng.util.TextValidator
 
 class AccountSetupConnectionSlide : SlideFragment() {
+  @BindView(R.id.hostWrapper)
+  lateinit var hostWrapper: TextInputLayout
   @BindView(R.id.host)
   lateinit var hostField: TextInputEditText
 
+  @BindView(R.id.portWrapper)
+  lateinit var portWrapper: TextInputLayout
+
   @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()
+    return hostValidator.isValid && portValidator.isValid
   }
 
   override val title = R.string.slideAccountConnectionTitle
@@ -49,17 +50,30 @@ class AccountSetupConnectionSlide : SlideFragment() {
                                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
-  }
+    hostValidator = object : TextValidator(
+      hostWrapper::setError, resources.getString(R.string.hintInvalidHost)
+    ) {
+      override fun validate(text: Editable)
+        = text.toString().matches(Patterns.DOMAIN_NAME.toRegex())
+
+      override fun onChanged() = updateValidity()
+    }
+    portValidator = object : TextValidator(
+      portWrapper::setError, resources.getString(R.string.hintInvalidPort)
+    ) {
+      override fun validate(text: Editable)
+        = text.toString().toIntOrNull() in (0 until 65536)
 
-  override fun onDestroyView() {
-    hostField.removeTextChangedListener(textWatcher)
-    portField.removeTextChangedListener(textWatcher)
-    super.onDestroyView()
+      override fun onChanged() = updateValidity()
+    }
+
+    hostField.addTextChangedListener(hostValidator)
+    portField.addTextChangedListener(portValidator)
+    hostValidator.afterTextChanged(hostField.text)
+    portValidator.afterTextChanged(portField.text)
+    return view
   }
 
-  private fun validHost() = hostField.text.isNotEmpty()
-  private fun validPort() = (0 until 65536).contains(portField.text.toString().toIntOrNull())
+  private lateinit var hostValidator: TextValidator
+  private lateinit var portValidator: TextValidator
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSetupNameSlide.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSetupNameSlide.kt
index 026752a2e..33d68251b 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSetupNameSlide.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSetupNameSlide.kt
@@ -2,8 +2,8 @@ package de.kuschku.quasseldroid_ng.ui.setup.accounts
 
 import android.os.Bundle
 import android.support.design.widget.TextInputEditText
+import android.support.design.widget.TextInputLayout
 import android.text.Editable
-import android.text.TextWatcher
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -11,19 +11,16 @@ import butterknife.BindView
 import butterknife.ButterKnife
 import de.kuschku.quasseldroid_ng.R
 import de.kuschku.quasseldroid_ng.ui.setup.SlideFragment
+import de.kuschku.quasseldroid_ng.util.TextValidator
 
 class AccountSetupNameSlide : SlideFragment() {
+  @BindView(R.id.nameWrapper)
+  lateinit var nameWrapper: TextInputLayout
   @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()
+    return nameValidator.isValid
   }
 
   override val title = R.string.slideAccountNameTitle
@@ -43,14 +40,18 @@ class AccountSetupNameSlide : SlideFragment() {
                                savedInstanceState: Bundle?): View {
     val view = inflater.inflate(R.layout.setup_account_name, container, false)
     ButterKnife.bind(this, view)
-    nameField.addTextChangedListener(textWatcher)
+    nameValidator = object : TextValidator(
+      nameWrapper::setError, resources.getString(R.string.hintInvalidName)
+    ) {
+      override fun validate(text: Editable)
+        = text.isNotBlank()
+
+      override fun onChanged() = updateValidity()
+    }
+    nameField.addTextChangedListener(nameValidator)
+    nameValidator.afterTextChanged(nameField.text)
     return view
   }
 
-  override fun onDestroyView() {
-    nameField.removeTextChangedListener(textWatcher)
-    super.onDestroyView()
-  }
-
-  private fun validName() = nameField.text.isNotEmpty()
+  private lateinit var nameValidator: TextValidator
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSetupUserSlide.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSetupUserSlide.kt
index 34559a4af..8e10124b9 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSetupUserSlide.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountSetupUserSlide.kt
@@ -2,8 +2,8 @@ package de.kuschku.quasseldroid_ng.ui.setup.accounts
 
 import android.os.Bundle
 import android.support.design.widget.TextInputEditText
+import android.support.design.widget.TextInputLayout
 import android.text.Editable
-import android.text.TextWatcher
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -11,22 +11,21 @@ import butterknife.BindView
 import butterknife.ButterKnife
 import de.kuschku.quasseldroid_ng.R
 import de.kuschku.quasseldroid_ng.ui.setup.SlideFragment
+import de.kuschku.quasseldroid_ng.util.TextValidator
 
 class AccountSetupUserSlide : SlideFragment() {
+  @BindView(R.id.userWrapper)
+  lateinit var userWrapper: TextInputLayout
   @BindView(R.id.user)
   lateinit var userField: TextInputEditText
 
+  @BindView(R.id.passWrapper)
+  lateinit var passWrapper: TextInputLayout
   @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()
+    return true
   }
 
   override val title = R.string.slideAccountUserTitle
@@ -48,17 +47,18 @@ class AccountSetupUserSlide : SlideFragment() {
                                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
-  }
+    userValidator = object : TextValidator(
+      userWrapper::setError, resources.getString(R.string.hintInvalidUser)
+    ) {
+      override fun validate(text: Editable)
+        = text.isNotBlank()
 
-  override fun onDestroyView() {
-    userField.removeTextChangedListener(textWatcher)
-    passField.removeTextChangedListener(textWatcher)
-    super.onDestroyView()
+      override fun onChanged() = updateValidity()
+    }
+    userField.addTextChangedListener(userValidator)
+    userValidator.afterTextChanged(userField.text)
+    return view
   }
 
-  private fun validUser() = userField.text.isNotEmpty()
-  private fun validPass() = passField.text.isNotEmpty()
+  private lateinit var userValidator: TextValidator
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/Patterns.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/Patterns.kt
new file mode 100644
index 000000000..c4e55304c
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/Patterns.kt
@@ -0,0 +1,64 @@
+package de.kuschku.quasseldroid_ng.util
+
+import java.util.regex.Pattern
+
+@SuppressWarnings("Access")
+object Patterns {
+  const val IPv4
+    = "(?:(?:[0-1]?[0-9]?[0-9]|2[0-5][0-5])\\.){3}(?:[0-1]?[0-9]?[0-9]|2[0-5][0-5])"
+
+  const val IPv6
+    = "(?:(?:(?:[0-9a-fA-F]{1,4}:){7}(?:[0-9a-fA-F]{1,4}))|(?:(?:(?:[0-9a-fA-F]{1,4}:){1,7}|:):(?:[0-9a-fA-F]{1,4}))|(?:(?:(?:[0-9a-fA-F]{1,4}:){1,6}|:):(?:[0-9a-fA-F]{1,4}:){0,1}(?:[0-9a-fA-F]{1,4}))|(?:(?:(?:[0-9a-fA-F]{1,4}:){1,5}|:):(?:[0-9a-fA-F]{1,4}:){0,2}(?:[0-9a-fA-F]{1,4}))|(?:(?:(?:[0-9a-fA-F]{1,4}:){1,4}|:):(?:[0-9a-fA-F]{1,4}:){0,3}(?:[0-9a-fA-F]{1,4}))|(?:(?:(?:[0-9a-fA-F]{1,4}:){1,3}|:):(?:[0-9a-fA-F]{1,4}:){1,4}(?:[0-9a-fA-F]{1,4}))|(?:(?:(?:[0-9a-fA-F]{1,4}:){1,2}|:):(?:[0-9a-fA-F]{1,4}:){0,5}(?:[0-9a-fA-F]{1,4}))|(?:(?:(?:[0-9a-fA-F]{1,4}:)|:):(?:[0-9a-fA-F]{1,4}:){0,6}(?:[0-9a-fA-F]{1,4})))"
+
+  const val IP_ADDRESS_STRING = "(?:" + IPv4 + "|" + IPv6 + ")"
+
+  const val UCS_CHAR = "[" +
+    "\u00A0-\uD7FF" +
+    "\uF900-\uFDCF" +
+    "\uFDF0-\uFFEF" +
+    "\uD800\uDC00-\uD83F\uDFFD" +
+    "\uD840\uDC00-\uD87F\uDFFD" +
+    "\uD880\uDC00-\uD8BF\uDFFD" +
+    "\uD8C0\uDC00-\uD8FF\uDFFD" +
+    "\uD900\uDC00-\uD93F\uDFFD" +
+    "\uD940\uDC00-\uD97F\uDFFD" +
+    "\uD980\uDC00-\uD9BF\uDFFD" +
+    "\uD9C0\uDC00-\uD9FF\uDFFD" +
+    "\uDA00\uDC00-\uDA3F\uDFFD" +
+    "\uDA40\uDC00-\uDA7F\uDFFD" +
+    "\uDA80\uDC00-\uDABF\uDFFD" +
+    "\uDAC0\uDC00-\uDAFF\uDFFD" +
+    "\uDB00\uDC00-\uDB3F\uDFFD" +
+    "\uDB44\uDC00-\uDB7F\uDFFD" +
+    "&&[^\u00A0[\u2000-\u200A]\u2028\u2029\u202F\u3000]" +
+    "]"
+  /**
+   * Valid characters for IRI label defined in RFC 3987.
+   */
+  const val LABEL_CHAR
+    = "a-zA-Z0-9" + UCS_CHAR
+  /**
+   * Valid characters for IRI TLD defined in RFC 3987.
+   */
+  const val TLD_CHAR
+    = "a-zA-Z" + UCS_CHAR
+  /**
+   * RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets.
+   */
+  const val IRI_LABEL
+    = "[" + LABEL_CHAR + "](?:[" + LABEL_CHAR + "_\\-]{0,61}[" + LABEL_CHAR + "]){0,1}"
+  /**
+   * RFC 3492 references RFC 1034 and limits Punycode algorithm output to 63 characters.
+   */
+  const val PUNYCODE_TLD
+    = "xn\\-\\-[\\w\\-]{0,58}\\w"
+  const val TLD
+    = "(?:" + PUNYCODE_TLD + "|" + "[" + TLD_CHAR + "]{2,63}" + ")"
+  const val HOST_NAME
+    = "(?:" + IRI_LABEL + "\\.)+" + TLD + ".?"
+  const val LOCAL_HOST_NAME
+    = "(?:" + IRI_LABEL + "\\.)*" + IRI_LABEL
+  const val DOMAIN_NAME_STR
+    = "(?:" + LOCAL_HOST_NAME + "|" + HOST_NAME + "|" + IP_ADDRESS_STRING + ")"
+  val DOMAIN_NAME = Pattern.compile(DOMAIN_NAME_STR)
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/TextValidator.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/TextValidator.kt
new file mode 100644
index 000000000..777fe5c2c
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/TextValidator.kt
@@ -0,0 +1,22 @@
+package de.kuschku.quasseldroid_ng.util
+
+import android.text.Editable
+import android.text.TextWatcher
+
+abstract class TextValidator(private val errorListener: (String?) -> Unit,
+                             private val error: String) : TextWatcher {
+  override fun afterTextChanged(p0: Editable) {
+    isValid = validate(p0)
+    errorListener(if (isValid) null else error)
+    onChanged()
+  }
+
+  protected open fun onChanged() = Unit
+
+  override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) = Unit
+  override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) = Unit
+
+  abstract fun validate(text: Editable): Boolean
+  var isValid = false
+    private set
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/AndroidCompatibilityUtils.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/compatibility/AndroidCompatibilityUtils.kt
similarity index 95%
rename from app/src/main/java/de/kuschku/quasseldroid_ng/util/AndroidCompatibilityUtils.kt
rename to app/src/main/java/de/kuschku/quasseldroid_ng/util/compatibility/AndroidCompatibilityUtils.kt
index aee755bc8..15ab06a45 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/util/AndroidCompatibilityUtils.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/compatibility/AndroidCompatibilityUtils.kt
@@ -1,4 +1,4 @@
-package de.kuschku.quasseldroid_ng.util
+package de.kuschku.quasseldroid_ng.util.compatibility
 
 import android.os.Build
 import de.kuschku.libquassel.util.compatibility.CompatibilityUtils
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/AndroidHandlerService.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/compatibility/AndroidHandlerService.kt
similarity index 96%
rename from app/src/main/java/de/kuschku/quasseldroid_ng/util/AndroidHandlerService.kt
rename to app/src/main/java/de/kuschku/quasseldroid_ng/util/compatibility/AndroidHandlerService.kt
index c7278f3aa..d1d341a09 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/util/AndroidHandlerService.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/compatibility/AndroidHandlerService.kt
@@ -1,4 +1,4 @@
-package de.kuschku.quasseldroid_ng.util
+package de.kuschku.quasseldroid_ng.util.compatibility
 
 import android.os.Handler
 import android.os.HandlerThread
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/AndroidLoggingHandler.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/compatibility/AndroidLoggingHandler.kt
similarity index 80%
rename from app/src/main/java/de/kuschku/quasseldroid_ng/util/AndroidLoggingHandler.kt
rename to app/src/main/java/de/kuschku/quasseldroid_ng/util/compatibility/AndroidLoggingHandler.kt
index bd2ebaf1d..564df9023 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/util/AndroidLoggingHandler.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/compatibility/AndroidLoggingHandler.kt
@@ -1,15 +1,18 @@
-package de.kuschku.quasseldroid_ng.util
+package de.kuschku.quasseldroid_ng.util.compatibility
 
 import android.util.Log
 import de.kuschku.libquassel.util.compatibility.LoggingHandler
 
 object AndroidLoggingHandler : LoggingHandler() {
   override fun isLoggable(logLevel: LogLevel, tag: String): Boolean {
-    return Log.isLoggable(tag, priority(logLevel))
+    return Log.isLoggable(tag,
+                          priority(
+                            logLevel))
   }
 
   override fun log(logLevel: LogLevel, tag: String, message: String?, throwable: Throwable?) {
-    val priority = priority(logLevel)
+    val priority = priority(
+      logLevel)
     if (message != null)
       Log.println(priority, tag, message)
     if (throwable != null)
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/AndroidStreamChannelFactory.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/compatibility/AndroidStreamChannelFactory.kt
similarity index 92%
rename from app/src/main/java/de/kuschku/quasseldroid_ng/util/AndroidStreamChannelFactory.kt
rename to app/src/main/java/de/kuschku/quasseldroid_ng/util/compatibility/AndroidStreamChannelFactory.kt
index 40ca74403..7b4a2a987 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/util/AndroidStreamChannelFactory.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/compatibility/AndroidStreamChannelFactory.kt
@@ -1,4 +1,4 @@
-package de.kuschku.quasseldroid_ng.util
+package de.kuschku.quasseldroid_ng.util.compatibility
 
 import de.kuschku.libquassel.util.compatibility.StreamChannelFactory
 import de.kuschku.quasseldroid_ng.util.backport.ReadableStreamChannel
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index ae8a6ba04..2f767fd29 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -22,7 +22,7 @@
         android:layout_height="wrap_content"
         android:minWidth="88dp"
         android:text="Connect"
-        android:theme="@style/RaisedThemedButton" />
+        android:theme="@style/AppTheme.Button.Colored" />
 
       <Button
         android:id="@+id/disconnect"
@@ -30,7 +30,7 @@
         android:layout_height="wrap_content"
         android:minWidth="88dp"
         android:text="Disconnect"
-        android:theme="@style/RaisedButton" />
+        android:theme="@style/AppTheme.Button" />
 
       <Button
         android:id="@+id/clear"
@@ -38,7 +38,7 @@
         android:layout_height="wrap_content"
         android:minWidth="88dp"
         android:text="Clear"
-        android:theme="@style/RaisedButton" />
+        android:theme="@style/AppTheme.Button" />
     </LinearLayout>
 
     <TextView
diff --git a/app/src/main/res/layout/activity_setup.xml b/app/src/main/res/layout/activity_setup.xml
index 8a58d8776..4ded7a441 100644
--- a/app/src/main/res/layout/activity_setup.xml
+++ b/app/src/main/res/layout/activity_setup.xml
@@ -16,9 +16,6 @@
     android:layout_marginBottom="16dp"
     android:layout_marginRight="16dp"
     android:tint="#ffffff"
-    app:backgroundTint="#8A808080"
-    app:elevation="0dip"
-    app:fabSize="normal"
-    app:pressedTranslationZ="0dip" />
+    app:fabSize="normal" />
 
 </merge>
diff --git a/app/src/main/res/layout/setup_account_connection.xml b/app/src/main/res/layout/setup_account_connection.xml
index 83ae78673..86e7e13b4 100644
--- a/app/src/main/res/layout/setup_account_connection.xml
+++ b/app/src/main/res/layout/setup_account_connection.xml
@@ -27,9 +27,10 @@
   android:padding="32dp">
 
   <android.support.design.widget.TextInputLayout
+    android:id="@+id/hostWrapper"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:hint="@string/labelConnectionHostname"
+    android:hint="@string/labelConnectionHost"
     app:errorEnabled="true">
 
     <android.support.design.widget.TextInputEditText
@@ -40,6 +41,7 @@
   </android.support.design.widget.TextInputLayout>
 
   <android.support.design.widget.TextInputLayout
+    android:id="@+id/portWrapper"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:hint="@string/labelConnectionPort"
diff --git a/app/src/main/res/layout/setup_account_edit.xml b/app/src/main/res/layout/setup_account_edit.xml
index 93e4873d7..c00a9e81d 100644
--- a/app/src/main/res/layout/setup_account_edit.xml
+++ b/app/src/main/res/layout/setup_account_edit.xml
@@ -24,7 +24,7 @@
 
         <FrameLayout
           android:layout_width="56dp"
-          android:layout_height="match_parent">
+          android:layout_height="68dp">
 
           <ImageView
             android:layout_width="24dp"
@@ -35,6 +35,7 @@
         </FrameLayout>
 
         <android.support.design.widget.TextInputLayout
+          android:id="@+id/nameWrapper"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:hint="@string/labelAccountName"
@@ -60,7 +61,7 @@
 
         <FrameLayout
           android:layout_width="56dp"
-          android:layout_height="match_parent">
+          android:layout_height="68dp">
 
           <ImageView
             android:layout_width="24dp"
@@ -70,45 +71,44 @@
             app:srcCompat="@drawable/ic_server_network" />
         </FrameLayout>
 
-        <android.support.design.widget.TextInputLayout
+        <LinearLayout
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
-          android:hint="@string/labelConnectionHostname"
-          app:errorEnabled="true">
+          android:orientation="vertical">
 
-          <android.support.design.widget.TextInputEditText
-            android:id="@+id/host"
+          <android.support.design.widget.TextInputLayout
+            android:id="@+id/hostWrapper"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:inputType="textUri" />
-        </android.support.design.widget.TextInputLayout>
-      </LinearLayout>
-
-
-      <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:paddingLeft="16dp"
-        android:paddingRight="16dp">
-
-        <android.support.v4.widget.Space
-          android:layout_width="56dp"
-          android:layout_height="match_parent" />
-
-        <android.support.design.widget.TextInputLayout
-          android:layout_width="match_parent"
-          android:layout_height="wrap_content"
-          android:hint="@string/labelConnectionPort"
-          app:errorEnabled="true">
-
-          <android.support.design.widget.TextInputEditText
-            android:id="@+id/port"
+            android:hint="@string/labelConnectionHost"
+            app:errorEnabled="true">
+
+            <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.v4.widget.Space
+            android:layout_width="56dp"
+            android:layout_height="match_parent" />
+
+          <android.support.design.widget.TextInputLayout
+            android:id="@+id/portWrapper"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:inputType="number"
-            android:numeric="integer"
-            android:text="@string/defaultConnectionPort" />
-        </android.support.design.widget.TextInputLayout>
+            android:hint="@string/labelConnectionPort"
+            app:errorEnabled="true">
+
+            <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>
       </LinearLayout>
 
       <android.support.v4.widget.Space
@@ -124,7 +124,7 @@
 
         <FrameLayout
           android:layout_width="56dp"
-          android:layout_height="match_parent">
+          android:layout_height="68dp">
 
           <ImageView
             android:layout_width="24dp"
@@ -134,44 +134,40 @@
             app:srcCompat="@drawable/ic_account" />
         </FrameLayout>
 
-        <android.support.design.widget.TextInputLayout
+        <LinearLayout
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
-          android:hint="@string/labelAccountUsername">
+          android:orientation="vertical">
 
-          <android.support.design.widget.TextInputEditText
-            android:id="@+id/user"
+          <android.support.design.widget.TextInputLayout
+            android:id="@+id/userWrapper"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:inputType="textVisiblePassword|textNoSuggestions"
-            app:errorEnabled="true" />
-        </android.support.design.widget.TextInputLayout>
-      </LinearLayout>
-
-
-      <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:paddingLeft="16dp"
-        android:paddingRight="16dp">
-
-        <android.support.v4.widget.Space
-          android:layout_width="56dp"
-          android:layout_height="match_parent" />
-
-        <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:hint="@string/labelAccountUser">
+
+            <android.support.design.widget.TextInputEditText
+              android:id="@+id/user"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:inputType="textVisiblePassword|textNoSuggestions"
+              app:errorEnabled="true" />
+          </android.support.design.widget.TextInputLayout>
+
+          <android.support.design.widget.TextInputLayout
+            android:id="@+id/passWrapper"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:inputType="textPassword"
-            android:password="true"
-            app:errorEnabled="true" />
-        </android.support.design.widget.TextInputLayout>
+            android:hint="@string/labelAccountPass">
+
+            <android.support.design.widget.TextInputEditText
+              android:id="@+id/pass"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:inputType="textPassword"
+              app:errorEnabled="true" />
+          </android.support.design.widget.TextInputLayout>
+
+        </LinearLayout>
       </LinearLayout>
 
       <android.support.v4.widget.Space
@@ -179,16 +175,4 @@
         android:layout_height="16dp" />
     </LinearLayout>
   </android.support.v4.widget.NestedScrollView>
-
-  <android.support.design.widget.FloatingActionButton
-    android:id="@+id/save_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:fabSize="normal"
-    app:srcCompat="@drawable/ic_save" />
 </FrameLayout>
diff --git a/app/src/main/res/layout/setup_account_name.xml b/app/src/main/res/layout/setup_account_name.xml
index 46c48f9a2..591099361 100644
--- a/app/src/main/res/layout/setup_account_name.xml
+++ b/app/src/main/res/layout/setup_account_name.xml
@@ -6,6 +6,7 @@
   android:padding="32dp">
 
   <android.support.design.widget.TextInputLayout
+    android:id="@+id/nameWrapper"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:hint="@string/labelAccountName"
diff --git a/app/src/main/res/layout/setup_account_user.xml b/app/src/main/res/layout/setup_account_user.xml
index 6bf604941..4468f2c97 100644
--- a/app/src/main/res/layout/setup_account_user.xml
+++ b/app/src/main/res/layout/setup_account_user.xml
@@ -6,9 +6,10 @@
   android:padding="32dp">
 
   <android.support.design.widget.TextInputLayout
+    android:id="@+id/userWrapper"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:hint="@string/labelAccountUsername">
+    android:hint="@string/labelAccountUser">
 
     <android.support.design.widget.TextInputEditText
       android:id="@+id/user"
@@ -19,9 +20,10 @@
   </android.support.design.widget.TextInputLayout>
 
   <android.support.design.widget.TextInputLayout
+    android:id="@+id/passWrapper"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:hint="@string/labelAccountPassword">
+    android:hint="@string/labelAccountPass">
 
     <android.support.design.widget.TextInputEditText
       android:id="@+id/pass"
diff --git a/app/src/main/res/layout/setup_slide.xml b/app/src/main/res/layout/setup_slide.xml
index 679e33f2c..c31e16eb4 100644
--- a/app/src/main/res/layout/setup_slide.xml
+++ b/app/src/main/res/layout/setup_slide.xml
@@ -58,9 +58,14 @@
     </android.support.design.widget.CollapsingToolbarLayout>
   </android.support.design.widget.AppBarLayout>
 
-  <FrameLayout
-    android:id="@+id/content_host"
+  <android.support.v4.widget.NestedScrollView
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+    app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+    <FrameLayout
+      android:id="@+id/content_host"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content" />
+  </android.support.v4.widget.NestedScrollView>
 </android.support.design.widget.CoordinatorLayout>
diff --git a/app/src/main/res/menu/setup_edit_account.xml b/app/src/main/res/menu/setup_edit_account.xml
index fa05be637..6c17b9047 100644
--- a/app/src/main/res/menu/setup_edit_account.xml
+++ b/app/src/main/res/menu/setup_edit_account.xml
@@ -1,10 +1,14 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto">
+  <item
+    android:id="@+id/save"
+    android:title="Save"
+    app:showAsAction="ifRoom" />
   <item
     android:id="@+id/delete"
     android:icon="@drawable/ic_delete"
     android:title="Delete"
     app:iconTint="#fff"
-    app:showAsAction="ifRoom" />
+    app:showAsAction="never" />
 </menu>
diff --git a/app/src/main/res/values/strings_setup.xml b/app/src/main/res/values/strings_setup.xml
index 87287e669..2a5608326 100644
--- a/app/src/main/res/values/strings_setup.xml
+++ b/app/src/main/res/values/strings_setup.xml
@@ -8,15 +8,20 @@
   <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="labelConnectionHost">Host</string>
   <string name="labelConnectionPort">Port</string>
 
+  <string name="hintInvalidHost">Not a valid hostname</string>
+  <string name="hintInvalidPort">Not a valid 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>
+  <string name="labelAccountUser">Username</string>
+  <string name="labelAccountPass">Password</string>
+
+  <string name="hintInvalidUser">Username can not be empty</string>
 
   <!-- Account Name -->
   <string name="slideAccountNameTitle">Customize Account</string>
@@ -24,6 +29,8 @@
 
   <string name="labelAccountName">Account name</string>
 
+  <string name="hintInvalidName">Name can not be empty</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>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 965fd7437..97b0c5a58 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -14,11 +14,12 @@
 
   <style name="SetupTheme" parent="AppTheme.NoActionBar" />
 
-  <style name="RaisedButton" parent="AppTheme">
-    <item name="colorButtonNormal">?attr/background</item>
+  <style name="AppTheme.Button" parent="AppTheme">
+    <item name="colorButtonNormal">@android:color/background_light</item>
   </style>
 
-  <style name="RaisedThemedButton" parent="RaisedButton">
+  <style name="AppTheme.Button.Colored" parent="AppTheme.Button">
+    <item name="android:textColor">?android:attr/textColorPrimaryInverse</item>
     <item name="colorButtonNormal">@color/colorAccent</item>
   </style>
 
-- 
GitLab