Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add enter app lock passcode UI and logic [WPB-4695] #2278

Merged
merged 7 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.wire.android.BuildConfig
Expand All @@ -52,6 +53,7 @@
private val IS_LOGGING_ENABLED = booleanPreferencesKey("is_logging_enabled")
private val IS_ENCRYPTED_PROTEUS_STORAGE_ENABLED = booleanPreferencesKey("is_encrypted_proteus_storage_enabled")
private val APP_LOCK_PASSCODE = stringPreferencesKey("app_lock_passcode")
private val APP_LOCK_TIMESTAMP = longPreferencesKey("app_lock_timestamp")
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = PREFERENCES_NAME)
private fun userMigrationStatusKey(userId: String): Preferences.Key<Int> = intPreferencesKey("user_migration_status_$userId")
private fun userDoubleTapToastStatusKey(userId: String): Preferences.Key<Boolean> =
Expand Down Expand Up @@ -158,10 +160,18 @@
suspend fun setAppLockPasscode(passcode: String) {
context.dataStore.edit {
try {
it[APP_LOCK_PASSCODE] = EncryptionManager.encrypt(APP_LOCK_PASSCODE.name, passcode)
val encrypted = EncryptionManager.encrypt(APP_LOCK_PASSCODE.name, passcode)
it[APP_LOCK_PASSCODE] = encrypted

Check warning on line 164 in app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt#L163-L164

Added lines #L163 - L164 were not covered by tests
} catch (e: Exception) {
it.remove(APP_LOCK_PASSCODE)
}
}
}

fun getAppLockTimestampFlow(): Flow<Long?> =
context.dataStore.data.map { it[APP_LOCK_TIMESTAMP] }

Check warning on line 172 in app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt#L172

Added line #L172 was not covered by tests

suspend fun setAppLockTimestamp(timestamp: Long) {
context.dataStore.edit { it[APP_LOCK_TIMESTAMP] = timestamp }
}

Check warning on line 176 in app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt#L176

Added line #L176 was not covered by tests
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
package com.wire.android.feature

import com.wire.android.datastore.GlobalDataStore
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton

@ViewModelScoped
@Singleton
class ObserveAppLockConfigUseCase @Inject constructor(
private val globalDataStore: GlobalDataStore,
) {
Expand Down
19 changes: 19 additions & 0 deletions app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
import com.wire.android.ui.common.topappbar.CommonTopAppBar
import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModel
import com.wire.android.ui.destinations.ConversationScreenDestination
import com.wire.android.ui.destinations.EnterLockCodeScreenDestination
import com.wire.android.ui.destinations.HomeScreenDestination
import com.wire.android.ui.destinations.ImportMediaScreenDestination
import com.wire.android.ui.destinations.IncomingCallScreenDestination
Expand All @@ -78,6 +79,7 @@
import com.wire.android.ui.destinations.WelcomeScreenDestination
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.snackbar.LocalSnackbarHostState
import com.wire.android.ui.theme.WireTheme
Expand All @@ -91,6 +93,8 @@
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onSubscription
Expand All @@ -108,6 +112,9 @@
@Inject
lateinit var proximitySensorManager: ProximitySensorManager

@Inject
lateinit var lockCodeTimeManager: LockCodeTimeManager

private val viewModel: WireActivityViewModel by viewModels()

private val featureFlagNotificationViewModel: FeatureFlagNotificationViewModel by viewModels()
Expand Down Expand Up @@ -179,6 +186,7 @@
setUpNavigation(navigator.navController, onComplete, scope)
isLoaded = true
handleScreenshotCensoring()
handleAppLock()

Check warning on line 189 in app/src/main/kotlin/com/wire/android/ui/WireActivity.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/WireActivity.kt#L189

Added line #L189 was not covered by tests
handleDialogs(navigator::navigate)
}
}
Expand Down Expand Up @@ -229,6 +237,17 @@
}
}

@Composable
private fun handleAppLock() {
LaunchedEffect(Unit) {
lockCodeTimeManager.shouldLock()
.filter { it }
.collectLatest {
navigationCommands.emit(NavigationCommand(EnterLockCodeScreenDestination))
}
}
}

Check warning on line 249 in app/src/main/kotlin/com/wire/android/ui/WireActivity.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/WireActivity.kt#L243-L249

Added lines #L243 - L249 were not covered by tests

@Composable
private fun handleDialogs(navigate: (NavigationCommand) -> Unit) {
featureFlagNotificationViewModel.loadInitialSync()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* 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.compose.foundation.ScrollState
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
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.navigation.Navigator
import com.wire.android.ui.common.button.WireButtonState
import com.wire.android.ui.common.button.WirePrimaryButton
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.rememberBottomBarElevationState
import com.wire.android.ui.common.scaffold.WireScaffold
import com.wire.android.ui.common.textfield.WirePasswordTextField
import com.wire.android.ui.common.textfield.WireTextFieldState
import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireDimensions
import java.util.Locale

@RootNavGraph
@Destination
@Composable
fun EnterLockCodeScreen(
viewModel: EnterLockScreenViewModel = hiltViewModel(),
navigator: Navigator
) {
EnterLockCodeScreenContent(
navigator = navigator,
state = viewModel.state,
scrollState = rememberScrollState(),
onPasswordChanged = viewModel::onPasswordChanged,
onContinue = viewModel::onContinue,
onBackPress = { navigator.finish() }
)
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun EnterLockCodeScreenContent(
navigator: Navigator,
state: EnterLockCodeViewState,
scrollState: ScrollState,
onPasswordChanged: (TextFieldValue) -> Unit,
onBackPress: () -> Unit,
onContinue: () -> Unit
) {
LaunchedEffect(state.done) {
if (state.done) {
navigator.navigateBack()
}
}
BackHandler {
onBackPress()
}

WireScaffold(topBar = {
WireCenterAlignedTopAppBar(
onNavigationPressed = onBackPress,
elevation = dimensions().spacing0x,
title = stringResource(id = R.string.settings_enter_lock_screen_title)
)
}) { internalPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(internalPadding)
) {
Column(
modifier = Modifier
.weight(weight = 1f, fill = true)
.verticalScroll(scrollState)
.padding(MaterialTheme.wireDimensions.spacing16x)
.semantics {
testTagsAsResourceId = true
}
) {
WirePasswordTextField(
value = state.password,
onValueChange = onPasswordChanged,
labelMandatoryIcon = true,
descriptionText = stringResource(R.string.create_account_details_password_description),
imeAction = ImeAction.Done,
modifier = Modifier
.testTag("password"),
state = when (state.error) {
EnterLockCodeError.InvalidValue -> WireTextFieldState.Error(
errorText = stringResource(R.string.settings_enter_lock_screen_wrong_passcode_label)
)
EnterLockCodeError.None -> WireTextFieldState.Default
},
autofill = false,
placeholderText = stringResource(R.string.settings_set_lock_screen_passcode_label),
labelText = stringResource(R.string.settings_set_lock_screen_passcode_label).uppercase(Locale.getDefault())
)
Spacer(modifier = Modifier.weight(1f))
}

Surface(
shadowElevation = scrollState.rememberBottomBarElevationState().value,
color = MaterialTheme.wireColorScheme.background,
modifier = Modifier.semantics {
testTagsAsResourceId = true
}
) {
Box(modifier = Modifier.padding(MaterialTheme.wireDimensions.spacing16x)) {
val enabled = state.password.text.isNotBlank() && state.isUnlockEnabled
ContinueButton(
enabled = enabled,
onContinue = onContinue
)
}
}
}
}
}

@Composable
private fun ContinueButton(
modifier: Modifier = Modifier.fillMaxWidth(),
enabled: Boolean,
onContinue: () -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
Column(modifier = modifier) {
WirePrimaryButton(
text = stringResource(R.string.settings_enter_lock_screen_unlock_button_label),
onClick = onContinue,
state = if (enabled) WireButtonState.Default else WireButtonState.Disabled,
interactionSource = interactionSource,
modifier = Modifier
.fillMaxWidth()
.testTag("continue_button")
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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.compose.ui.text.input.TextFieldValue

data class EnterLockCodeViewState(
val continueEnabled: Boolean = false,
val password: TextFieldValue = TextFieldValue(),
val isUnlockEnabled: Boolean = false,
val error: EnterLockCodeError = EnterLockCodeError.None,
val done: Boolean = false
)

Check warning on line 28 in app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockCodeViewState.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockCodeViewState.kt#L22-L28

Added lines #L22 - L28 were not covered by tests

sealed class EnterLockCodeError {
data object None : EnterLockCodeError()
data object InvalidValue : EnterLockCodeError()

Check warning on line 32 in app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockCodeViewState.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockCodeViewState.kt#L31-L32

Added lines #L31 - L32 were not covered by tests
}
Loading
Loading