diff options
Diffstat (limited to 'app/src')
6 files changed, 101 insertions, 2 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1ca91d62..bd8776d1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,7 +12,8 @@ <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> - <!--<uses-permission android:name="android.permission.USE_FINGERPRINT" />--> + <uses-permission android:name="android.permission.USE_FINGERPRINT" /> + <uses-permission android:name="android.permission.USE_BIOMETRIC" /> <application android:name=".FrostApp" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt index 18ae4b0b..cf8acdd3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt @@ -42,6 +42,7 @@ import com.pitchedapps.frost.db.save import com.pitchedapps.frost.db.saveTabs import com.pitchedapps.frost.db.selectAll import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.utils.BiometricUtils import com.pitchedapps.frost.utils.EXTRA_COOKIES import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs @@ -79,6 +80,7 @@ class StartActivity : KauBaseActivity() { } launch { + val authDefer = BiometricUtils.authenticate(this@StartActivity) try { migrate() FbCookie.switchBackUser() @@ -86,6 +88,7 @@ class StartActivity : KauBaseActivity() { L.i { "Cookies loaded at time ${System.currentTimeMillis()}" } L._d { "Cookies: ${cookies.joinToString("\t", transform = CookieEntity::toSensitiveString)}" } loadAssets() + authDefer.await() when { cookies.isEmpty() -> launchNewTask<LoginActivity>() // Has cookies but no selected account diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt index f3226e2f..ec4ff9dd 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt @@ -64,6 +64,7 @@ import com.pitchedapps.frost.kotlin.subscribeDuringJob import com.pitchedapps.frost.services.FrostRunnable import com.pitchedapps.frost.utils.ARG_URL import com.pitchedapps.frost.utils.ARG_USER_ID +import com.pitchedapps.frost.utils.BiometricUtils import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.Showcase @@ -214,8 +215,10 @@ open class WebOverlayActivityBase(private val forceDesktopAgent: Boolean) : Base userAgentString = USER_AGENT_DESKTOP Prefs.prevId = Prefs.userId launch { + val authDefer = BiometricUtils.authenticate(this@WebOverlayActivityBase) if (userId != Prefs.userId) FbCookie.switchUser(userId) + authDefer.await() reloadBase(true) if (Showcase.firstWebOverlay) { coordinator.frostSnackbar(R.string.web_overlay_swipe_hint) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/BiometricUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/BiometricUtils.kt new file mode 100644 index 00000000..476e490d --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/BiometricUtils.kt @@ -0,0 +1,89 @@ +package com.pitchedapps.frost.utils + +import android.content.Context +import android.hardware.fingerprint.FingerprintManager +import android.os.Build +import androidx.biometric.BiometricPrompt +import androidx.fragment.app.FragmentActivity +import ca.allanwang.kau.utils.string +import com.pitchedapps.frost.R +import kotlinx.coroutines.CompletableDeferred +import java.util.concurrent.Executor +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +typealias BiometricDeferred = CompletableDeferred<BiometricPrompt.CryptoObject?> + +/** + * Container for [BiometricPrompt] + * Inspired by coroutine's CommonPool + */ +object BiometricUtils { + + private val executor: Executor + get() = pool ?: getOrCreatePoolSync() + + @Volatile + private var pool: ExecutorService? = null + + /** + * Checks if biometric authentication is possible + * Currently, this means checking for enrolled fingerprints + */ + @Suppress("DEPRECATION") + fun isSupported(context: Context): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false + val fingerprintManager = context.getSystemService(FingerprintManager::class.java) ?: return false + return fingerprintManager.isHardwareDetected && fingerprintManager.hasEnrolledFingerprints() + } + + private fun getOrCreatePoolSync(): Executor = + pool ?: Executors.newSingleThreadExecutor().also { pool = it } + + private fun shouldPrompt(context: Context): Boolean { + return true + } + + fun authenticate(activity: FragmentActivity): BiometricDeferred { + val deferred: BiometricDeferred = CompletableDeferred() + if (!shouldPrompt(activity)) { + deferred.complete(null) + return deferred + } + val info = BiometricPrompt.PromptInfo.Builder() + .setTitle(activity.string(R.string.biometrics_prompt_title)) + .setNegativeButtonText(activity.string(R.string.kau_cancel)) + .build() + BiometricPrompt(activity, executor, Callback(activity, deferred)).authenticate(info) + return deferred + } + + private class Callback(val activity: FragmentActivity, val deferred: BiometricDeferred) : + BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + deferred.cancel() + activity.finish() + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + deferred.complete(result.cryptoObject) + } + + override fun onAuthenticationFailed() { + deferred.cancel() + activity.finish() + } + } + + /** + * For completeness we provide a shutdown function. + * In practice, we initialize the executor only when it is first used, + * and keep it alive throughout the app lifecycle, as it will be used an arbitrary number of times, + * with unknown frequency + */ + @Synchronized + fun shutdown() { + pool?.shutdown() + pool = null + } +}
\ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index db9b30d5..5eb1c9e7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -62,6 +62,8 @@ <string name="no_new_notifications">No new notifications found</string> + <!--Biometrics--> + <string name="biometrics_prompt_title">Authenticate Frost</string> <string name="today">Today</string> <string name="yesterday">Yesterday</string> <!-- @@ -73,5 +75,4 @@ The first element is the day, and the second element is the time --> <string name="time_template">%1s at %2s</string> - </resources> diff --git a/app/src/main/res/values/strings_pref_behaviour.xml b/app/src/main/res/values/strings_pref_behaviour.xml index 77c35c1c..09d4fc63 100644 --- a/app/src/main/res/values/strings_pref_behaviour.xml +++ b/app/src/main/res/values/strings_pref_behaviour.xml @@ -21,6 +21,8 @@ <string name="autoplay_settings_desc">Open Facebook\'s auto play settings. Note that it must be disabled for PIP to work.</string> <string name="exit_confirmation">Exit Confirmation</string> <string name="exit_confirmation_desc">Show confirmation dialog before exiting the app</string> + <string name="enable_biometrics">Enable biometrics</string> + <string name="enable_biometrics_desc">Require biometric authentication after inactivity</string> <string name="analytics">Analytics</string> <string name="analytics_desc">Enable anonymous analytics and bug reports to help improve the app. No personal information is ever exposed.</string> |