Skip to content

Commit

Permalink
feat: unlock app with biometrics (WPB-4696) (#2321)
Browse files Browse the repository at this point in the history
  • Loading branch information
ohassine committed Oct 16, 2023
1 parent c94a9e0 commit 01f9c30
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 5 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions 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
Expand Down
@@ -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)
}
}
21 changes: 16 additions & 5 deletions app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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() {
Expand All @@ -128,7 +128,6 @@ class WireActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
proximitySensorManager.initialize()
lifecycle.addObserver(currentScreenManager)

WindowCompat.setDecorFitsSystemWindows(window, false)

viewModel.observePersistentConnectionStatus()
Expand Down Expand Up @@ -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)
)
}
}
}
}
Expand Down
@@ -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()
}
}
@@ -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()
}
}
5 changes: 5 additions & 0 deletions app/src/main/res/values/strings.xml
Expand Up @@ -1217,4 +1217,9 @@
<string name="conversation_options_create_password_protected_guest_link_password_copied">Password copied to clipboard</string>
<string name="conversation_options_create_password_protected_guest_link_password_description">Use at least 8 characters, with one lowercase letter, one capital letter, a number, and a special character.</string>
<string name="conversation_options_create_password_protected_guest_link_password_generated">New password generated</string>
<!-- biometrics-->
<string name="biometrics_prompt_dialog_title">Verify that it\'s you</string>
<string name="biometrics_prompt_dialog_subtitle">To unlock Wire</string>
<string name="biometrics_use_passcode_button">Use passcode</string>

</resources>
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Expand Up @@ -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"
Expand Down Expand Up @@ -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" }
Expand Down

0 comments on commit 01f9c30

Please sign in to comment.