| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * 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 |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0N |
| * |
| * 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.android.credentialmanager |
| |
| import android.content.Intent |
| import android.credentials.ui.BaseDialogResult |
| import android.credentials.ui.RequestInfo |
| import android.net.Uri |
| import android.os.Bundle |
| import android.os.ResultReceiver |
| import android.util.Log |
| import androidx.activity.ComponentActivity |
| import androidx.activity.compose.rememberLauncherForActivityResult |
| import androidx.activity.compose.setContent |
| 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 |
| import com.android.credentialmanager.getflow.hasContentToDisplay |
| import com.android.credentialmanager.ui.theme.PlatformTheme |
| |
| @ExperimentalMaterialApi |
| class CredentialSelectorActivity : ComponentActivity() { |
| override fun onCreate(savedInstanceState: Bundle?) { |
| super.onCreate(savedInstanceState) |
| Log.d(Constants.LOG_TAG, "Creating new CredentialSelectorActivity") |
| try { |
| val (isCancellationRequest, shouldShowCancellationUi, _) = |
| maybeCancelUIUponRequest(intent) |
| if (isCancellationRequest && !shouldShowCancellationUi) { |
| return |
| } |
| val userConfigRepo = UserConfigRepo(this) |
| val credManRepo = CredentialManagerRepo(this, intent, userConfigRepo) |
| setContent { |
| PlatformTheme { |
| CredentialManagerBottomSheet( |
| credManRepo, |
| userConfigRepo |
| ) |
| } |
| } |
| } catch (e: Exception) { |
| onInitializationError(e, intent) |
| } |
| } |
| |
| override fun onNewIntent(intent: Intent) { |
| super.onNewIntent(intent) |
| setIntent(intent) |
| try { |
| val viewModel: CredentialSelectorViewModel by viewModels() |
| 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) |
| viewModel.onNewCredentialManagerRepo(credManRepo) |
| } |
| } catch (e: Exception) { |
| onInitializationError(e, intent) |
| } |
| } |
| |
| /** |
| * 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(true, 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 |
| private fun CredentialManagerBottomSheet( |
| credManRepo: CredentialManagerRepo, |
| userConfigRepo: UserConfigRepo, |
| ) { |
| val viewModel: CredentialSelectorViewModel = viewModel { |
| CredentialSelectorViewModel(credManRepo, userConfigRepo) |
| } |
| val launcher = rememberLauncherForActivityResult( |
| StartBalIntentSenderForResultContract() |
| ) { |
| viewModel.onProviderActivityResult(ProviderActivityResult(it.resultCode, it.data)) |
| } |
| LaunchedEffect(viewModel.uiState.dialogState) { |
| handleDialogState(viewModel.uiState.dialogState) |
| } |
| |
| val createCredentialUiState = viewModel.uiState.createCredentialUiState |
| val getCredentialUiState = viewModel.uiState.getCredentialUiState |
| 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, |
| providerActivityLauncher = launcher |
| ) |
| } else if (getCredentialUiState != null && hasContentToDisplay(getCredentialUiState)) { |
| GetCredentialScreen( |
| viewModel = viewModel, |
| getCredentialUiState = getCredentialUiState, |
| providerActivityLauncher = launcher |
| ) |
| } else { |
| Log.d(Constants.LOG_TAG, "UI wasn't able to render neither get nor create flow") |
| reportInstantiationErrorAndFinishActivity(credManRepo) |
| } |
| } |
| |
| private fun reportInstantiationErrorAndFinishActivity(credManRepo: CredentialManagerRepo) { |
| Log.w(Constants.LOG_TAG, "Finishing the activity due to instantiation failure.") |
| credManRepo.onParsingFailureCancel() |
| this@CredentialSelectorActivity.finish() |
| } |
| |
| private fun handleDialogState(dialogState: DialogState) { |
| if (dialogState == DialogState.COMPLETE) { |
| Log.d(Constants.LOG_TAG, "Received signal to finish the activity.") |
| this@CredentialSelectorActivity.finish() |
| } else if (dialogState == DialogState.CANCELED_FOR_SETTINGS) { |
| Log.d(Constants.LOG_TAG, "Received signal to finish the activity and launch settings.") |
| val settingsIntent = Intent(ACTION_CREDENTIAL_PROVIDER) |
| settingsIntent.data = Uri.parse("package:" + this.getPackageName()) |
| this@CredentialSelectorActivity.startActivity(settingsIntent) |
| this@CredentialSelectorActivity.finish() |
| } |
| } |
| |
| private fun onInitializationError(e: Exception, intent: Intent) { |
| Log.e(Constants.LOG_TAG, "Failed to show the credential selector; closing the activity", e) |
| val resultReceiver = intent.getParcelableExtra( |
| android.credentials.ui.Constants.EXTRA_RESULT_RECEIVER, |
| ResultReceiver::class.java |
| ) |
| val requestInfo = intent.extras?.getParcelable( |
| RequestInfo.EXTRA_REQUEST_INFO, |
| RequestInfo::class.java |
| ) |
| CredentialManagerRepo.sendCancellationCode( |
| BaseDialogResult.RESULT_CODE_DATA_PARSING_FAILURE, |
| requestInfo?.token, resultReceiver |
| ) |
| this.finish() |
| } |
| |
| @Composable |
| private fun UiCancellationScreen(appDisplayName: String) { |
| Snackbar( |
| contentText = stringResource(R.string.request_cancelled_by, appDisplayName), |
| onDismiss = { this@CredentialSelectorActivity.finish() }, |
| dismissOnTimeout = true, |
| ) |
| } |
| |
| companion object { |
| const val ACTION_CREDENTIAL_PROVIDER = "android.settings.CREDENTIAL_PROVIDER" |
| } |
| } |