Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bdb7f0b0 authored by Helen Qin's avatar Helen Qin Committed by Android (Google) Code Review
Browse files

Merge "[CredManUi] Support UI for app cancellation" into udc-dev

parents 8f21c8a4 e1ec1baf
Loading
Loading
Loading
Loading
+27 −1
Original line number Diff line number Diff line
@@ -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
+4 −2
Original line number Diff line number Diff line
@@ -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;
    }

+10 −3
Original line number Diff line number Diff line
@@ -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
            )
        }
    }

+71 −20
Original line number Diff line number Diff line
@@ -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()
            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,
@@ -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,
        )
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -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. */
Loading