From 01f9c30522af7c698c9d3d73032ecf82b4f14438 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Mon, 16 Oct 2023 10:23:24 +0200 Subject: [PATCH] feat: unlock app with biometrics (WPB-4696) (#2321) --- app/build.gradle.kts | 1 + .../wire/android/GlobalObserversManager.kt | 20 ++++ .../android/biomitric/BiometricPromptUtils.kt | 95 +++++++++++++++++++ .../com/wire/android/ui/WireActivity.kt | 21 +++- .../appLock/AppUnlockWithBiometricsScreen.kt | 93 ++++++++++++++++++ .../AppUnlockWithBiometricsViewModel.kt | 32 +++++++ app/src/main/res/values/strings.xml | 5 + gradle/libs.versions.toml | 2 + 8 files changed, 264 insertions(+), 5 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/biomitric/BiometricPromptUtils.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsScreen.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5511fee85d..22005ee6cd 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 ad4183deff..8da0269e3a 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 0000000000..3ea1dbce96 --- /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 1f6299032a..26fe01a0ab 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 0000000000..8a18796e45 --- /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 0000000000..240051dc38 --- /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 4ff166492b..b122a9196b 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 4fb2ed8fe2..2d011498f4 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" }