Explorar el Código

Biometrics lock (closes #1686)

arkon hace 5 años
padre
commit
8bb83782c7

+ 5 - 0
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'

+ 3 - 0
app/src/main/AndroidManifest.xml

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

+ 19 - 1
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)
     }

+ 6 - 0
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"

+ 6 - 0
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)

+ 6 - 0
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)
+    }
+
 }

+ 6 - 0
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<ReaderPresenter>() {
         initializeMenu()
     }
 
+    override fun onResume() {
+        super.onResume()
+        BiometricUnlockDelegate.onResume(this)
+    }
+
     /**
      * Called when the activity is destroyed. Cleans up the viewer, configuration and any view.
      */

+ 45 - 0
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)
+    }
+
+}

+ 36 - 0
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<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())
+    }
+
+}

+ 31 - 0
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 }
+                }
+            }
+        }
     }
 
 }

+ 11 - 0
app/src/main/res/values/strings.xml

@@ -24,6 +24,7 @@
     <string name="label_extension_info">Extension info</string>
     <string name="label_help">Help</string>
 
+    <string name="unlock_library">Unlock Library</string>
 
     <!-- Actions -->
     <string name="action_settings">Settings</string>
@@ -131,6 +132,16 @@
     <string name="system_default">System default</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 -->
     <string name="pref_category_library_display">Display</string>
     <string name="pref_library_columns">Library manga per row</string>