Skip to content

Commit

Permalink
Revamp permission denied dialog and add link to sign up form (#2492)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
shobhitagarwal1612 and gino-m authored Jun 17, 2024
1 parent c22c06e commit cdd50f1
Show file tree
Hide file tree
Showing 11 changed files with 223 additions and 167 deletions.
1 change: 1 addition & 0 deletions ground/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
44 changes: 44 additions & 0 deletions ground/src/main/java/com/google/android/ground/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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()
}
13 changes: 10 additions & 3 deletions ground/src/main/java/com/google/android/ground/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -55,6 +58,9 @@ constructor(
authenticationManager: AuthenticationManager,
) : AbstractViewModel() {

private val _uiState: MutableStateFlow<MainUiState?> = MutableStateFlow(null)
var uiState: StateFlow<MainUiState?> = _uiState.asStateFlow()

/** The window insets determined by the activity. */
val windowInsets: MutableLiveData<WindowInsetsCompat> = MutableLiveData()

Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = {}) }
}

This file was deleted.

30 changes: 30 additions & 0 deletions ground/src/main/res/drawable/baseline_warning_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>

<!--
~ 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.
-->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">

<path
android:fillColor="@android:color/white"
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z" />

</vector>
Loading

0 comments on commit cdd50f1

Please sign in to comment.