diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 558fb658ef..68444cd86a 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -108,7 +108,7 @@ jobs: #Telegram curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \ - -F "document=@app/build/outputs/apk/google/alpha/app-google-universal-alpha.apk" \ + -F "document=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \ -F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \ https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument diff --git a/app/build.gradle b/app/build.gradle index 811cab900e..fe30452e98 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -101,6 +101,8 @@ dependencies { implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.webkit:webkit:1.11.0' implementation "com.anggrayudi:storage:1.5.5" + implementation "androidx.biometric:biometric:1.1.0" + // Glide ext.glide_version = '4.16.0' diff --git a/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt b/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt index 43b2cb5537..e37c68a552 100644 --- a/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt +++ b/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt @@ -16,6 +16,8 @@ import ani.dantotsu.navBarHeight import ani.dantotsu.profile.User import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.statusBarHeight +import ani.dantotsu.toast +import ani.dantotsu.util.Logger class StatusActivity : AppCompatActivity(), StoriesCallback { private lateinit var activity: ArrayList @@ -44,10 +46,14 @@ class StatusActivity : AppCompatActivity(), StoriesCallback { val key = "activities" val watchedActivity = PrefManager.getCustomVal>(key, setOf()) - val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity ) - val startIndex = if ( startFrom > 0) startFrom else 0 - binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1) - + if (activity.getOrNull(position) != null) { + val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity ) + val startIndex = if ( startFrom > 0) startFrom else 0 + binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1) + } else { + Logger.log("index out of bounds for position $position of size ${activity.size}") + finish() + } } private fun findFirstNonMatch(watchedActivity: Set, activity: List): Int { @@ -58,13 +64,16 @@ class StatusActivity : AppCompatActivity(), StoriesCallback { } return -1 } + override fun onPause() { super.onPause() binding.stories.pause() } + override fun onResume() { super.onResume() - binding.stories.resume() + if (hasWindowFocus()) + binding.stories.resume() } override fun onWindowFocusChanged(hasFocus: Boolean) { diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt index 44b044577f..048ba39848 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt @@ -424,7 +424,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi } override fun onResume() { - navBar.selectTabAt(selected) + if (::navBar.isInitialized) + navBar.selectTabAt(selected) super.onResume() } diff --git a/app/src/main/java/ani/dantotsu/others/CrashActivity.kt b/app/src/main/java/ani/dantotsu/others/CrashActivity.kt index d3b5e75425..c410d687b6 100644 --- a/app/src/main/java/ani/dantotsu/others/CrashActivity.kt +++ b/app/src/main/java/ani/dantotsu/others/CrashActivity.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.os.Bundle import android.view.View import android.view.ViewGroup +import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity import androidx.core.content.FileProvider import androidx.core.view.updateLayoutParams @@ -24,7 +25,10 @@ class CrashActivity : AppCompatActivity() { ThemeManager(this).applyTheme() initActivity(this) binding = ActivityCrashBinding.inflate(layoutInflater) - + window.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE + ) setContentView(binding.root) binding.root.updateLayoutParams { topMargin = statusBarHeight diff --git a/app/src/main/java/ani/dantotsu/others/calc/BiometricPromptUtils.kt b/app/src/main/java/ani/dantotsu/others/calc/BiometricPromptUtils.kt new file mode 100644 index 0000000000..6e91ed0756 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/others/calc/BiometricPromptUtils.kt @@ -0,0 +1,57 @@ +package ani.dantotsu.others.calc + +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import ani.dantotsu.R +import ani.dantotsu.util.Logger + +object BiometricPromptUtils { + private const val TAG = "BiometricPromptUtils" + + /** + * Create a BiometricPrompt instance + * @param activity: AppCompatActivity + * @param processSuccess: success callback + */ + fun createBiometricPrompt( + activity: AppCompatActivity, + processSuccess: (BiometricPrompt.AuthenticationResult) -> Unit + ): BiometricPrompt { + val executor = ContextCompat.getMainExecutor(activity) + + val callback = object : BiometricPrompt.AuthenticationCallback() { + + override fun onAuthenticationError(errCode: Int, errString: CharSequence) { + super.onAuthenticationError(errCode, errString) + Logger.log("$TAG errCode is $errCode and errString is: $errString") + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Logger.log("$TAG User biometric rejected.") + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + Log.d(TAG, "Authentication was successful") + processSuccess(result) + } + } + return BiometricPrompt(activity, executor, callback) + } + + /** + * Create a BiometricPrompt.PromptInfo instance + * @param activity: AppCompatActivity + * @return BiometricPrompt.PromptInfo: instance + */ + fun createPromptInfo(activity: AppCompatActivity): BiometricPrompt.PromptInfo = + BiometricPrompt.PromptInfo.Builder().apply { + setTitle(activity.getString(R.string.bio_prompt_info_title)) + setDescription(activity.getString(R.string.bio_prompt_info_desc)) + setConfirmationRequired(false) + setNegativeButtonText(activity.getString(R.string.cancel)) + }.build() +} diff --git a/app/src/main/java/ani/dantotsu/others/calc/CalcActivity.kt b/app/src/main/java/ani/dantotsu/others/calc/CalcActivity.kt index 442ab39629..a5d762d190 100644 --- a/app/src/main/java/ani/dantotsu/others/calc/CalcActivity.kt +++ b/app/src/main/java/ani/dantotsu/others/calc/CalcActivity.kt @@ -1,10 +1,14 @@ package ani.dantotsu.others.calc +import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan +import android.view.MotionEvent import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat @@ -16,6 +20,8 @@ import ani.dantotsu.databinding.ActivityCalcBinding import ani.dantotsu.getThemeColor import ani.dantotsu.initActivity import ani.dantotsu.navBarHeight +import ani.dantotsu.settings.saving.PrefManager +import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.statusBarHeight import ani.dantotsu.themes.ThemeManager import ani.dantotsu.util.NumberConverter.Companion.toBinary @@ -24,7 +30,13 @@ import ani.dantotsu.util.NumberConverter.Companion.toHex class CalcActivity : AppCompatActivity() { private lateinit var binding: ActivityCalcBinding private lateinit var code: String + private val handler = Handler(Looper.getMainLooper()) + private val runnable = Runnable { + success() + } private val stack = CalcStack() + + @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ThemeManager(this).applyTheme() @@ -73,6 +85,29 @@ class CalcActivity : AppCompatActivity() { binding.displayHex.text = "" binding.display.text = "0" } + if (PrefManager.getVal(PrefName.OverridePassword, false)) { + buttonClear.setOnTouchListener { v, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + handler.postDelayed(runnable, 10000) + true + } + + MotionEvent.ACTION_UP -> { + v.performClick() + handler.removeCallbacks(runnable) + true + } + + MotionEvent.ACTION_CANCEL -> { + handler.removeCallbacks(runnable) + true + } + + else -> false + } + } + } buttonBackspace.setOnClickListener { stack.remove() updateDisplay() @@ -81,6 +116,20 @@ class CalcActivity : AppCompatActivity() { } } + override fun onResume() { + super.onResume() + if (hasPermission) { + success() + } + if (PrefManager.getVal(PrefName.BiometricToken, "").isNotEmpty()) { + val bioMetricPrompt = BiometricPromptUtils.createBiometricPrompt(this) { + success() + } + val promptInfo = BiometricPromptUtils.createPromptInfo(this) + bioMetricPrompt.authenticate(promptInfo) + } + } + private fun success() { hasPermission = true ContextCompat.startActivity( diff --git a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt index 0fa6561258..748e476e5f 100644 --- a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt @@ -43,6 +43,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import uy.kohesive.injekt.injectLazy +import java.util.Locale class ExtensionsActivity : AppCompatActivity() { lateinit var binding: ActivityExtensionsBinding @@ -173,8 +174,11 @@ class ExtensionsActivity : AppCompatActivity() { initActivity(this) binding.languageselect.setOnClickListener { val languageOptions = - LanguageMapper.Companion.Language.entries.map { it.name }.toTypedArray() - val builder = AlertDialog.Builder(currContext(), R.style.MyPopup) + LanguageMapper.Companion.Language.entries.map { entry -> + entry.name.lowercase().replace("_", " ") + .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() } + }.toTypedArray() + val builder = AlertDialog.Builder(this, R.style.MyPopup) val listOrder: String = PrefManager.getVal(PrefName.LangSort) val index = LanguageMapper.Companion.Language.entries.toTypedArray() .indexOfFirst { it.code == listOrder } diff --git a/app/src/main/java/ani/dantotsu/settings/InstallerSteps.kt b/app/src/main/java/ani/dantotsu/settings/InstallerSteps.kt index 39b91b3eb1..e66d39c4d3 100644 --- a/app/src/main/java/ani/dantotsu/settings/InstallerSteps.kt +++ b/app/src/main/java/ani/dantotsu/settings/InstallerSteps.kt @@ -6,6 +6,7 @@ import androidx.core.app.NotificationCompat import ani.dantotsu.R import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.snackString +import ani.dantotsu.util.Logger import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.extension.InstallStep import uy.kohesive.injekt.Injekt @@ -30,6 +31,7 @@ class InstallerSteps( fun onError(error: Throwable, extra: () -> Unit) { Injekt.get().logException(error) + Logger.log(error) val builder = NotificationCompat.Builder( context, Notifications.CHANNEL_DOWNLOADER_ERROR diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt index bd590873dc..c8dc29d551 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt @@ -8,9 +8,12 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.ArrayAdapter +import android.widget.CheckBox import android.widget.EditText import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricManager +import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.documentfile.provider.DocumentFile import androidx.recyclerview.widget.LinearLayoutManager @@ -21,6 +24,7 @@ import ani.dantotsu.databinding.DialogUserAgentBinding import ani.dantotsu.download.DownloadsManager import ani.dantotsu.initActivity import ani.dantotsu.navBarHeight +import ani.dantotsu.others.calc.BiometricPromptUtils import ani.dantotsu.restartApp import ani.dantotsu.savePrefsToDownloads import ani.dantotsu.settings.saving.PrefManager @@ -39,6 +43,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.UUID class SettingsCommonActivity : AppCompatActivity() { @@ -183,14 +188,47 @@ class SettingsCommonActivity : AppCompatActivity() { (dialog as AlertDialog).findViewById(R.id.passwordInput) val confirmPasswordInput = dialog.findViewById(R.id.confirmPasswordInput) + val forgotPasswordCheckbox = + dialog.findViewById(R.id.forgotPasswordCheckbox) + if (forgotPasswordCheckbox?.isChecked == true) { + PrefManager.setVal(PrefName.OverridePassword, true) + } val password = passwordInput?.text.toString() val confirmPassword = confirmPasswordInput?.text.toString() if (password == confirmPassword && password.isNotEmpty()) { PrefManager.setVal(PrefName.AppPassword, password) - toast(R.string.success) - dialog.dismiss() + if (dialog.findViewById(R.id.biometricCheckbox)?.isChecked == true) { + val canBiometricPrompt = + BiometricManager.from(applicationContext) + .canAuthenticate( + BiometricManager.Authenticators.BIOMETRIC_WEAK + ) == BiometricManager.BIOMETRIC_SUCCESS + if (canBiometricPrompt) { + val biometricPrompt = + BiometricPromptUtils.createBiometricPrompt( + this@SettingsCommonActivity + ) { _ -> + val token = UUID.randomUUID().toString() + PrefManager.setVal( + PrefName.BiometricToken, + token + ) + toast(R.string.success) + dialog.dismiss() + } + val promptInfo = + BiometricPromptUtils.createPromptInfo( + this@SettingsCommonActivity + ) + biometricPrompt.authenticate(promptInfo) + } + } else { + PrefManager.setVal(PrefName.BiometricToken, "") + toast(R.string.success) + dialog.dismiss() + } } else { toast(R.string.password_mismatch) } @@ -200,6 +238,8 @@ class SettingsCommonActivity : AppCompatActivity() { } .setNeutralButton(R.string.remove) { dialog, _ -> PrefManager.setVal(PrefName.AppPassword, "") + PrefManager.setVal(PrefName.BiometricToken, "") + PrefManager.setVal(PrefName.OverridePassword, false) toast(R.string.success) dialog.dismiss() } @@ -209,6 +249,19 @@ class SettingsCommonActivity : AppCompatActivity() { passwordDialog.setOnShowListener { passwordDialog.findViewById(R.id.passwordInput) ?.requestFocus() + val biometricCheckbox = + passwordDialog.findViewById(R.id.biometricCheckbox) + val forgotPasswordCheckbox = + passwordDialog.findViewById(R.id.forgotPasswordCheckbox) + val canAuthenticate = + BiometricManager.from(applicationContext).canAuthenticate( + BiometricManager.Authenticators.BIOMETRIC_WEAK + ) == BiometricManager.BIOMETRIC_SUCCESS + biometricCheckbox?.isVisible = canAuthenticate + biometricCheckbox?.isChecked = + PrefManager.getVal(PrefName.BiometricToken, "").isNotEmpty() + forgotPasswordCheckbox?.isChecked = + PrefManager.getVal(PrefName.OverridePassword) } passwordDialog.show() } diff --git a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt index 1d69ebad10..fe6db422b3 100644 --- a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt +++ b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt @@ -202,4 +202,6 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files MALCodeChallenge(Pref(Location.Protected, String::class, "")), MALToken(Pref(Location.Protected, MAL.ResponseToken::class, "")), AppPassword(Pref(Location.Protected, String::class, "")), + BiometricToken(Pref(Location.Protected, String::class, "")), + OverridePassword(Pref(Location.Protected, Boolean::class, false)), } \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_calc.xml b/app/src/main/res/layout-land/activity_calc.xml new file mode 100644 index 0000000000..de7d9d5fdb --- /dev/null +++ b/app/src/main/res/layout-land/activity_calc.xml @@ -0,0 +1,390 @@ + + + + + + + + + + + + + + + + + + +