Loading core/java/android/credentials/ui/CancelUiRequest.java +27 −1 Original line number Diff line number Diff line Loading @@ -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 Loading core/java/android/credentials/ui/IntentFactory.java +4 −2 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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; } Loading packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +10 −3 Original line number Diff line number Diff line Loading @@ -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() Loading @@ -128,6 +133,7 @@ class CredentialManagerRepo( isPasskeyFirstUse )!!, getCredentialUiState = null, cancelRequestState = cancelUiRequestState ) } RequestInfo.TYPE_GET -> { Loading @@ -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}") Loading Loading @@ -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 ) } } Loading packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt +71 −20 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading @@ -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() 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) Loading @@ -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) Loading @@ -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, Loading Loading @@ -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, ) } } packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +9 −0 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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. */ Loading Loading
core/java/android/credentials/ui/CancelUiRequest.java +27 −1 Original line number Diff line number Diff line Loading @@ -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 Loading
core/java/android/credentials/ui/IntentFactory.java +4 −2 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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; } Loading
packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +10 −3 Original line number Diff line number Diff line Loading @@ -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() Loading @@ -128,6 +133,7 @@ class CredentialManagerRepo( isPasskeyFirstUse )!!, getCredentialUiState = null, cancelRequestState = cancelUiRequestState ) } RequestInfo.TYPE_GET -> { Loading @@ -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}") Loading Loading @@ -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 ) } } Loading
packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt +71 −20 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading @@ -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() 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) Loading @@ -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) Loading @@ -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, Loading Loading @@ -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, ) } }
packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +9 −0 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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. */ Loading