Biometrics lock (closes #1686)

This commit is contained in:
arkon 2020-02-21 22:58:19 -05:00
parent aa05458f1d
commit 8bb83782c7
11 changed files with 174 additions and 1 deletions

View File

@ -116,6 +116,11 @@ dependencies {
implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.multidex:multidex:2.0.1' 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 // UI library
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.1.0'

View File

@ -54,6 +54,9 @@
</activity> </activity>
<activity <activity
android:name=".ui.reader.ReaderActivity" /> android:name=".ui.reader.ReaderActivity" />
<activity
android:name=".ui.security.BiometricUnlockActivity"
android:theme="@style/Theme.Splash" />
<activity <activity
android:name=".ui.webview.WebViewActivity" android:name=".ui.webview.WebViewActivity"
android:configChanges="uiMode|orientation|screenSize" /> android:configChanges="uiMode|orientation|screenSize" />

View File

@ -3,18 +3,26 @@ package eu.kanade.tachiyomi
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.Configuration 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 androidx.multidex.MultiDex
import com.evernote.android.job.JobManager import com.evernote.android.job.JobManager
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.notification.Notifications 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.data.updater.UpdaterJob
import eu.kanade.tachiyomi.ui.security.BiometricUnlockDelegate
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import org.acra.ACRA import org.acra.ACRA
import org.acra.annotation.ReportsCrashes import org.acra.annotation.ReportsCrashes
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.registry.default.DefaultRegistrar import uy.kohesive.injekt.registry.default.DefaultRegistrar
@ReportsCrashes( @ReportsCrashes(
@ -24,7 +32,7 @@ import uy.kohesive.injekt.registry.default.DefaultRegistrar
buildConfigClass = BuildConfig::class, buildConfigClass = BuildConfig::class,
excludeMatchingSharedPreferencesKeys = [".*username.*", ".*password.*", ".*token.*"] excludeMatchingSharedPreferencesKeys = [".*username.*", ".*password.*", ".*token.*"]
) )
open class App : Application() { open class App : Application(), LifecycleObserver {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -38,6 +46,8 @@ open class App : Application() {
setupNotificationChannels() setupNotificationChannels()
LocaleHelper.updateConfiguration(this, resources.configuration) LocaleHelper.updateConfiguration(this, resources.configuration)
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
} }
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {
@ -50,6 +60,14 @@ open class App : Application() {
LocaleHelper.updateConfiguration(this, newConfig, true) 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() { protected open fun setupAcra() {
ACRA.init(this) ACRA.init(this)
} }

View File

@ -105,6 +105,12 @@ object PreferenceKeys {
const val startScreen = "start_screen" 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 downloadNew = "download_new"
const val downloadNewCategories = "download_new_categories" const val downloadNewCategories = "download_new_categories"

View File

@ -52,6 +52,12 @@ class PreferencesHelper(val context: Context) {
fun startScreen() = prefs.getInt(Keys.startScreen, 1) 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 clear() = prefs.edit().clear().apply()
fun themeMode() = rxPrefs.getString(Keys.themeMode, Values.THEME_MODE_SYSTEM) fun themeMode() = rxPrefs.getString(Keys.themeMode, Values.THEME_MODE_SYSTEM)

View File

@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatDelegate
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.security.BiometricUnlockDelegate
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
@ -46,4 +47,9 @@ abstract class BaseActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
override fun onResume() {
super.onResume()
BiometricUnlockDelegate.onResume(this)
}
} }

View File

@ -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.R2LPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer 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.lang.plusAssign
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.GLUtil
@ -149,6 +150,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
initializeMenu() initializeMenu()
} }
override fun onResume() {
super.onResume()
BiometricUnlockDelegate.onResume(this)
}
/** /**
* Called when the activity is destroyed. Cleans up the viewer, configuration and any view. * Called when the activity is destroyed. Cleans up the viewer, configuration and any view.
*/ */

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.setting package eu.kanade.tachiyomi.ui.setting
import android.os.Build import android.os.Build
import androidx.biometric.BiometricManager
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
@ -115,6 +116,36 @@ class SettingsGeneralController : SettingsController() {
defaultValue = "1" defaultValue = "1"
summary = "%s" 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 }
}
}
}
} }
} }

View File

@ -24,6 +24,7 @@
<string name="label_extension_info">Extension info</string> <string name="label_extension_info">Extension info</string>
<string name="label_help">Help</string> <string name="label_help">Help</string>
<string name="unlock_library">Unlock Library</string>
<!-- Actions --> <!-- Actions -->
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
@ -131,6 +132,16 @@
<string name="system_default">System default</string> <string name="system_default">System default</string>
<string name="pref_date_format">Date format</string> <string name="pref_date_format">Date format</string>
<string name="pref_category_security">Security</string>
<string name="lock_with_biometrics">Lock with biometrics</string>
<string name="lock_when_idle">Lock when idle</string>
<string name="lock_always">Always</string>
<string name="lock_never">Never</string>
<plurals name="lock_after_mins">
<item quantity="one">After 1 minute</item>
<item quantity="other">After %1$s minutes</item>
</plurals>
<!-- Library section --> <!-- Library section -->
<string name="pref_category_library_display">Display</string> <string name="pref_category_library_display">Display</string>
<string name="pref_library_columns">Library manga per row</string> <string name="pref_library_columns">Library manga per row</string>