diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5511fee85d8..22005ee6cd4 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -71,6 +71,7 @@ dependencies {
implementation(libs.androidx.dataStore)
implementation(libs.androidx.splashscreen)
implementation(libs.androidx.exifInterface)
+ implementation(libs.androidx.biometric)
implementation(libs.ktx.dateTime)
implementation(libs.material)
diff --git a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt
index ad4183defff..8da0269e3ae 100644
--- a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt
+++ b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt
@@ -1,3 +1,23 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ *
+ */
+
package com.wire.android
import com.wire.android.di.KaliumCoreLogic
diff --git a/app/src/main/kotlin/com/wire/android/biomitric/BiometricPromptUtils.kt b/app/src/main/kotlin/com/wire/android/biomitric/BiometricPromptUtils.kt
new file mode 100644
index 00000000000..3ea1dbce96e
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/biomitric/BiometricPromptUtils.kt
@@ -0,0 +1,95 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+
+package com.wire.android.biomitric
+
+import android.content.Context
+import androidx.appcompat.app.AppCompatActivity
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.ERROR_NEGATIVE_BUTTON
+import androidx.core.content.ContextCompat
+import com.wire.android.R
+import com.wire.android.appLogger
+
+private const val TAG = "BiometricPromptUtils"
+
+object BiometricPromptUtils {
+ fun createBiometricPrompt(
+ activity: AppCompatActivity,
+ onSuccess: () -> Unit,
+ onCancel: () -> Unit,
+ onRequestPasscode: () -> Unit
+ ): BiometricPrompt {
+ val executor = ContextCompat.getMainExecutor(activity)
+
+ val callback = object : BiometricPrompt.AuthenticationCallback() {
+
+ override fun onAuthenticationError(errorCode: Int, errorString: CharSequence) {
+ super.onAuthenticationError(errorCode, errorString)
+ appLogger.i("$TAG errorCode is $errorCode and errorString is: $errorString")
+ if (errorCode == ERROR_NEGATIVE_BUTTON) {
+ onRequestPasscode()
+ } else {
+ onCancel()
+ }
+ }
+
+ override fun onAuthenticationFailed() {
+ super.onAuthenticationFailed()
+ appLogger.i("$TAG User biometric rejected")
+ }
+
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ super.onAuthenticationSucceeded(result)
+ appLogger.i("$TAG User biometric accepted")
+ onSuccess()
+ }
+ }
+ return BiometricPrompt(activity, executor, callback)
+ }
+
+ fun createPromptInfo(context: Context): BiometricPrompt.PromptInfo =
+ BiometricPrompt.PromptInfo.Builder().apply {
+ setTitle(context.getString(R.string.biometrics_prompt_dialog_title))
+ setSubtitle(context.getString(R.string.biometrics_prompt_dialog_subtitle))
+ setConfirmationRequired(false)
+ setNegativeButtonText(context.getString(R.string.biometrics_use_passcode_button))
+ }.build()
+}
+
+fun AppCompatActivity.showBiometricPrompt(
+ onSuccess: () -> Unit,
+ onCancel: () -> Unit,
+ onRequestPasscode: () -> Unit
+) {
+ appLogger.i("$TAG showing biometrics dialog...")
+
+ val canAuthenticate = BiometricManager.from(this)
+ .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+ if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
+ val biometricPrompt = BiometricPromptUtils.createBiometricPrompt(
+ this,
+ onSuccess,
+ onCancel,
+ onRequestPasscode,
+ )
+ val promptInfo = BiometricPromptUtils.createPromptInfo(this)
+ biometricPrompt.authenticate(promptInfo)
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
index 1f6299032a8..26fe01a0ab5 100644
--- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
@@ -29,6 +29,7 @@ import androidx.activity.compose.ReportDrawnWhen
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
+import androidx.biometric.BiometricManager
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.SnackbarHostState
@@ -43,7 +44,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -63,8 +63,10 @@ import com.wire.android.navigation.NavigationGraph
import com.wire.android.navigation.navigateToItem
import com.wire.android.navigation.rememberNavigator
import com.wire.android.ui.calling.ProximitySensorManager
+import com.wire.android.ui.common.snackbar.LocalSnackbarHostState
import com.wire.android.ui.common.topappbar.CommonTopAppBar
import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModel
+import com.wire.android.ui.destinations.AppUnlockWithBiometricsScreenDestination
import com.wire.android.ui.destinations.ConversationScreenDestination
import com.wire.android.ui.destinations.EnterLockCodeScreenDestination
import com.wire.android.ui.destinations.HomeScreenDestination
@@ -81,7 +83,6 @@ import com.wire.android.ui.home.E2EIRequiredDialog
import com.wire.android.ui.home.E2EISnoozeDialog
import com.wire.android.ui.home.appLock.LockCodeTimeManager
import com.wire.android.ui.home.sync.FeatureFlagNotificationViewModel
-import com.wire.android.ui.common.snackbar.LocalSnackbarHostState
import com.wire.android.ui.theme.WireTheme
import com.wire.android.util.CurrentScreenManager
import com.wire.android.util.LocalSyncStateObserver
@@ -101,7 +102,6 @@ import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.launch
import javax.inject.Inject
-@OptIn(ExperimentalComposeUiApi::class)
@AndroidEntryPoint
@Suppress("TooManyFunctions")
class WireActivity : AppCompatActivity() {
@@ -128,7 +128,6 @@ class WireActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
proximitySensorManager.initialize()
lifecycle.addObserver(currentScreenManager)
-
WindowCompat.setDecorFitsSystemWindows(window, false)
viewModel.observePersistentConnectionStatus()
@@ -243,7 +242,19 @@ class WireActivity : AppCompatActivity() {
lockCodeTimeManager.isLocked()
.filter { it }
.collectLatest {
- navigationCommands.emit(NavigationCommand(EnterLockCodeScreenDestination))
+ val canAuthenticateWithBiometrics = BiometricManager
+ .from(this@WireActivity)
+ .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+
+ if (canAuthenticateWithBiometrics == BiometricManager.BIOMETRIC_SUCCESS) {
+ navigationCommands.emit(
+ NavigationCommand(AppUnlockWithBiometricsScreenDestination)
+ )
+ } else {
+ navigationCommands.emit(
+ NavigationCommand(EnterLockCodeScreenDestination)
+ )
+ }
}
}
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsScreen.kt
new file mode 100644
index 00000000000..8a18796e45a
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsScreen.kt
@@ -0,0 +1,93 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.ui.home.appLock
+
+import androidx.activity.compose.BackHandler
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootNavGraph
+import com.wire.android.R
+import com.wire.android.biomitric.showBiometricPrompt
+import com.wire.android.navigation.BackStackMode
+import com.wire.android.navigation.NavigationCommand
+import com.wire.android.navigation.Navigator
+import com.wire.android.ui.common.colorsScheme
+import com.wire.android.ui.common.dimensions
+import com.wire.android.ui.destinations.EnterLockCodeScreenDestination
+
+@RootNavGraph
+@Destination
+@Composable
+fun AppUnlockWithBiometricsScreen(
+ appUnlockWithBiometricsViewModel: AppUnlockWithBiometricsViewModel = hiltViewModel(),
+ navigator: Navigator,
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(colorsScheme().background)
+ ) {
+ Icon(
+ modifier = Modifier
+ .padding(top = dimensions().spacing80x)
+ .align(Alignment.TopCenter),
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_wire_logo),
+ tint = MaterialTheme.colorScheme.onBackground,
+ contentDescription = stringResource(id = R.string.content_description_welcome_wire_logo)
+ )
+
+ val activity = LocalContext.current
+ LaunchedEffect(Unit) {
+ (activity as AppCompatActivity).showBiometricPrompt(
+ onSuccess = {
+ appUnlockWithBiometricsViewModel.onAppUnlocked()
+ navigator.navigateBack()
+ },
+ onCancel = {
+ navigator.finish()
+ },
+ onRequestPasscode = {
+ navigator.navigate(
+ NavigationCommand(
+ EnterLockCodeScreenDestination(),
+ BackStackMode.CLEAR_WHOLE
+ )
+ )
+ }
+ )
+ }
+ }
+ BackHandler {
+ navigator.finish()
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsViewModel.kt
new file mode 100644
index 00000000000..240051dc381
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsViewModel.kt
@@ -0,0 +1,32 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.ui.home.appLock
+
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class AppUnlockWithBiometricsViewModel @Inject constructor(
+ private val lockCodeTimeManager: LockCodeTimeManager
+) : ViewModel() {
+
+ fun onAppUnlocked() {
+ lockCodeTimeManager.appUnlocked()
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4ff166492bb..b122a9196bd 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1217,4 +1217,9 @@
Password copied to clipboard
Use at least 8 characters, with one lowercase letter, one capital letter, a number, and a special character.
New password generated
+
+ Verify that it\'s you
+ To unlock Wire
+ Use passcode
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4fb2ed8fe22..2d011498f43 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -34,6 +34,7 @@ androidx-paging3Compose = "1.0.0-alpha18"
androidx-splashscreen = "1.0.1"
androidx-workManager = "2.8.1"
androidx-browser = "1.5.0"
+androidx-biometric = "1.1.0"
# Compose
compose = "1.6.0-alpha07"
@@ -150,6 +151,7 @@ androidx-dataStore = { module = "androidx.datastore:datastore-preferences", vers
androidx-exifInterface = { module = "androidx.exifinterface:exifinterface", version.ref = "androidx-exif" }
androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splashscreen" }
androidx-profile-installer = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }
+androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "androidx-biometric" }
# Dependency Injection
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }