From b20b3d6f5c6c965876de4409fd79fec5f9d5543e Mon Sep 17 00:00:00 2001
From: NerdNumber9 <nerd.number9@yandex.com>
Date: Thu, 24 Aug 2017 17:11:43 -0400
Subject: [PATCH] Add fingerprint to lock UI Migrate login UI to conductor Fix
 batch add controller not saving EditText content onResume Prevent
 double-locking of lock UI Remove back button from lock UI Fix login
 preference not updating

---
 app/build.gradle                              |   9 +-
 app/src/main/AndroidManifest.xml              |   3 +-
 app/src/main/java/eu/kanade/tachiyomi/App.kt  |   2 +
 .../data/preference/PreferencesHelper.kt      |   2 +
 .../tachiyomi/source/online/all/EHentai.kt    |   8 +-
 .../kanade/tachiyomi/ui/main/MainActivity.kt  |  25 ++-
 .../ui/setting/SettingsEhController.kt        |  16 +-
 .../ui/setting/SettingsGeneralController.kt   |  23 ++-
 .../exh/ui/batchadd/BatchAddController.kt     |   9 +-
 .../java/exh/ui/batchadd/BatchAddPresenter.kt |  14 +-
 .../java/exh/ui/lock/FingerLockPreference.kt  | 149 ++++++++++++++++++
 .../java/exh/ui/lock/LockChangeHandler.kt     |   4 +-
 .../main/java/exh/ui/lock/LockController.kt   |  74 ++++++++-
 .../main/java/exh/ui/lock/LockPreference.kt   |  24 ++-
 .../main/java/exh/ui/lock/LockPresenter.kt    |  15 +-
 .../{LoginActivity.kt => LoginController.kt}  | 109 +++++++------
 .../main/java/exh/ui/login/LoginPresenter.kt  |   7 +
 app/src/main/java/exh/util/ViewUtil.kt        |   8 +
 app/src/main/res/layout/activity_lock.xml     |  16 ++
 .../main/res/layout/eh_fragment_batch_add.xml |  26 +--
 app/src/main/res/values/themes.xml            |   4 +
 21 files changed, 438 insertions(+), 109 deletions(-)
 create mode 100644 app/src/main/java/exh/ui/lock/FingerLockPreference.kt
 rename app/src/main/java/exh/ui/login/{LoginActivity.kt => LoginController.kt} (66%)
 create mode 100644 app/src/main/java/exh/ui/login/LoginPresenter.kt
 create mode 100644 app/src/main/java/exh/util/ViewUtil.kt

diff --git a/app/build.gradle b/app/build.gradle
index 058b0e089..5dd61f9d4 100755
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -219,9 +219,16 @@ dependencies {
     //JVE (Regex) (EH)
     compile 'ru.lanwen.verbalregex:java-verbal-expressions:1.4'
 
-    //Pin lock view
+    //Pin lock view (EXH)
     compile 'com.andrognito.pinlockview:pinlockview:1.0.1'
 
+    //Reprint (EXH)
+    compile 'com.github.ajalt.reprint:core:3.1.0@aar' // required: supports marshmallow devices
+    compile 'com.github.ajalt.reprint:rxjava:3.1.0@aar' // optional: the RxJava 1 interface
+
+    //Swirl (EXH)
+    compile 'com.mattprecious.swirl:swirl:1.0.0'
+
     // Tests
     //Paper DB screws up tests
     /*testCompile 'junit:junit:4.12'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5762deea3..2447e6800 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -10,6 +10,7 @@
     <uses-permission android:name="android.permission.WAKE_LOCK" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
+    <uses-permission android:name="android.permission.USE_FINGERPRINT" />
     <uses-permission android:name="android.permission.GET_TASKS"/>
     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
         tools:ignore="ProtectedPermissions" />
@@ -106,7 +107,7 @@
 
         <!-- EH -->
         <activity
-            android:name="exh.ui.login.LoginActivity"
+            android:name="exh.ui.login.LoginController"
             android:label="@string/label_login">
             <!-- TODO parent activity -->
         </activity>
diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt
index af89227b6..4997f2587 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/App.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt
@@ -5,6 +5,7 @@ import android.content.Context
 import android.content.res.Configuration
 import android.support.multidex.MultiDex
 import com.evernote.android.job.JobManager
+import com.github.ajalt.reprint.core.Reprint
 import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
 import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
 import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
@@ -26,6 +27,7 @@ open class App : Application() {
 
         setupJobManager()
         Paper.init(this) //Setup metadata DB (EH)
+        Reprint.initialize(this) //Setup fingerprint (EH)
 
         LocaleHelper.updateConfiguration(this, resources.configuration)
     }
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
index 46cedc1bf..586a4c0c4 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
@@ -199,5 +199,7 @@ class PreferencesHelper(val context: Context) {
     fun lockSalt() = rxPrefs.getString("lock_salt", null)
 
     fun lockLength() = rxPrefs.getInteger("lock_length", -1)
+
+    fun lockUseFingerprint() = rxPrefs.getBoolean("lock_finger", false)
     // <-- EH
 }
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt
index 0217a417c..53077e13b 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt
@@ -19,7 +19,7 @@ import rx.Observable
 import uy.kohesive.injekt.injectLazy
 import java.net.URLEncoder
 import java.util.*
-import exh.ui.login.LoginActivity
+import exh.ui.login.LoginController
 import exh.util.UriFilter
 import exh.util.UriGroup
 import okhttp3.CacheControl
@@ -323,9 +323,9 @@ class EHentai(override val id: Long,
     val cookiesHeader by lazy {
         val cookies: MutableMap<String, String> = mutableMapOf()
         if(prefs.enableExhentai().getOrDefault()) {
-            cookies.put(LoginActivity.MEMBER_ID_COOKIE, prefs.memberIdVal().get()!!)
-            cookies.put(LoginActivity.PASS_HASH_COOKIE, prefs.passHashVal().get()!!)
-            cookies.put(LoginActivity.IGNEOUS_COOKIE, prefs.igneousVal().get()!!)
+            cookies.put(LoginController.MEMBER_ID_COOKIE, prefs.memberIdVal().get()!!)
+            cookies.put(LoginController.PASS_HASH_COOKIE, prefs.passHashVal().get()!!)
+            cookies.put(LoginController.IGNEOUS_COOKIE, prefs.igneousVal().get()!!)
         }
 
         //Setup settings
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
index 2adeadc5f..642ab27bf 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.main
 import android.animation.ObjectAnimator
 import android.app.ActivityManager
 import android.app.Service
-import android.app.usage.UsageStats
 import android.app.usage.UsageStatsManager
 import android.content.Intent
 import android.graphics.Color
@@ -15,8 +14,6 @@ import android.support.v7.graphics.drawable.DrawerArrowDrawable
 import android.view.ViewGroup
 import com.bluelinelabs.conductor.*
 import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
-import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler
-import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler
 import eu.kanade.tachiyomi.Migrations
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -40,7 +37,6 @@ import exh.ui.lock.lockEnabled
 import exh.ui.lock.notifyLockSecurity
 import kotlinx.android.synthetic.main.main_activity.*
 import uy.kohesive.injekt.injectLazy
-import java.util.*
 
 
 class MainActivity : BaseActivity() {
@@ -49,7 +45,7 @@ class MainActivity : BaseActivity() {
 
     val preferences: PreferencesHelper by injectLazy()
 
-    private var drawerArrow: DrawerArrowDrawable? = null
+    var drawerArrow: DrawerArrowDrawable? = null
 
     private var secondaryDrawer: ViewGroup? = null
 
@@ -153,6 +149,8 @@ class MainActivity : BaseActivity() {
         if (savedInstanceState == null) {
             val lockEnabled = lockEnabled(preferences)
             if (lockEnabled) {
+                //Special case first lock
+                toolbar.navigationIcon = null
                 doLock()
 
                 //Check lock security
@@ -236,6 +234,16 @@ class MainActivity : BaseActivity() {
             drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
         }
 
+        // --> EH
+        //Special case and hide drawer arrow for lock controller
+        if(to is LockController) {
+            supportActionBar?.setDisplayHomeAsUpEnabled(false)
+        } else {
+            supportActionBar?.setDisplayHomeAsUpEnabled(true)
+            toolbar.navigationIcon = drawerArrow
+        }
+        // <-- EH
+
         ObjectAnimator.ofFloat(drawerArrow, "progress", if (showHamburger) 0f else 1f).start()
 
         if (from is TabbedController) {
@@ -270,10 +278,9 @@ class MainActivity : BaseActivity() {
     // --> EH
     //Lock code
     var willLock = false
-    var disableLock = false
     override fun onRestart() {
         super.onRestart()
-        if(willLock && lockEnabled() && !disableLock) {
+        if(willLock && lockEnabled()) {
             doLock()
         }
 
@@ -286,6 +293,10 @@ class MainActivity : BaseActivity() {
     }
 
     fun tryLock() {
+        //Do not double-lock
+        if(router.backstack.lastOrNull()?.controller() is LockController)
+            return
+
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
             val mUsageStatsManager = getSystemService("usagestats") as UsageStatsManager
             val time = System.currentTimeMillis()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhController.kt
index 650138ce5..74b2a1dee 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhController.kt
@@ -1,9 +1,11 @@
 package eu.kanade.tachiyomi.ui.setting
 
-import android.content.Intent
 import android.support.v7.preference.PreferenceScreen
+import com.bluelinelabs.conductor.RouterTransaction
+import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
 import exh.ui.migration.MetadataFetchDialog
-import exh.ui.login.LoginActivity
+import exh.ui.login.LoginController
+import rx.android.schedulers.AndroidSchedulers
 
 /**
  * EH Settings fragment
@@ -20,9 +22,11 @@ class SettingsEhController : SettingsController() {
             isPersistent = false
             defaultValue = false
             preferences.enableExhentai()
-                    .asObservable().subscribeUntilDestroy {
+                    .asObservable()
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .subscribeUntilDestroy {
                 isChecked = it
-            }
+           }
 
             onChange { newVal ->
                 newVal as Boolean
@@ -30,7 +34,9 @@ class SettingsEhController : SettingsController() {
                     preferences.enableExhentai().set(false)
                     true
                 } else {
-                    startActivity(Intent(context, LoginActivity::class.java))
+                    router.pushController(RouterTransaction.with(LoginController())
+                            .pushChangeHandler(FadeChangeHandler())
+                            .popChangeHandler(FadeChangeHandler()))
                     false
                 }
             }
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt
index ee2a241f9..1ac99c63c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt
@@ -4,6 +4,7 @@ import android.app.Dialog
 import android.os.Bundle
 import android.os.Handler
 import android.support.v7.preference.PreferenceScreen
+import android.support.v7.preference.SwitchPreference
 import android.view.View
 import com.afollestad.materialdialogs.MaterialDialog
 import eu.kanade.tachiyomi.R
@@ -13,6 +14,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.data.preference.getOrDefault
 import eu.kanade.tachiyomi.ui.base.controller.DialogController
 import eu.kanade.tachiyomi.util.LocaleHelper
+import exh.ui.lock.FingerLockPreference
 import exh.ui.lock.LockPreference
 import kotlinx.android.synthetic.main.pref_library_columns.view.*
 import rx.Observable
@@ -176,12 +178,25 @@ class SettingsGeneralController : SettingsController() {
                 true
             }
         }
-        LockPreference(context).apply {
-            key = "pref_app_lock"
+        preferenceCategory {
             title = "Application lock"
-            isPersistent = false
 
-            addPreference(this)
+            LockPreference(context).apply {
+                key = "pref_app_lock"
+                isPersistent = false
+
+                addPreference(this)
+            }
+
+            FingerLockPreference(context).apply {
+                key = "pref_lock_finger"
+                isPersistent = false
+
+                addPreference(this)
+
+                //Call after addPreference
+                dependency = "pref_app_lock"
+            }
         }
     }
 
diff --git a/app/src/main/java/exh/ui/batchadd/BatchAddController.kt b/app/src/main/java/exh/ui/batchadd/BatchAddController.kt
index 401cd9120..bad59e8e2 100755
--- a/app/src/main/java/exh/ui/batchadd/BatchAddController.kt
+++ b/app/src/main/java/exh/ui/batchadd/BatchAddController.kt
@@ -35,7 +35,7 @@ class BatchAddController : NucleusController<BatchAddPresenter>() {
             }
 
             progress_dismiss_btn.clicks().subscribeUntilDestroy {
-                presenter.currentlyAddingRelay.call(false)
+                presenter.currentlyAddingRelay.call(BatchAddPresenter.STATE_PROGRESS_TO_INPUT)
             }
 
             val progressSubscriptions = CompositeSubscription()
@@ -44,7 +44,7 @@ class BatchAddController : NucleusController<BatchAddPresenter>() {
                     .observeOn(AndroidSchedulers.mainThread())
                     .subscribeUntilDestroy {
                         progressSubscriptions.clear()
-                        if(it) {
+                        if(it == BatchAddPresenter.STATE_INPUT_TO_PROGRESS) {
                             showProgress(this)
                             progressSubscriptions += presenter.progressRelay
                                     .observeOn(AndroidSchedulers.mainThread())
@@ -79,7 +79,10 @@ class BatchAddController : NucleusController<BatchAddPresenter>() {
                                     }?.let {
                                 progressSubscriptions += it
                             }
-                        } else hideProgress(this)
+                        } else if(it == BatchAddPresenter.STATE_PROGRESS_TO_INPUT) {
+                            hideProgress(this)
+                            presenter.currentlyAddingRelay.call(BatchAddPresenter.STATE_IDLE)
+                        }
                     }
         }
     }
diff --git a/app/src/main/java/exh/ui/batchadd/BatchAddPresenter.kt b/app/src/main/java/exh/ui/batchadd/BatchAddPresenter.kt
index 40dffcb15..5e0a05076 100644
--- a/app/src/main/java/exh/ui/batchadd/BatchAddPresenter.kt
+++ b/app/src/main/java/exh/ui/batchadd/BatchAddPresenter.kt
@@ -15,18 +15,18 @@ class BatchAddPresenter: BasePresenter<BatchAddController>() {
     val progressTotalRelay = BehaviorRelay.create(0)!!
     val progressRelay = BehaviorRelay.create(0)!!
     var eventRelay: ReplayRelay<String>? = null
-    val currentlyAddingRelay = BehaviorRelay.create(false)!!
+    val currentlyAddingRelay = BehaviorRelay.create(STATE_IDLE)!!
 
     fun addGalleries(galleries: String) {
         eventRelay = ReplayRelay.create()
-        val splitGalleries = galleries.split("\n").map {
+        val splitGalleries = galleries.split("\n").mapNotNull {
             it.trim().nullIfBlank()
-        }.filterNotNull()
+        }
 
         progressRelay.call(0)
         progressTotalRelay.call(splitGalleries.size)
 
-        currentlyAddingRelay.call(true)
+        currentlyAddingRelay.call(STATE_INPUT_TO_PROGRESS)
 
         thread {
             val succeeded = mutableListOf<String>()
@@ -48,4 +48,10 @@ class BatchAddPresenter: BasePresenter<BatchAddController>() {
             eventRelay?.call(summary)
         }
     }
+
+    companion object {
+        const val STATE_IDLE = 0
+        const val STATE_INPUT_TO_PROGRESS = 1
+        const val STATE_PROGRESS_TO_INPUT = 2
+    }
 }
diff --git a/app/src/main/java/exh/ui/lock/FingerLockPreference.kt b/app/src/main/java/exh/ui/lock/FingerLockPreference.kt
new file mode 100644
index 000000000..6a29e2a7f
--- /dev/null
+++ b/app/src/main/java/exh/ui/lock/FingerLockPreference.kt
@@ -0,0 +1,149 @@
+package exh.ui.lock
+
+import android.annotation.SuppressLint
+import android.annotation.TargetApi
+import android.content.Context
+import android.os.Build
+import android.support.v7.preference.SwitchPreferenceCompat
+import android.support.v7.widget.LinearLayoutCompat
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.ViewGroup
+import android.widget.TextView
+import com.afollestad.materialdialogs.MaterialDialog
+import com.github.ajalt.reprint.core.AuthenticationResult
+import com.github.ajalt.reprint.core.Reprint
+import com.github.ajalt.reprint.rxjava.RxReprint
+import com.mattprecious.swirl.SwirlView
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.ui.setting.onChange
+import exh.util.dpToPx
+import rx.android.schedulers.AndroidSchedulers
+import uy.kohesive.injekt.injectLazy
+
+class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+        SwitchPreferenceCompat(context, attrs) {
+
+    val prefs: PreferencesHelper by injectLazy()
+
+    val fingerprintSupported
+        get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+                && Reprint.isHardwarePresent()
+                && Reprint.hasFingerprintRegistered()
+
+    val useFingerprint
+        get() = fingerprintSupported
+                && prefs.lockUseFingerprint().getOrDefault()
+
+    @SuppressLint("NewApi")
+    override fun onAttached() {
+        super.onAttached()
+        if(fingerprintSupported) {
+            updateSummary()
+            onChange {
+                if(it as Boolean)
+                    tryChange()
+                else
+                    prefs.lockUseFingerprint().set(false)
+                !it
+            }
+        } else {
+            title = "Fingerprint unsupported"
+            shouldDisableView = true
+            summary = if(!Reprint.hasFingerprintRegistered())
+                "No fingerprints enrolled!"
+            else
+                "Fingerprint unlock is unsupported on this device!"
+            onChange { false }
+        }
+    }
+
+    private fun updateSummary() {
+        isChecked = useFingerprint
+        title = if(isChecked)
+            "Fingerprint enabled"
+        else
+            "Fingerprint disabled"
+    }
+
+    @TargetApi(Build.VERSION_CODES.M)
+    fun tryChange() {
+        val statusTextView = TextView(context).apply {
+            text = "Please touch the fingerprint sensor"
+            val size = ViewGroup.LayoutParams.WRAP_CONTENT
+            layoutParams = (layoutParams ?: ViewGroup.LayoutParams(
+                    size, size
+            )).apply {
+                width = size
+                height = size
+                setPadding(0, 0, dpToPx(context, 8), 0)
+            }
+        }
+        val iconView = SwirlView(context).apply {
+            val size = dpToPx(context, 30)
+            layoutParams = (layoutParams ?: ViewGroup.LayoutParams(
+                    size, size
+            )).apply {
+                width = size
+                height = size
+            }
+            setState(SwirlView.State.OFF, false)
+        }
+        val linearLayout = LinearLayoutCompat(context).apply {
+            orientation = LinearLayoutCompat.HORIZONTAL
+            gravity = Gravity.CENTER_VERTICAL
+            val size = LinearLayoutCompat.LayoutParams.WRAP_CONTENT
+            layoutParams = (layoutParams ?: LinearLayoutCompat.LayoutParams(
+                    size, size
+            )).apply {
+                width = size
+                height = size
+                val pSize = dpToPx(context, 24)
+                setPadding(pSize, 0, pSize, 0)
+            }
+
+            addView(statusTextView)
+            addView(iconView)
+        }
+        val dialog = MaterialDialog.Builder(context)
+                .title("Fingerprint verification")
+                .customView(linearLayout, false)
+                .negativeText("Cancel")
+                .autoDismiss(true)
+                .cancelable(true)
+                .canceledOnTouchOutside(true)
+                .show()
+        iconView.setState(SwirlView.State.ON)
+        val subscription = RxReprint.authenticate()
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe { result ->
+                    when (result.status) {
+                        AuthenticationResult.Status.SUCCESS -> {
+                            iconView.setState(SwirlView.State.ON)
+                            prefs.lockUseFingerprint().set(true)
+                            dialog.dismiss()
+                            updateSummary()
+                        }
+                        AuthenticationResult.Status.NONFATAL_FAILURE -> {
+                            iconView.setState(SwirlView.State.ERROR)
+                            statusTextView.text = result.errorMessage
+                        }
+                        AuthenticationResult.Status.FATAL_FAILURE, null -> {
+                            MaterialDialog.Builder(context)
+                                    .title("Fingerprint verification failed!")
+                                    .content(result.errorMessage)
+                                    .positiveText("Ok")
+                                    .autoDismiss(true)
+                                    .cancelable(true)
+                                    .canceledOnTouchOutside(false)
+                                    .show()
+                            dialog.dismiss()
+                        }
+                    }
+                }
+        dialog.setOnDismissListener {
+            subscription.unsubscribe()
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/exh/ui/lock/LockChangeHandler.kt b/app/src/main/java/exh/ui/lock/LockChangeHandler.kt
index e70227396..d72f7db42 100644
--- a/app/src/main/java/exh/ui/lock/LockChangeHandler.kt
+++ b/app/src/main/java/exh/ui/lock/LockChangeHandler.kt
@@ -23,8 +23,8 @@ class LockChangeHandler : AnimatorChangeHandler {
         val viewAnimators = ArrayList<Animator>()
 
         if (!isPush && from != null) {
-            viewAnimators.add(ObjectAnimator.ofFloat(from, View.SCALE_X, 5f))
-            viewAnimators.add(ObjectAnimator.ofFloat(from, View.SCALE_Y, 5f))
+            viewAnimators.add(ObjectAnimator.ofFloat(from, View.SCALE_X, 3f))
+            viewAnimators.add(ObjectAnimator.ofFloat(from, View.SCALE_Y, 3f))
             viewAnimators.add(ObjectAnimator.ofFloat(from, View.ALPHA, 0f))
         }
 
diff --git a/app/src/main/java/exh/ui/lock/LockController.kt b/app/src/main/java/exh/ui/lock/LockController.kt
index 59e5ff6b3..4ab9621dd 100755
--- a/app/src/main/java/exh/ui/lock/LockController.kt
+++ b/app/src/main/java/exh/ui/lock/LockController.kt
@@ -1,19 +1,35 @@
 package exh.ui.lock
 
+import android.annotation.SuppressLint
 import android.os.Bundle
+import android.support.v4.hardware.fingerprint.FingerprintManagerCompat
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.widget.CardView
+import android.util.TypedValue
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import android.widget.FrameLayout
 import com.afollestad.materialdialogs.MaterialDialog
 import com.andrognito.pinlockview.PinLockListener
+import com.github.ajalt.reprint.core.AuthenticationResult
+import com.github.ajalt.reprint.core.Reprint
+import com.github.ajalt.reprint.rxjava.RxReprint
+import com.mattprecious.swirl.SwirlView
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.data.preference.getOrDefault
 import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.main.MainActivity
+import exh.util.dpToPx
 import kotlinx.android.synthetic.main.activity_lock.view.*
+import kotlinx.android.synthetic.main.main_activity.view.*
 import uy.kohesive.injekt.injectLazy
 
 class LockController : NucleusController<LockPresenter>() {
+
+    val prefs: PreferencesHelper by injectLazy()
+
     override fun inflateView(inflater: LayoutInflater, container: ViewGroup)
         = inflater.inflate(R.layout.activity_lock, container, false)!!
 
@@ -21,8 +37,6 @@ class LockController : NucleusController<LockPresenter>() {
 
     override fun getTitle() = "Application locked"
 
-    val prefs: PreferencesHelper by injectLazy()
-
     override fun onViewCreated(view: View, savedViewState: Bundle?) {
         super.onViewCreated(view, savedViewState)
 
@@ -32,6 +46,7 @@ class LockController : NucleusController<LockPresenter>() {
         }
 
         with(view) {
+            //Setup pin lock
             pin_lock_view.attachIndicatorDots(indicator_dots)
 
             pin_lock_view.pinLength = prefs.lockLength().getOrDefault()
@@ -60,6 +75,61 @@ class LockController : NucleusController<LockPresenter>() {
         }
     }
 
+    @SuppressLint("NewApi")
+    override fun onAttach(view: View) {
+        super.onAttach(view)
+
+        with(view) {
+            //Fingerprint
+            if (presenter.useFingerprint) {
+                swirl_container.removeAllViews()
+                val icon = SwirlView(context).apply {
+                    val size = dpToPx(context, 60)
+                    layoutParams = (layoutParams ?: ViewGroup.LayoutParams(
+                            size, size
+                    )).apply {
+                        width = size
+                        height = size
+
+                        val pSize = dpToPx(context, 8)
+                        setPadding(pSize, pSize, pSize, pSize)
+                    }
+                    val typedVal = TypedValue()
+                    activity!!.theme!!.resolveAttribute(android.R.attr.windowBackground, typedVal, true)
+                    setBackgroundColor(typedVal.data)
+                    //Disable elevation if dark theme is active
+                    if (typedVal.data == resources.getColor(R.color.backgroundDark, activity!!.theme!!))
+                        this@with.swirl_container.cardElevation = 0f
+                    setState(SwirlView.State.OFF, false)
+                }
+                swirl_container.addView(icon)
+                icon.setState(SwirlView.State.ON)
+                RxReprint.authenticate()
+                        .subscribeUntilDetach {
+                            when (it.status) {
+                                AuthenticationResult.Status.SUCCESS -> closeLock()
+                                AuthenticationResult.Status.NONFATAL_FAILURE -> icon.setState(SwirlView.State.ERROR)
+                                AuthenticationResult.Status.FATAL_FAILURE, null -> {
+                                    MaterialDialog.Builder(context)
+                                            .title("Fingerprint error!")
+                                            .content(it.errorMessage)
+                                            .cancelable(false)
+                                            .canceledOnTouchOutside(false)
+                                            .positiveText("Ok")
+                                            .autoDismiss(true)
+                                            .show()
+                                    icon.setState(SwirlView.State.OFF)
+                                }
+                            }
+                        }
+            }
+        }
+    }
+
+    override fun onDetach(view: View) {
+        super.onDetach(view)
+    }
+
     fun closeLock() {
         router.popCurrentController()
     }
diff --git a/app/src/main/java/exh/ui/lock/LockPreference.kt b/app/src/main/java/exh/ui/lock/LockPreference.kt
index 8985d3e21..824b2376e 100755
--- a/app/src/main/java/exh/ui/lock/LockPreference.kt
+++ b/app/src/main/java/exh/ui/lock/LockPreference.kt
@@ -2,11 +2,14 @@ package exh.ui.lock
 
 import android.content.Context
 import android.support.v7.preference.Preference
+import android.support.v7.preference.SwitchPreference
+import android.support.v7.preference.SwitchPreferenceCompat
 import android.text.InputType
 import android.util.AttributeSet
 import com.afollestad.materialdialogs.MaterialDialog
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.ui.setting.onChange
 import rx.Observable
 import rx.android.schedulers.AndroidSchedulers
 import rx.schedulers.Schedulers
@@ -15,7 +18,7 @@ import java.math.BigInteger
 import java.security.SecureRandom
 
 class LockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
-        Preference(context, attrs) {
+        SwitchPreferenceCompat(context, attrs) {
 
     private val secureRandom by lazy { SecureRandom() }
 
@@ -24,17 +27,24 @@ class LockPreference @JvmOverloads constructor(context: Context, attrs: Attribut
     override fun onAttached() {
         super.onAttached()
         updateSummary()
+        onChange {
+            tryChange()
+            false
+        }
     }
 
     private fun updateSummary() {
-        summary = if(lockEnabled(prefs))
-            "Application is locked"
-        else
-            "Application is not locked, tap to lock"
+        isChecked = lockEnabled(prefs)
+        if(isChecked) {
+            title = "Lock enabled"
+            summary = "Tap to disable or change pin code"
+        } else {
+            title = "Lock disabled"
+            summary = "Tap to enable"
+        }
     }
 
-    override fun onClick() {
-        super.onClick()
+    fun tryChange() {
         if(!notifyLockSecurity(context)) {
             MaterialDialog.Builder(context)
                     .title("Lock application")
diff --git a/app/src/main/java/exh/ui/lock/LockPresenter.kt b/app/src/main/java/exh/ui/lock/LockPresenter.kt
index 22c02d31c..5941e6f12 100644
--- a/app/src/main/java/exh/ui/lock/LockPresenter.kt
+++ b/app/src/main/java/exh/ui/lock/LockPresenter.kt
@@ -1,6 +1,19 @@
 package exh.ui.lock
 
+import android.os.Build
+import com.github.ajalt.reprint.core.Reprint
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import uy.kohesive.injekt.injectLazy
 
-class LockPresenter: BasePresenter<LockController>()
+class LockPresenter: BasePresenter<LockController>() {
+    val prefs: PreferencesHelper by injectLazy()
+
+    val useFingerprint
+        get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+                && Reprint.isHardwarePresent()
+                && Reprint.hasFingerprintRegistered()
+                && prefs.lockUseFingerprint().getOrDefault()
+}
 
diff --git a/app/src/main/java/exh/ui/login/LoginActivity.kt b/app/src/main/java/exh/ui/login/LoginController.kt
similarity index 66%
rename from app/src/main/java/exh/ui/login/LoginActivity.kt
rename to app/src/main/java/exh/ui/login/LoginController.kt
index a997969fb..7b495fb63 100755
--- a/app/src/main/java/exh/ui/login/LoginActivity.kt
+++ b/app/src/main/java/exh/ui/login/LoginController.kt
@@ -3,7 +3,9 @@ package exh.ui.login
 import android.net.Uri
 import android.os.Build
 import android.os.Bundle
-import android.view.MenuItem
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
 import android.webkit.CookieManager
 import android.webkit.WebView
 import android.webkit.WebViewClient
@@ -12,9 +14,9 @@ import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.source.online.all.EHentai
-import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 import exh.EXH_SOURCE_ID
-import kotlinx.android.synthetic.main.eh_activity_login.*
+import kotlinx.android.synthetic.main.eh_activity_login.view.*
 import rx.Observable
 import rx.android.schedulers.AndroidSchedulers
 import rx.schedulers.Schedulers
@@ -23,70 +25,75 @@ import uy.kohesive.injekt.injectLazy
 import java.net.HttpCookie
 
 /**
- * LoginActivity
+ * LoginController
  */
 
-class LoginActivity : BaseActivity() {
-
+class LoginController : NucleusController<LoginPresenter>() {
     val preferenceManager: PreferencesHelper by injectLazy()
 
     val sourceManager: SourceManager by injectLazy()
 
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setContentView(R.layout.eh_activity_login)
+    override fun getTitle() = "ExHentai login"
 
-        setup()
-    }
+    override fun createPresenter() = LoginPresenter()
 
-    fun setup() {
-        btn_cancel.setOnClickListener { onBackPressed() }
-        btn_recheck.setOnClickListener { webview.loadUrl("http://exhentai.org/") }
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup) =
+            inflater.inflate(R.layout.eh_activity_login, container, false)!!
 
-        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-            CookieManager.getInstance().removeAllCookies {
-                runOnUiThread {
-                    startWebview()
+    override fun onViewCreated(view: View, savedViewState: Bundle?) {
+        super.onViewCreated(view, savedViewState)
+
+        with(view) {
+            btn_cancel.setOnClickListener { router.popCurrentController() }
+            btn_recheck.setOnClickListener { webview.loadUrl("http://exhentai.org/") }
+
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                CookieManager.getInstance().removeAllCookies {
+                    Observable.fromCallable {
+                        startWebview(view)
+                    }.subscribeOn(AndroidSchedulers.mainThread()).subscribe()
                 }
+            } else {
+                CookieManager.getInstance().removeAllCookie()
+                startWebview(view)
             }
-        } else {
-            CookieManager.getInstance().removeAllCookie()
-            startWebview()
         }
     }
 
-    fun startWebview() {
-        webview.settings.javaScriptEnabled = true
-        webview.settings.domStorageEnabled = true
+    fun startWebview(view: View) {
+        with(view) {
+            webview.settings.javaScriptEnabled = true
+            webview.settings.domStorageEnabled = true
 
-        webview.loadUrl("https://forums.e-hentai.org/index.php?act=Login")
+            webview.loadUrl("https://forums.e-hentai.org/index.php?act=Login")
 
-        webview.setWebViewClient(object : WebViewClient() {
-            override fun onPageFinished(view: WebView, url: String) {
-                super.onPageFinished(view, url)
-                Timber.d(url)
-                val parsedUrl = Uri.parse(url)
-                if(parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) {
-                    //Hide distracting content
-                    view.loadUrl(HIDE_JS)
+            webview.setWebViewClient(object : WebViewClient() {
+                override fun onPageFinished(view: WebView, url: String) {
+                    super.onPageFinished(view, url)
+                    Timber.d(url)
+                    val parsedUrl = Uri.parse(url)
+                    if (parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) {
+                        //Hide distracting content
+                        view.loadUrl(HIDE_JS)
 
-                    //Check login result
-                    if(parsedUrl.getQueryParameter("code")?.toInt() != 0) {
-                        if(checkLoginCookies(url)) view.loadUrl("http://exhentai.org/")
-                    }
-                } else if(parsedUrl.host.equals("exhentai.org", ignoreCase = true)) {
-                    //At ExHentai, check that everything worked out...
-                    if(applyExHentaiCookies(url)) {
-                        preferenceManager.enableExhentai().set(true)
-                        finishLogin()
+                        //Check login result
+                        if (parsedUrl.getQueryParameter("code")?.toInt() != 0) {
+                            if (checkLoginCookies(url)) view.loadUrl("http://exhentai.org/")
+                        }
+                    } else if (parsedUrl.host.equals("exhentai.org", ignoreCase = true)) {
+                        //At ExHentai, check that everything worked out...
+                        if (applyExHentaiCookies(url)) {
+                            preferenceManager.enableExhentai().set(true)
+                            finishLogin(view)
+                        }
                     }
                 }
-            }
-        })
+            })
+        }
     }
 
-    fun finishLogin() {
-        val progressDialog = MaterialDialog.Builder(this)
+    fun finishLogin(view: View) {
+        val progressDialog = MaterialDialog.Builder(view.context)
                 .title("Finalizing login")
                 .progress(true, 0)
                 .content("Please wait...")
@@ -108,7 +115,7 @@ class LoginActivity : BaseActivity() {
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe {
                     progressDialog.dismiss()
-                    onBackPressed()
+                    router.popCurrentController()
                 }
     }
 
@@ -164,14 +171,6 @@ class LoginActivity : BaseActivity() {
         }
     }
 
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        when (item.itemId) {
-            android.R.id.home -> onBackPressed()
-            else -> return super.onOptionsItemSelected(item)
-        }
-        return true
-    }
-
     companion object {
         const val MEMBER_ID_COOKIE = "ipb_member_id"
         const val PASS_HASH_COOKIE = "ipb_pass_hash"
diff --git a/app/src/main/java/exh/ui/login/LoginPresenter.kt b/app/src/main/java/exh/ui/login/LoginPresenter.kt
new file mode 100644
index 000000000..3d39afcc5
--- /dev/null
+++ b/app/src/main/java/exh/ui/login/LoginPresenter.kt
@@ -0,0 +1,7 @@
+package exh.ui.login
+
+import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+
+class LoginPresenter: BasePresenter<LoginController>() {
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/exh/util/ViewUtil.kt b/app/src/main/java/exh/util/ViewUtil.kt
new file mode 100644
index 000000000..edd35d1a9
--- /dev/null
+++ b/app/src/main/java/exh/util/ViewUtil.kt
@@ -0,0 +1,8 @@
+package exh.util
+
+import android.content.Context
+
+fun dpToPx(context: Context, dp: Int): Int {
+    val scale = context.resources.displayMetrics.density
+    return (dp * scale + 0.5f).toInt()
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_lock.xml b/app/src/main/res/layout/activity_lock.xml
index 7005ce1ba..ad4ac7150 100755
--- a/app/src/main/res/layout/activity_lock.xml
+++ b/app/src/main/res/layout/activity_lock.xml
@@ -4,6 +4,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    xmlns:card_view="http://schemas.android.com/apk/res-auto"
     android:background="@color/backgroundDark">
 
     <com.andrognito.pinlockview.PinLockView
@@ -26,4 +27,19 @@
         app:layout_constraintRight_toRightOf="parent"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintVertical_chainStyle="packed" />
+
+    <android.support.v7.widget.CardView
+        android:id="@+id/swirl_container"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="8dp"
+        android:layout_marginLeft="8dp"
+        android:layout_marginRight="8dp"
+        card_view:cardCornerRadius="30dp"
+        card_view:cardElevation="4dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent">
+
+    </android.support.v7.widget.CardView>
 </android.support.constraint.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/eh_fragment_batch_add.xml b/app/src/main/res/layout/eh_fragment_batch_add.xml
index 03faf0ec6..a4001371a 100755
--- a/app/src/main/res/layout/eh_fragment_batch_add.xml
+++ b/app/src/main/res/layout/eh_fragment_batch_add.xml
@@ -11,7 +11,7 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:animateLayoutChanges="true"
-        android:padding="16dp ">
+        android:padding="16dp">
 
         <TextView
             android:id="@+id/input_title_view"
@@ -19,7 +19,7 @@
             android:layout_height="wrap_content"
             android:text="Enter the galleries to add (separated by a new line):"
             android:textAppearance="@style/TextAppearance.Medium.Title"
-            android:visibility="gone"
+            android:visibility="visible"
             app:layout_constraintLeft_toLeftOf="parent"
             app:layout_constraintRight_toRightOf="parent"
             app:layout_constraintTop_toTopOf="parent" />
@@ -31,7 +31,7 @@
             android:ems="10"
             android:hint="Example:\n\nhttp://e-hentai.org/g/12345/1a2b3c4e\nhttp://g.e-hentai.org/g/67890/6f7g8h9i\nhttp://exhentai.org/g/13579/1a3b5c7e\nhttps://exhentai.org/g/24680/2f4g6h8i\n"
             android:inputType="textMultiLine"
-            android:visibility="gone"
+            android:visibility="visible"
             app:layout_constraintBottom_toTopOf="@+id/btn_add_galleries"
             app:layout_constraintLeft_toLeftOf="@+id/input_title_view"
             app:layout_constraintRight_toRightOf="@+id/input_title_view"
@@ -42,7 +42,7 @@
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:text="Add Galleries"
-            android:visibility="gone"
+            android:visibility="visible"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintLeft_toLeftOf="@+id/galleries_box"
             app:layout_constraintRight_toRightOf="@+id/galleries_box" />
@@ -58,7 +58,7 @@
             android:layout_marginTop="0dp"
             android:text="Adding galleries..."
             android:textAppearance="@style/TextAppearance.Medium.Title"
-            android:visibility="visible"
+            android:visibility="gone"
             app:layout_constraintLeft_toLeftOf="parent"
             app:layout_constraintRight_toRightOf="parent"
             app:layout_constraintTop_toTopOf="parent" />
@@ -71,7 +71,7 @@
             android:layout_marginBottom="0dp"
             android:layout_marginEnd="8dp"
             android:layout_marginRight="8dp"
-            android:visibility="visible"
+            android:visibility="gone"
             app:layout_constraintBottom_toTopOf="@+id/progress_dismiss_btn"
             app:layout_constraintHorizontal_bias="0.0"
             app:layout_constraintLeft_toLeftOf="@+id/progress_log_wrapper"
@@ -85,7 +85,7 @@
             android:singleLine="true"
             android:text=""
             android:textAlignment="textEnd"
-            android:visibility="visible"
+            android:visibility="gone"
             app:layout_constraintRight_toRightOf="@+id/progress_log_wrapper"
             app:layout_constraintTop_toTopOf="@+id/progress_bar" />
 
@@ -93,15 +93,15 @@
             android:id="@+id/progress_dismiss_btn"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
+            android:layout_marginBottom="0dp"
             android:layout_marginLeft="0dp"
             android:layout_marginRight="0dp"
             android:text="Finish"
-            android:visibility="visible"
-            app:layout_constraintLeft_toLeftOf="@+id/progress_log_wrapper"
-            app:layout_constraintRight_toRightOf="@+id/progress_log_wrapper"
-            app:layout_constraintHorizontal_bias="0.0"
+            android:visibility="gone"
             app:layout_constraintBottom_toBottomOf="@+id/btn_add_galleries"
-            android:layout_marginBottom="0dp" />
+            app:layout_constraintHorizontal_bias="0.0"
+            app:layout_constraintLeft_toLeftOf="@+id/progress_log_wrapper"
+            app:layout_constraintRight_toRightOf="@+id/progress_log_wrapper" />
 
         <ScrollView
             android:id="@+id/progress_log_wrapper"
@@ -109,7 +109,7 @@
             android:layout_height="0dp"
             android:layout_marginBottom="8dp"
             android:layout_marginTop="8dp"
-            android:visibility="visible"
+            android:visibility="gone"
             app:layout_constraintBottom_toTopOf="@+id/progress_bar"
             app:layout_constraintHorizontal_bias="0.0"
             app:layout_constraintLeft_toLeftOf="@+id/progress_title_view"
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 8a82972a3..e1faa2590 100755
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -24,6 +24,10 @@
         <item name="android:divider">@color/dividerLight</item>
         <item name="android:listDivider">@drawable/line_divider_light</item>
 
+        <!-- Swirl (EH) -->
+        <item name="swirl_ridgeColor">?android:attr/textColorSecondary</item>
+        <item name="swirl_errorColor">?android:attr/colorAccent</item>
+
         <!-- Themes -->
         <item name="windowActionModeOverlay">true</item>
         <item name="actionBarTheme">@style/Theme.ActionBar.Light</item>