From 8bb83782c7d636ad1a4e8daed05a1776659b9dcf Mon Sep 17 00:00:00 2001 From: arkon Date: Fri, 21 Feb 2020 22:58:19 -0500 Subject: [PATCH] Biometrics lock (closes #1686) --- app/build.gradle | 5 +++ app/src/main/AndroidManifest.xml | 3 ++ app/src/main/java/eu/kanade/tachiyomi/App.kt | 20 ++++++++- .../data/preference/PreferenceKeys.kt | 6 +++ .../data/preference/PreferencesHelper.kt | 6 +++ .../ui/base/activity/BaseActivity.kt | 6 +++ .../tachiyomi/ui/reader/ReaderActivity.kt | 6 +++ .../ui/security/BiometricUnlockActivity.kt | 45 +++++++++++++++++++ .../ui/security/BiometricUnlockDelegate.kt | 36 +++++++++++++++ .../ui/setting/SettingsGeneralController.kt | 31 +++++++++++++ app/src/main/res/values/strings.xml | 11 +++++ 11 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricUnlockActivity.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricUnlockDelegate.kt diff --git a/app/build.gradle b/app/build.gradle index f33a8bdbcc..27f478aa1d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -116,6 +116,11 @@ dependencies { implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.multidex:multidex:2.0.1' + implementation 'androidx.biometric:biometric:1.0.1' + + final lifecycle_version = '2.1.0' + implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" // UI library implementation 'com.google.android.material:material:1.1.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 88dd239560..8a038bdd1b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -54,6 +54,9 @@ + diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index c3a338a457..fd68315bb4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -3,18 +3,26 @@ package eu.kanade.tachiyomi import android.app.Application import android.content.Context import android.content.res.Configuration +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import androidx.lifecycle.ProcessLifecycleOwner import androidx.multidex.MultiDex import com.evernote.android.job.JobManager import eu.kanade.tachiyomi.data.backup.BackupCreatorJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.updater.UpdaterJob +import eu.kanade.tachiyomi.ui.security.BiometricUnlockDelegate import eu.kanade.tachiyomi.util.system.LocaleHelper import org.acra.ACRA import org.acra.annotation.ReportsCrashes import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.InjektScope +import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.registry.default.DefaultRegistrar @ReportsCrashes( @@ -24,7 +32,7 @@ import uy.kohesive.injekt.registry.default.DefaultRegistrar buildConfigClass = BuildConfig::class, excludeMatchingSharedPreferencesKeys = [".*username.*", ".*password.*", ".*token.*"] ) -open class App : Application() { +open class App : Application(), LifecycleObserver { override fun onCreate() { super.onCreate() @@ -38,6 +46,8 @@ open class App : Application() { setupNotificationChannels() LocaleHelper.updateConfiguration(this, resources.configuration) + + ProcessLifecycleOwner.get().lifecycle.addObserver(this) } override fun attachBaseContext(base: Context) { @@ -50,6 +60,14 @@ open class App : Application() { LocaleHelper.updateConfiguration(this, newConfig, true) } + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + fun onAppBackgrounded() { + val preferences: PreferencesHelper by injectLazy() + if (preferences.lockAppAfter().getOrDefault() >= 0) { + BiometricUnlockDelegate.locked = true + } + } + protected open fun setupAcra() { ACRA.init(this) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index bb4efcd0f6..d714dbd4cd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -105,6 +105,12 @@ object PreferenceKeys { const val startScreen = "start_screen" + const val useBiometricLock = "use_biometric_lock" + + const val lockAppAfter = "lock_app_after" + + const val lastAppUnlock = "last_app_unlock" + const val downloadNew = "download_new" const val downloadNewCategories = "download_new_categories" 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 22a4a6b664..d4500176c8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -52,6 +52,12 @@ class PreferencesHelper(val context: Context) { fun startScreen() = prefs.getInt(Keys.startScreen, 1) + fun useBiometricLock() = rxPrefs.getBoolean(Keys.useBiometricLock, false) + + fun lockAppAfter() = rxPrefs.getInteger(Keys.lockAppAfter, 0) + + fun lastAppUnlock() = rxPrefs.getLong(Keys.lastAppUnlock, 0) + fun clear() = prefs.edit().clear().apply() fun themeMode() = rxPrefs.getString(Keys.themeMode, Values.THEME_MODE_SYSTEM) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt index 7da27f88c4..2935982f10 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt @@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatDelegate import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.ui.security.BiometricUnlockDelegate import eu.kanade.tachiyomi.util.system.LocaleHelper import uy.kohesive.injekt.injectLazy import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values @@ -46,4 +47,9 @@ abstract class BaseActivity : AppCompatActivity() { super.onCreate(savedInstanceState) } + override fun onResume() { + super.onResume() + BiometricUnlockDelegate.onResume(this) + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 2973872c99..6a0addc6a6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -33,6 +33,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer +import eu.kanade.tachiyomi.ui.security.BiometricUnlockDelegate import eu.kanade.tachiyomi.util.lang.plusAssign import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.system.GLUtil @@ -149,6 +150,11 @@ class ReaderActivity : BaseRxActivity() { initializeMenu() } + override fun onResume() { + super.onResume() + BiometricUnlockDelegate.onResume(this) + } + /** * Called when the activity is destroyed. Cleans up the viewer, configuration and any view. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricUnlockActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricUnlockActivity.kt new file mode 100644 index 0000000000..fdfaad2b81 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricUnlockActivity.kt @@ -0,0 +1,45 @@ +package eu.kanade.tachiyomi.ui.security + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricPrompt +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import uy.kohesive.injekt.injectLazy +import java.util.Date +import java.util.concurrent.Executors + +/** + * Blank activity with a BiometricPrompt. + */ +class BiometricUnlockActivity : AppCompatActivity() { + + private val preferences: PreferencesHelper by injectLazy() + private val executor = Executors.newSingleThreadExecutor() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val biometricPrompt = BiometricPrompt(this, executor, object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + finishAffinity() + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + BiometricUnlockDelegate.locked = false + preferences.lastAppUnlock().set(Date().time) + finish() + } + }) + + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.unlock_library)) + .setDeviceCredentialAllowed(true) + .build() + + biometricPrompt.authenticate(promptInfo) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricUnlockDelegate.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricUnlockDelegate.kt new file mode 100644 index 0000000000..ab684fa649 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricUnlockDelegate.kt @@ -0,0 +1,36 @@ +package eu.kanade.tachiyomi.ui.security + +import android.content.Intent +import androidx.biometric.BiometricManager +import androidx.fragment.app.FragmentActivity +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import uy.kohesive.injekt.injectLazy +import java.util.Date + +object BiometricUnlockDelegate { + + private val preferences by injectLazy() + + var locked: Boolean = true + + fun onResume(activity: FragmentActivity) { + val lockApp = preferences.useBiometricLock().getOrDefault() + if (lockApp && BiometricManager.from(activity).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { + if (isAppLocked()) { + val intent = Intent(activity, BiometricUnlockActivity::class.java) + activity.startActivity(intent) + activity.overridePendingTransition(0, 0) + } + } else if (lockApp) { + preferences.useBiometricLock().set(false) + } + } + + private fun isAppLocked(): Boolean { + return locked && + (preferences.lockAppAfter().getOrDefault() <= 0 + || Date().time >= preferences.lastAppUnlock().getOrDefault() + 60 * 1000 * preferences.lockAppAfter().getOrDefault()) + } + +} 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 14f1d0eb9a..b82ead818c 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 @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.setting import android.os.Build +import androidx.biometric.BiometricManager import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.getOrDefault @@ -115,6 +116,36 @@ class SettingsGeneralController : SettingsController() { defaultValue = "1" summary = "%s" } + + if (BiometricManager.from(context).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { + preferenceCategory { + titleRes = R.string.pref_category_security + + switchPreference { + key = Keys.useBiometricLock + titleRes = R.string.lock_with_biometrics + defaultValue = false + } + intListPreference { + key = Keys.lockAppAfter + titleRes = R.string.lock_when_idle + val values = arrayOf("0", "1", "2", "5", "10", "-1") + entries = values.mapNotNull { + when (it) { + "-1" -> context.getString(R.string.lock_never) + "0" -> context.getString(R.string.lock_always) + else -> resources?.getQuantityString(R.plurals.lock_after_mins, it.toInt(), it) + } + }.toTypedArray() + entryValues = values + defaultValue = "0" + summary = "%s" + + preferences.useBiometricLock().asObservable() + .subscribeUntilDestroy { isVisible = it } + } + } + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ed53ea60e8..49a9e922e8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,6 +24,7 @@ Extension info Help + Unlock Library Settings @@ -131,6 +132,16 @@ System default Date format + Security + Lock with biometrics + Lock when idle + Always + Never + + After 1 minute + After %1$s minutes + + Display Library manga per row