aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/build.gradle2
-rw-r--r--app/src/main/AndroidManifest.xml3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/BiometricUtils.kt89
-rw-r--r--app/src/main/res/values/strings.xml3
-rw-r--r--app/src/main/res/values/strings_pref_behaviour.xml2
-rw-r--r--gradle.properties4
8 files changed, 106 insertions, 3 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 472669cc..4d1ec15e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -206,6 +206,8 @@ dependencies {
implementation "androidx.core:core-ktx:${KTX}"
+ implementation "androidx.biometric:biometric:${ANDX_BIOMETRIC}"
+
implementation "org.koin:koin-android:${KOIN}"
testImplementation "org.koin:koin-test:${KOIN}"
androidTestImplementation "org.koin:koin-test:${KOIN}"
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>
diff --git a/gradle.properties b/gradle.properties
index 86e6040a..fe5a0971 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -17,7 +17,7 @@ APP_ID=Frost
APP_GROUP=com.pitchedapps
KAU=4.0.0
-KOTLIN=1.3.21
+KOTLIN=1.3.30
# https://mvnrepository.com/artifact/com.android.tools.build/gradle?repo=google
ANDROID_GRADLE=3.3.2
@@ -25,6 +25,8 @@ ANDROID_GRADLE=3.3.2
# https://github.com/diffplug/spotless/blob/master/plugin-gradle/CHANGES.md
SPOTLESS=3.21.1
+ANDX_BIOMETRIC=1.0.0-alpha04
+
# https://github.com/bugsnag/bugsnag-android/releases
BUGSNAG=4.12.0
# https://github.com/bugsnag/bugsnag-android-gradle-plugin/releases