mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Biometrics lock (closes #1686)
This commit is contained in:
		| @@ -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) | ||||
|     } | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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. | ||||
|      */ | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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()) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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 } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user