summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/credentials/ui/CancelUiRequest.java28
-rw-r--r--core/java/android/credentials/ui/IntentFactory.java6
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt13
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt91
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt9
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt2
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/common/ui/SnackBar.kt20
7 files changed, 142 insertions, 27 deletions
diff --git a/core/java/android/credentials/ui/CancelUiRequest.java b/core/java/android/credentials/ui/CancelUiRequest.java
index 6bd9de481a79..d4c249e58c8a 100644
--- a/core/java/android/credentials/ui/CancelUiRequest.java
+++ b/core/java/android/credentials/ui/CancelUiRequest.java
@@ -40,24 +40,50 @@ public final class CancelUiRequest implements Parcelable {
@NonNull
private final IBinder mToken;
+ private final boolean mShouldShowCancellationUi;
+
+ @NonNull
+ private final String mAppPackageName;
+
/** Returns the request token matching the user request that should be cancelled. */
@NonNull
public IBinder getToken() {
return mToken;
}
- public CancelUiRequest(@NonNull IBinder token) {
+ @NonNull
+ public String getAppPackageName() {
+ return mAppPackageName;
+ }
+
+ /**
+ * Returns whether the UI should render a cancellation UI upon the request. If false, the UI
+ * will be silently cancelled.
+ */
+ public boolean shouldShowCancellationUi() {
+ return mShouldShowCancellationUi;
+ }
+
+ public CancelUiRequest(@NonNull IBinder token, boolean shouldShowCancellationUi,
+ @NonNull String appPackageName) {
mToken = token;
+ mShouldShowCancellationUi = shouldShowCancellationUi;
+ mAppPackageName = appPackageName;
}
private CancelUiRequest(@NonNull Parcel in) {
mToken = in.readStrongBinder();
AnnotationValidations.validate(NonNull.class, null, mToken);
+ mShouldShowCancellationUi = in.readBoolean();
+ mAppPackageName = in.readString8();
+ AnnotationValidations.validate(NonNull.class, null, mAppPackageName);
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeStrongBinder(mToken);
+ dest.writeBoolean(mShouldShowCancellationUi);
+ dest.writeString8(mAppPackageName);
}
@Override
diff --git a/core/java/android/credentials/ui/IntentFactory.java b/core/java/android/credentials/ui/IntentFactory.java
index dcfef56f86a4..5e8372d68eb2 100644
--- a/core/java/android/credentials/ui/IntentFactory.java
+++ b/core/java/android/credentials/ui/IntentFactory.java
@@ -72,7 +72,8 @@ public class IntentFactory {
* @hide
*/
@NonNull
- public static Intent createCancelUiIntent(@NonNull IBinder requestToken) {
+ public static Intent createCancelUiIntent(@NonNull IBinder requestToken,
+ boolean shouldShowCancellationUi, @NonNull String appPackageName) {
Intent intent = new Intent();
ComponentName componentName =
ComponentName.unflattenFromString(
@@ -81,7 +82,8 @@ public class IntentFactory {
com.android.internal.R.string
.config_credentialManagerDialogComponent));
intent.setComponent(componentName);
- intent.putExtra(CancelUiRequest.EXTRA_CANCEL_UI_REQUEST, new CancelUiRequest(requestToken));
+ intent.putExtra(CancelUiRequest.EXTRA_CANCEL_UI_REQUEST,
+ new CancelUiRequest(requestToken, shouldShowCancellationUi, appPackageName));
return intent;
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 28f9453a48a2..452455c9838c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -110,6 +110,11 @@ class CredentialManagerRepo(
ResultReceiver::class.java
)
+ val cancellationRequest = getCancelUiRequest(intent)
+ val cancelUiRequestState = cancellationRequest?.let {
+ CancelUiRequestState(getAppLabel(context.getPackageManager(), it.appPackageName))
+ }
+
initialUiState = when (requestInfo.type) {
RequestInfo.TYPE_CREATE -> {
val defaultProviderId = userConfigRepo.getDefaultProviderId()
@@ -128,6 +133,7 @@ class CredentialManagerRepo(
isPasskeyFirstUse
)!!,
getCredentialUiState = null,
+ cancelRequestState = cancelUiRequestState
)
}
RequestInfo.TYPE_GET -> {
@@ -142,6 +148,7 @@ class CredentialManagerRepo(
if (autoSelectEntry == null) ProviderActivityState.NOT_APPLICABLE
else ProviderActivityState.READY_TO_LAUNCH,
isAutoSelectFlow = autoSelectEntry != null,
+ cancelRequestState = cancelUiRequestState
)
}
else -> throw IllegalStateException("Unrecognized request type: ${requestInfo.type}")
@@ -238,12 +245,12 @@ class CredentialManagerRepo(
}
}
- /** Return the request token whose UI should be cancelled, or null otherwise. */
- fun getCancelUiRequestToken(intent: Intent): IBinder? {
+ /** Return the cancellation request if present. */
+ fun getCancelUiRequest(intent: Intent): CancelUiRequest? {
return intent.extras?.getParcelable(
CancelUiRequest.EXTRA_CANCEL_UI_REQUEST,
CancelUiRequest::class.java
- )?.token
+ )
}
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index 5d72424c8f8a..2efe1bee43cc 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -30,11 +30,13 @@ import androidx.activity.viewModels
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.credentialmanager.common.Constants
import com.android.credentialmanager.common.DialogState
import com.android.credentialmanager.common.ProviderActivityResult
import com.android.credentialmanager.common.StartBalIntentSenderForResultContract
+import com.android.credentialmanager.common.ui.Snackbar
import com.android.credentialmanager.createflow.CreateCredentialScreen
import com.android.credentialmanager.createflow.hasContentToDisplay
import com.android.credentialmanager.getflow.GetCredentialScreen
@@ -49,10 +51,9 @@ class CredentialSelectorActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
Log.d(Constants.LOG_TAG, "Creating new CredentialSelectorActivity")
try {
- if (CredentialManagerRepo.getCancelUiRequestToken(intent) != null) {
- Log.d(
- Constants.LOG_TAG, "Received UI cancellation intent; cancelling the activity.")
- this.finish()
+ val (isCancellationRequest, shouldShowCancellationUi, _) =
+ maybeCancelUIUponRequest(intent)
+ if (isCancellationRequest && !shouldShowCancellationUi) {
return
}
val userConfigRepo = UserConfigRepo(this)
@@ -75,14 +76,15 @@ class CredentialSelectorActivity : ComponentActivity() {
setIntent(intent)
Log.d(Constants.LOG_TAG, "Existing activity received new intent")
try {
- val cancelUiRequestToken = CredentialManagerRepo.getCancelUiRequestToken(intent)
val viewModel: CredentialSelectorViewModel by viewModels()
- if (cancelUiRequestToken != null &&
- viewModel.shouldCancelCurrentUi(cancelUiRequestToken)) {
- Log.d(
- Constants.LOG_TAG, "Received UI cancellation intent; cancelling the activity.")
- this.finish()
- return
+ val (isCancellationRequest, shouldShowCancellationUi, appDisplayName) =
+ maybeCancelUIUponRequest(intent, viewModel)
+ if (isCancellationRequest) {
+ if (shouldShowCancellationUi) {
+ viewModel.onCancellationUiRequested(appDisplayName)
+ } else {
+ return
+ }
} else {
val userConfigRepo = UserConfigRepo(this)
val credManRepo = CredentialManagerRepo(this, intent, userConfigRepo)
@@ -93,11 +95,41 @@ class CredentialSelectorActivity : ComponentActivity() {
}
}
+ /**
+ * Cancels the UI activity if requested by the backend. Different from the other finishing
+ * helpers, this does not report anything back to the Credential Manager service backend.
+ *
+ * Can potentially show a transient snackbar before finishing, if the request specifies so.
+ *
+ * Returns <isCancellationRequest, shouldShowCancellationUi, appDisplayName>.
+ */
+ private fun maybeCancelUIUponRequest(
+ intent: Intent,
+ viewModel: CredentialSelectorViewModel? = null
+ ): Triple<Boolean, Boolean, String?> {
+ val cancelUiRequest = CredentialManagerRepo.getCancelUiRequest(intent)
+ ?: return Triple(false, false, null)
+ if (viewModel != null && !viewModel.shouldCancelCurrentUi(cancelUiRequest.token)) {
+ // Cancellation was for a different request, don't cancel the current UI.
+ return Triple(false, false, null)
+ }
+ val shouldShowCancellationUi = cancelUiRequest.shouldShowCancellationUi()
+ Log.d(
+ Constants.LOG_TAG, "Received UI cancellation intent. Should show cancellation" +
+ " ui = $shouldShowCancellationUi")
+ val appDisplayName = getAppLabel(packageManager, cancelUiRequest.appPackageName)
+ if (!shouldShowCancellationUi) {
+ this.finish()
+ }
+ return Triple(true, shouldShowCancellationUi, appDisplayName)
+ }
+
+
@ExperimentalMaterialApi
@Composable
- fun CredentialManagerBottomSheet(
+ private fun CredentialManagerBottomSheet(
credManRepo: CredentialManagerRepo,
- userConfigRepo: UserConfigRepo
+ userConfigRepo: UserConfigRepo,
) {
val viewModel: CredentialSelectorViewModel = viewModel {
CredentialSelectorViewModel(credManRepo, userConfigRepo)
@@ -113,7 +145,17 @@ class CredentialSelectorActivity : ComponentActivity() {
val createCredentialUiState = viewModel.uiState.createCredentialUiState
val getCredentialUiState = viewModel.uiState.getCredentialUiState
- if (createCredentialUiState != null && hasContentToDisplay(createCredentialUiState)) {
+ val cancelRequestState = viewModel.uiState.cancelRequestState
+ if (cancelRequestState != null) {
+ if (cancelRequestState.appDisplayName == null) {
+ Log.d(Constants.LOG_TAG, "Received UI cancel request with an invalid package name.")
+ this.finish()
+ return
+ } else {
+ UiCancellationScreen(cancelRequestState.appDisplayName)
+ }
+ } else if (
+ createCredentialUiState != null && hasContentToDisplay(createCredentialUiState)) {
CreateCredentialScreen(
viewModel = viewModel,
createCredentialUiState = createCredentialUiState,
@@ -122,15 +164,15 @@ class CredentialSelectorActivity : ComponentActivity() {
} else if (getCredentialUiState != null && hasContentToDisplay(getCredentialUiState)) {
if (isFallbackScreen(getCredentialUiState)) {
GetGenericCredentialScreen(
- viewModel = viewModel,
- getCredentialUiState = getCredentialUiState,
- providerActivityLauncher = launcher
+ viewModel = viewModel,
+ getCredentialUiState = getCredentialUiState,
+ providerActivityLauncher = launcher
)
} else {
GetCredentialScreen(
- viewModel = viewModel,
- getCredentialUiState = getCredentialUiState,
- providerActivityLauncher = launcher
+ viewModel = viewModel,
+ getCredentialUiState = getCredentialUiState,
+ providerActivityLauncher = launcher
)
}
} else {
@@ -172,4 +214,13 @@ class CredentialSelectorActivity : ComponentActivity() {
)
this.finish()
}
+
+ @Composable
+ private fun UiCancellationScreen(appDisplayName: String) {
+ Snackbar(
+ contentText = stringResource(R.string.request_cancelled_by, appDisplayName),
+ onDismiss = { this@CredentialSelectorActivity.finish() },
+ dismissOnTimeout = true,
+ )
+ }
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
index e49e3f165cfc..7eb3bf46b493 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
@@ -51,6 +51,11 @@ data class UiState(
// True if the UI has one and only one auto selectable entry. Its provider activity will be
// launched immediately, and canceling it will cancel the whole UI flow.
val isAutoSelectFlow: Boolean = false,
+ val cancelRequestState: CancelUiRequestState?,
+)
+
+data class CancelUiRequestState(
+ val appDisplayName: String?,
)
class CredentialSelectorViewModel(
@@ -76,6 +81,10 @@ class CredentialSelectorViewModel(
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
}
+ fun onCancellationUiRequested(appDisplayName: String?) {
+ uiState = uiState.copy(cancelRequestState = CancelUiRequestState(appDisplayName))
+ }
+
/** Close the activity and don't report anything to the backend.
* Example use case is the no-auth-info snackbar where the activity should simply display the
* UI and then be dismissed. */
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 783cf3b47344..43da9807231b 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -64,7 +64,7 @@ import androidx.credentials.provider.RemoteEntry
import org.json.JSONObject
// TODO: remove all !! checks
-private fun getAppLabel(
+fun getAppLabel(
pm: PackageManager,
appPackageName: String
): String? {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SnackBar.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SnackBar.kt
index 514ff90be8d7..dfff3d694877 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SnackBar.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SnackBar.kt
@@ -30,20 +30,24 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.Color
+import androidx.compose.ui.platform.LocalAccessibilityManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.android.credentialmanager.R
import com.android.credentialmanager.common.material.Scrim
import com.android.credentialmanager.ui.theme.Shapes
+import kotlinx.coroutines.delay
@Composable
fun Snackbar(
contentText: String,
action: (@Composable () -> Unit)? = null,
onDismiss: () -> Unit,
+ dismissOnTimeout: Boolean = false,
) {
BoxWithConstraints {
Box(Modifier.fillMaxSize()) {
@@ -89,4 +93,20 @@ fun Snackbar(
}
}
}
+ val accessibilityManager = LocalAccessibilityManager.current
+ LaunchedEffect(true) {
+ if (dismissOnTimeout) {
+ // Same as SnackbarDuration.Short
+ val originalDuration = 4000L
+ val duration = if (accessibilityManager == null) originalDuration else
+ accessibilityManager.calculateRecommendedTimeoutMillis(
+ originalDuration,
+ containsIcons = true,
+ containsText = true,
+ containsControls = action != null,
+ )
+ delay(duration)
+ onDismiss()
+ }
+ }
} \ No newline at end of file