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
This commit is contained in:
NerdNumber9
2017-08-24 17:11:43 -04:00
parent 32d02f9329
commit b20b3d6f5c
21 changed files with 438 additions and 109 deletions

View File

@ -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)
}
}
}
}

View File

@ -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
}
}

View File

@ -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()
}
}
}

View File

@ -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))
}

View File

@ -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()
}

View File

@ -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")

View File

@ -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()
}

View File

@ -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"

View File

@ -0,0 +1,7 @@
package exh.ui.login
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
class LoginPresenter: BasePresenter<LoginController>() {
}

View File

@ -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()
}