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