diff --git a/README.md b/README.md
index 7fc99edf4b..095c6816cf 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ Tachiyomi is a free and open source manga reader for Android.
![screenshots of app](./.github/readme-images/theming-screenshots.gif)
## Newest Release
-[v0.9.2](https://github.com/Jays2Kings/tachiyomi/releases)
+[v0.9.3](https://github.com/Jays2Kings/tachiyomi/releases)
## Features
diff --git a/app/build.gradle b/app/build.gradle
index ab1c627797..efdfa26707 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -38,8 +38,8 @@ android {
minSdkVersion 21
targetSdkVersion 29
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- versionCode 44
- versionName '0.9.2'
+ versionCode 45
+ versionName '0.9.3'
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@@ -115,10 +115,11 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
- implementation 'androidx.recyclerview:recyclerview:1.0.0'
+ implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.browser:browser:1.0.0'
+ implementation 'androidx.biometric:biometric:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
@@ -126,6 +127,10 @@ dependencies {
standardImplementation 'com.google.firebase:firebase-core:17.2.1'
+ final lifecycle_version = "2.1.0"
+ implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
+
// ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'io.reactivex:rxjava:1.3.8'
@@ -245,7 +250,7 @@ dependencies {
}
buildscript {
- ext.kotlin_version = '1.3.50'
+ ext.kotlin_version = '1.3.61'
repositories {
mavenCentral()
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3861b81dac..6801ed687b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -50,6 +50,8 @@
+
= 0) {
+ MainActivity.unlocked = false
+ }
}
override fun attachBaseContext(base: Context) {
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 b9a2744741..4668ac1972 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
@@ -117,6 +117,12 @@ object PreferenceKeys {
const val downloadBadge = "display_download_badge"
+ const val useBiometrics = "use_biometrics"
+
+ const val lockAfter = "lock_after"
+
+ const val lastUnlock = "last_unlock"
+
@Deprecated("Use the preferences of the source")
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
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 17cbc29e0e..79f16009fe 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
@@ -176,6 +176,12 @@ class PreferencesHelper(val context: Context) {
fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
+ fun useBiometrics() = rxPrefs.getBoolean(Keys.useBiometrics, false)
+
+ fun lockAfter() = rxPrefs.getInteger(Keys.lockAfter, 0)
+
+ fun lastUnlock() = rxPrefs.getLong(Keys.lastUnlock, 0)
+
fun migrateFlags() = rxPrefs.getInteger("migrate_flags", Int.MAX_VALUE)
fun trustedSignatures() = rxPrefs.getStringSet("trusted_signatures", emptySet())
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/BiometricActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/BiometricActivity.kt
new file mode 100644
index 0000000000..1b18e44c16
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/BiometricActivity.kt
@@ -0,0 +1,47 @@
+package eu.kanade.tachiyomi.ui.main
+
+import android.os.Bundle
+import androidx.biometric.BiometricPrompt
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
+import uy.kohesive.injekt.injectLazy
+import java.util.Date
+import java.util.concurrent.Executors
+
+class BiometricActivity : BaseActivity() {
+ val executor = Executors.newSingleThreadExecutor()
+
+ val preferences: PreferencesHelper by injectLazy()
+ 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)
+ MainActivity.unlocked = true
+ preferences.lastUnlock().set(Date().time)
+ finish()
+ }
+
+ override fun onAuthenticationFailed() {
+ super.onAuthenticationFailed()
+ // TODO("Called when a biometric is valid but not recognized.")
+ }
+ })
+
+ val promptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setTitle(getString(R.string.unlock_library))
+ .setNegativeButtonText(getString(android.R.string.cancel))
+ .build()
+
+ biometricPrompt.authenticate(promptInfo)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
index 37318f2901..95c4e10647 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
@@ -16,6 +16,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
+import androidx.biometric.BiometricManager
import androidx.core.graphics.ColorUtils
import com.bluelinelabs.conductor.*
import com.google.android.material.snackbar.Snackbar
@@ -23,6 +24,7 @@ import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.base.controller.*
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
@@ -46,6 +48,7 @@ import eu.kanade.tachiyomi.util.updatePaddingRelative
import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.coroutines.delay
import uy.kohesive.injekt.injectLazy
+import java.util.Date
class MainActivity : BaseActivity() {
@@ -60,7 +63,6 @@ class MainActivity : BaseActivity() {
private var snackBar:Snackbar? = null
var extraViewForUndo:View? = null
private var canDismissSnackBar = false
-
fun setUndoSnackBar(snackBar: Snackbar?, extraViewToCheck: View? = null) {
this.snackBar = snackBar
canDismissSnackBar = false
@@ -248,6 +250,22 @@ class MainActivity : BaseActivity() {
}
}
+ override fun onResume() {
+ super.onResume()
+ val useBiometrics = preferences.useBiometrics().getOrDefault()
+ if (useBiometrics && BiometricManager.from(this)
+ .canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
+ if (!unlocked && (preferences.lockAfter().getOrDefault() <= 0 || Date().time >=
+ preferences.lastUnlock().getOrDefault() + 60 * 1000 * preferences.lockAfter().getOrDefault())) {
+ val intent = Intent(this, BiometricActivity::class.java)
+ startActivity(intent)
+ this.overridePendingTransition(0, 0)
+ }
+ }
+ else if (useBiometrics)
+ preferences.useBiometrics().set(false)
+ }
+
override fun onNewIntent(intent: Intent) {
if (!handleIntentAction(intent)) {
super.onNewIntent(intent)
@@ -314,6 +332,7 @@ class MainActivity : BaseActivity() {
} else if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) {
setSelectedDrawerItem(startScreenId)
} else if (backstackSize == 1 || !router.handleBack()) {
+ unlocked = false
super.onBackPressed()
}
}
@@ -412,6 +431,8 @@ class MainActivity : BaseActivity() {
const val INTENT_SEARCH_FILTER = "filter"
private const val URL_HELP = "https://tachiyomi.org/help/"
+
+ var unlocked = false
}
}
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 6b644f3895..56f128d56e 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
@@ -11,15 +11,19 @@ import android.graphics.Bitmap
import android.graphics.Color
import android.os.Build
import android.os.Bundle
-import com.google.android.material.bottomsheet.BottomSheetDialog
-import android.view.*
+import android.view.KeyEvent
+import android.view.Menu
+import android.view.MenuItem
+import android.view.MotionEvent
+import android.view.View
+import android.view.WindowManager
import android.view.animation.Animation
import android.view.animation.AnimationUtils
-import android.widget.LinearLayout
import android.widget.SeekBar
+import androidx.biometric.BiometricManager
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
+import com.google.android.material.bottomsheet.BottomSheetDialog
import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@@ -27,6 +31,8 @@ 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.ui.base.activity.BaseRxActivity
+import eu.kanade.tachiyomi.ui.main.BiometricActivity
+import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success
@@ -38,12 +44,17 @@ 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.util.*
+import eu.kanade.tachiyomi.util.GLUtil
+import eu.kanade.tachiyomi.util.getResourceColor
+import eu.kanade.tachiyomi.util.getUriCompat
+import eu.kanade.tachiyomi.util.gone
+import eu.kanade.tachiyomi.util.launchUI
+import eu.kanade.tachiyomi.util.plusAssign
+import eu.kanade.tachiyomi.util.toast
+import eu.kanade.tachiyomi.util.visible
import eu.kanade.tachiyomi.widget.SimpleAnimationListener
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
import kotlinx.android.synthetic.main.reader_activity.*
-import kotlinx.android.synthetic.main.reader_activity.toolbar
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import me.zhanghai.android.systemuihelper.SystemUiHelper
@@ -55,6 +66,7 @@ import rx.subscriptions.CompositeSubscription
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.io.File
+import java.util.Date
import java.util.concurrent.TimeUnit
/**
@@ -506,6 +518,23 @@ class ReaderActivity : BaseRxActivity(),
presenter.shareImage(page)
}
+ override fun onResume() {
+ super.onResume()
+ val useBiometrics = preferences.useBiometrics().getOrDefault()
+ if (useBiometrics && BiometricManager.from(this)
+ .canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
+ if (!MainActivity.unlocked && (preferences.lockAfter().getOrDefault() <= 0 || Date()
+ .time >=
+ preferences.lastUnlock().getOrDefault() + 60 * 1000 * preferences.lockAfter().getOrDefault())) {
+ val intent = Intent(this, BiometricActivity::class.java)
+ startActivity(intent)
+ this.overridePendingTransition(0, 0)
+ }
+ }
+ else if (useBiometrics)
+ preferences.useBiometrics().set(false)
+ }
+
/**
* Called from the presenter when a page is ready to be shared. It shows Android's default
* sharing tool.
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 e93a95f4c6..a3828bb25f 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,12 +1,11 @@
package eu.kanade.tachiyomi.ui.setting
import android.app.Dialog
-import android.os.Build
import android.os.Bundle
import android.os.Handler
-import androidx.appcompat.app.AppCompatDelegate
-import androidx.preference.PreferenceScreen
import android.view.View
+import androidx.biometric.BiometricManager
+import androidx.preference.PreferenceScreen
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@@ -16,6 +15,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.LocaleHelper
+import eu.kanade.tachiyomi.widget.preference.IntListPreference
import kotlinx.android.synthetic.main.pref_library_columns.view.*
import rx.Observable
import uy.kohesive.injekt.Injekt
@@ -199,6 +199,36 @@ class SettingsGeneralController : SettingsController() {
true
}
}
+ val biometricManager = BiometricManager.from(context)
+ if (biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
+ var preference:IntListPreference? = null
+ switchPreference {
+ key = Keys.useBiometrics
+ titleRes = R.string.lock_with_biometrics
+ defaultValue = false
+
+ onChange {
+ preference?.isVisible = it as Boolean
+ true
+ }
+ }
+ preference = intListPreference {
+ key = Keys.lockAfter
+ titleRes = R.string.lock_when_idle
+ isVisible = preferences.useBiometrics().getOrDefault()
+ val values = arrayOf("0", "2", "5", "10", "20", "30", "60", "90", "120", "-1")
+ entries = values.map {
+ when (it) {
+ "0" -> context.getString(R.string.lock_always)
+ "-1" -> context.getString(R.string.lock_never)
+ else -> context.getString(R.string.lock_after_mins, it)
+ }
+ }.toTypedArray()
+ entryValues = values
+ defaultValue = "0"
+ summary = "%s"
+ }
+ }
}
class LibraryColumnsDialog : DialogController() {
diff --git a/app/src/main/res/raw/changelog_release.xml b/app/src/main/res/raw/changelog_release.xml
index 699959e4da..44fa174c6a 100644
--- a/app/src/main/res/raw/changelog_release.xml
+++ b/app/src/main/res/raw/changelog_release.xml
@@ -1,5 +1,12 @@
+
+ Lock Tachiyomi using your fingerprint/Biometrics
+ Added search/sorting/mass enable/disable to catalouge sources
+ Extensions are now filtered to your locale, with an option to show other languages
+ Fixed AMOLED theme not having dark snackbar
+
+
Fixes notification text when there are multiple chapters
Simplified errors now show after restoring a backup on the popup dialog
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a6003a8ed5..ce9c5a058b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -27,6 +27,7 @@
Extensions
Extension info
Help
+ Unlock to access Library
@@ -161,6 +162,11 @@
System default
Default category
Always ask
+ Lock with biometrics
+ Lock when idle
+ Always
+ Never
+ After %1$s minutes
All