mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-23 02:21:47 +01:00
Biometrics lock (closes #1686)
This commit is contained in:
parent
aa05458f1d
commit
8bb83782c7
@ -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'
|
||||||
|
@ -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" />
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user