From cdd50f1b6c9d8664340b51144f66b591438ce9a1 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Tue, 18 Jun 2024 00:08:24 +0530 Subject: [PATCH] Revamp permission denied dialog and add link to sign up form (#2492) * Add icon * Add string messages and signup form link * Introduce a new UiState for MainActivity since the compose dialog can't be launched from a view model or navigator * Create a compose based dialog for permission denied errors * Integrate the dialog with activity and view model using ui state architecture * Remove obsolete dialog fragment and view model * Fix detect warnings * Update unit test * Move the signup form link to BuildConfig --------- Co-authored-by: Gino Miceli <228050+gino-m@users.noreply.github.com> --- ground/build.gradle | 1 + .../com/google/android/ground/MainActivity.kt | 44 +++++++ ...eniedDialogViewModel.kt => MainUiState.kt} | 22 +--- .../google/android/ground/MainViewModel.kt | 13 +- .../android/ground/PermissionDeniedDialog.kt | 122 ++++++++++++++++++ .../common/PermissionDeniedDialogFragment.kt | 39 ------ .../main/res/drawable/baseline_warning_24.xml | 30 +++++ .../res/layout/permission_denied_dialog.xml | 92 ------------- ground/src/main/res/navigation/nav_graph.xml | 10 +- ground/src/main/res/values/strings.xml | 5 +- .../android/ground/MainViewModelTest.kt | 12 +- 11 files changed, 223 insertions(+), 167 deletions(-) rename ground/src/main/java/com/google/android/ground/{ui/common/PermissionDeniedDialogViewModel.kt => MainUiState.kt} (53%) create mode 100644 ground/src/main/java/com/google/android/ground/PermissionDeniedDialog.kt delete mode 100644 ground/src/main/java/com/google/android/ground/ui/common/PermissionDeniedDialogFragment.kt create mode 100644 ground/src/main/res/drawable/baseline_warning_24.xml delete mode 100644 ground/src/main/res/layout/permission_denied_dialog.xml diff --git a/ground/build.gradle b/ground/build.gradle index 606b83c11d..d3783d7492 100644 --- a/ground/build.gradle +++ b/ground/build.gradle @@ -86,6 +86,7 @@ android { buildConfigField "String", "EMULATOR_HOST", "\"10.0.2.2\"" buildConfigField "int", "FIRESTORE_EMULATOR_PORT", "8080" buildConfigField "int", "AUTH_EMULATOR_PORT", "9099" + buildConfigField "String", "SIGNUP_FORM_LINK", "\"\"" manifestPlaceholders.usesCleartextTraffic = true } diff --git a/ground/src/main/java/com/google/android/ground/MainActivity.kt b/ground/src/main/java/com/google/android/ground/MainActivity.kt index 41514542cc..6cbac7251b 100644 --- a/ground/src/main/java/com/google/android/ground/MainActivity.kt +++ b/ground/src/main/java/com/google/android/ground/MainActivity.kt @@ -18,7 +18,13 @@ package com.google.android.ground import android.app.AlertDialog import android.content.Intent import android.os.Bundle +import android.view.ViewGroup import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.ComposeView import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.NavHostFragment @@ -36,8 +42,10 @@ import com.google.android.ground.ui.common.NavigationRequest import com.google.android.ground.ui.common.Navigator import com.google.android.ground.ui.common.ViewModelFactory import com.google.android.ground.ui.common.modalSpinner +import com.google.android.ground.ui.theme.AppTheme import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch import timber.log.Timber @@ -84,6 +92,42 @@ class MainActivity : AbstractActivity() { viewModel.signInProgressDialogVisibility.observe(this) { visible: Boolean -> onSignInProgress(visible) } + + lifecycleScope.launch { + viewModel.uiState.filterNotNull().collect { updateUi(binding.root, it) } + } + } + + private fun updateUi(viewGroup: ViewGroup, uiState: MainUiState) { + when (uiState) { + MainUiState.onPermissionDenied -> showPermissionDeniedDialog(viewGroup) + } + } + + private fun showPermissionDeniedDialog(viewGroup: ViewGroup) { + viewGroup.addView( + ComposeView(this).apply { + setContent { + AppTheme { + var showDialog by remember { mutableStateOf(true) } + if (showDialog) { + PermissionDeniedDialog( + // TODO(#2402): Read url from Firestore config/properties/signUpUrl + BuildConfig.SIGNUP_FORM_LINK, + onSignOut = { + showDialog = false + userRepository.signOut() + }, + onCloseApp = { + showDialog = false + navigator.finishApp() + }, + ) + } + } + } + } + ) } override fun onWindowInsetChanged(insets: WindowInsetsCompat) { diff --git a/ground/src/main/java/com/google/android/ground/ui/common/PermissionDeniedDialogViewModel.kt b/ground/src/main/java/com/google/android/ground/MainUiState.kt similarity index 53% rename from ground/src/main/java/com/google/android/ground/ui/common/PermissionDeniedDialogViewModel.kt rename to ground/src/main/java/com/google/android/ground/MainUiState.kt index 17c15c6949..8752155d27 100644 --- a/ground/src/main/java/com/google/android/ground/ui/common/PermissionDeniedDialogViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/MainUiState.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,23 +14,11 @@ * limitations under the License. */ -package com.google.android.ground.ui.common +package com.google.android.ground -import com.google.android.ground.repository.UserRepository -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject +sealed class MainUiState { -@HiltViewModel -class PermissionDeniedDialogViewModel -@Inject -internal constructor(private val navigator: Navigator, private val userRepository: UserRepository) : - AbstractViewModel() { + // TODO(#2402): Move remaining ui states from view model - fun closeApp() { - navigator.finishApp() - } - - fun signOut() { - userRepository.signOut() - } + data object onPermissionDenied : MainUiState() } diff --git a/ground/src/main/java/com/google/android/ground/MainViewModel.kt b/ground/src/main/java/com/google/android/ground/MainViewModel.kt index bbd5c78f33..029bad4c98 100644 --- a/ground/src/main/java/com/google/android/ground/MainViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/MainViewModel.kt @@ -36,6 +36,9 @@ import com.google.android.ground.ui.surveyselector.SurveySelectorFragmentDirecti import com.google.android.ground.util.isPermissionDeniedException import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber @@ -55,6 +58,9 @@ constructor( authenticationManager: AuthenticationManager, ) : AbstractViewModel() { + private val _uiState: MutableStateFlow = MutableStateFlow(null) + var uiState: StateFlow = _uiState.asStateFlow() + /** The window insets determined by the activity. */ val windowInsets: MutableLiveData = MutableLiveData() @@ -87,10 +93,11 @@ constructor( ) } - private fun onUserSignInError(error: Throwable): NavDirections { + private suspend fun onUserSignInError(error: Throwable): NavDirections? { Timber.e(error, "Sign in failed") return if (error.isPermissionDeniedException()) { - SignInFragmentDirections.showPermissionDeniedDialogFragment() + _uiState.emit(MainUiState.onPermissionDenied) + null } else { // TODO(#1808): Display some error dialog to the user with a helpful user-readable messagez. onUserSignedOut() @@ -111,7 +118,7 @@ constructor( return SignInFragmentDirections.showSignInScreen() } - private suspend fun onUserSignedIn(): NavDirections = + private suspend fun onUserSignedIn(): NavDirections? = try { userRepository.saveUserDetails() val tos = termsOfServiceRepository.getTermsOfService() diff --git a/ground/src/main/java/com/google/android/ground/PermissionDeniedDialog.kt b/ground/src/main/java/com/google/android/ground/PermissionDeniedDialog.kt new file mode 100644 index 0000000000..ac82e3f399 --- /dev/null +++ b/ground/src/main/java/com/google/android/ground/PermissionDeniedDialog.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.ground + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.google.android.ground.ui.compose.HyperlinkText +import com.google.android.ground.ui.theme.AppTheme +import timber.log.Timber + +@Composable +fun PermissionDeniedDialog(signupLink: String, onSignOut: () -> Unit, onCloseApp: () -> Unit) { + AlertDialog( + onDismissRequest = {}, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(id = R.drawable.baseline_warning_24), + contentDescription = "Alert Icon", + tint = MaterialTheme.colorScheme.error, + ) + + // Add some space between icon and title + Spacer(modifier = Modifier.width(12.dp)) + + Text( + stringResource(id = R.string.permission_denied), + style = MaterialTheme.typography.titleLarge, + ) + } + }, + text = { + Column { + if (signupLink.isNotEmpty()) { + SignupLink(signupLink) + } else { + Text( + stringResource(R.string.admin_request_access), + style = MaterialTheme.typography.bodyMedium, + ) + } + + // Empty line + Text(text = "") + + Text( + stringResource(R.string.signout_warning), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + ) + } + }, + dismissButton = { + OutlinedButton(onClick = { onSignOut() }) { Text(stringResource(id = R.string.sign_out)) } + }, + confirmButton = { + Button(onClick = { onCloseApp() }) { Text(stringResource(id = R.string.close_app)) } + }, + ) +} + +@Composable +private fun SignupLink(signupLink: String) { + val uriHandler = LocalUriHandler.current + + HyperlinkText( + textStyle = MaterialTheme.typography.bodyMedium, + fullTextResId = R.string.signup_request_access, + linkTextColor = MaterialTheme.colorScheme.primary, + hyperLinks = + mapOf( + "sign_up_link" to + { + runCatching { uriHandler.openUri(signupLink) } + .onFailure { Timber.e(it, "Failed to open sign-up link") } + } + ), + ) +} + +@Preview +@Composable +fun PermissionDeniedDialogWithSignupLinkPreview() { + AppTheme { + PermissionDeniedDialog(signupLink = "www.google.com", onSignOut = {}, onCloseApp = {}) + } +} + +@Preview +@Composable +fun PermissionDeniedDialogWithoutSignupLinkPreview() { + AppTheme { PermissionDeniedDialog(signupLink = "", onSignOut = {}, onCloseApp = {}) } +} diff --git a/ground/src/main/java/com/google/android/ground/ui/common/PermissionDeniedDialogFragment.kt b/ground/src/main/java/com/google/android/ground/ui/common/PermissionDeniedDialogFragment.kt deleted file mode 100644 index 9b76070213..0000000000 --- a/ground/src/main/java/com/google/android/ground/ui/common/PermissionDeniedDialogFragment.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.ground.ui.common - -import android.app.AlertDialog -import android.app.Dialog -import android.os.Bundle -import androidx.hilt.navigation.fragment.hiltNavGraphViewModels -import com.google.android.ground.R -import com.google.android.ground.databinding.PermissionDeniedDialogBinding -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class PermissionDeniedDialogFragment : AbstractDialogFragment() { - - private val viewModel: PermissionDeniedDialogViewModel by hiltNavGraphViewModels(R.id.navGraph) - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - super.onCreateDialog(savedInstanceState) - val inflater = requireActivity().layoutInflater - val binding = PermissionDeniedDialogBinding.inflate(inflater) - binding.viewModel = viewModel - return AlertDialog.Builder(requireActivity()).setView(binding.root).create() - } -} diff --git a/ground/src/main/res/drawable/baseline_warning_24.xml b/ground/src/main/res/drawable/baseline_warning_24.xml new file mode 100644 index 0000000000..db8d751b08 --- /dev/null +++ b/ground/src/main/res/drawable/baseline_warning_24.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/ground/src/main/res/layout/permission_denied_dialog.xml b/ground/src/main/res/layout/permission_denied_dialog.xml deleted file mode 100644 index cf2a2886a2..0000000000 --- a/ground/src/main/res/layout/permission_denied_dialog.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - -