1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0N 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.credentialmanager 18 19 import android.app.Activity 20 import android.content.Intent 21 import android.credentials.selection.BaseDialogResult 22 import android.credentials.selection.RequestInfo 23 import android.net.Uri 24 import android.os.Bundle 25 import android.os.ResultReceiver 26 import android.util.Log 27 import androidx.activity.ComponentActivity 28 import androidx.activity.OnBackPressedCallback 29 import androidx.activity.compose.rememberLauncherForActivityResult 30 import androidx.activity.compose.setContent 31 import androidx.activity.viewModels 32 import androidx.compose.material.ExperimentalMaterialApi 33 import androidx.compose.runtime.Composable 34 import androidx.compose.runtime.LaunchedEffect 35 import androidx.compose.ui.res.stringResource 36 import androidx.lifecycle.viewmodel.compose.viewModel 37 import com.android.compose.theme.PlatformTheme 38 import com.android.credentialmanager.common.Constants 39 import com.android.credentialmanager.common.DialogState 40 import com.android.credentialmanager.common.ProviderActivityResult 41 import com.android.credentialmanager.common.StartBalIntentSenderForResultContract 42 import com.android.credentialmanager.common.ui.Snackbar 43 import com.android.credentialmanager.createflow.CreateCredentialScreen 44 import com.android.credentialmanager.createflow.hasContentToDisplay 45 import com.android.credentialmanager.getflow.GetCredentialScreen 46 import com.android.credentialmanager.getflow.hasContentToDisplay 47 48 @ExperimentalMaterialApi 49 class CredentialSelectorActivity : ComponentActivity() { onCreatenull50 override fun onCreate(savedInstanceState: Bundle?) { 51 super.onCreate(savedInstanceState) 52 Log.d(Constants.LOG_TAG, "Creating new CredentialSelectorActivity") 53 overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, 54 0, 0) 55 overrideActivityTransition(Activity.OVERRIDE_TRANSITION_CLOSE, 56 0, 0) 57 58 try { 59 val (isCancellationRequest, shouldShowCancellationUi, _) = 60 maybeCancelUIUponRequest(intent) 61 if (isCancellationRequest && !shouldShowCancellationUi) { 62 return 63 } 64 val credManRepo = CredentialManagerRepo(this, intent, isNewActivity = true) 65 66 val backPressedCallback = object : OnBackPressedCallback( 67 true // default to enabled 68 ) { 69 override fun handleOnBackPressed() { 70 credManRepo.onUserCancel() 71 Log.d(Constants.LOG_TAG, "Activity back triggered: finish the activity.") 72 this@CredentialSelectorActivity.finish() 73 } 74 } 75 onBackPressedDispatcher.addCallback(this, backPressedCallback) 76 77 setContent { 78 PlatformTheme { 79 CredentialManagerBottomSheet(credManRepo) 80 } 81 } 82 } catch (e: Exception) { 83 onInitializationError(e, intent) 84 } 85 } 86 onNewIntentnull87 override fun onNewIntent(intent: Intent) { 88 super.onNewIntent(intent) 89 setIntent(intent) 90 try { 91 val viewModel: CredentialSelectorViewModel by viewModels() 92 val (isCancellationRequest, shouldShowCancellationUi, appDisplayName) = 93 maybeCancelUIUponRequest(intent, viewModel) 94 if (isCancellationRequest) { 95 if (shouldShowCancellationUi) { 96 viewModel.onCancellationUiRequested(appDisplayName) 97 } else { 98 return 99 } 100 } else { 101 val credManRepo = CredentialManagerRepo(this, intent, isNewActivity = false) 102 viewModel.onNewCredentialManagerRepo(credManRepo) 103 } 104 } catch (e: Exception) { 105 onInitializationError(e, intent) 106 } 107 } 108 109 /** 110 * Cancels the UI activity if requested by the backend. Different from the other finishing 111 * helpers, this does not report anything back to the Credential Manager service backend. 112 * 113 * Can potentially show a transient snackbar before finishing, if the request specifies so. 114 * 115 * Returns <isCancellationRequest, shouldShowCancellationUi, appDisplayName>. 116 */ maybeCancelUIUponRequestnull117 private fun maybeCancelUIUponRequest( 118 intent: Intent, 119 viewModel: CredentialSelectorViewModel? = null 120 ): Triple<Boolean, Boolean, String?> { 121 val cancelUiRequest = CredentialManagerRepo.getCancelUiRequest(intent) 122 ?: return Triple(false, false, null) 123 if (viewModel != null && !viewModel.shouldCancelCurrentUi(cancelUiRequest.token)) { 124 // Cancellation was for a different request, don't cancel the current UI. 125 return Triple(true, false, null) 126 } 127 val shouldShowCancellationUi = cancelUiRequest.shouldShowCancellationExplanation() 128 viewModel?.onDeveloperCancellationReceivedForBiometricPrompt() 129 Log.d( 130 Constants.LOG_TAG, "Received UI cancellation intent. Should show cancellation" + 131 " ui = $shouldShowCancellationUi") 132 val appDisplayName = getAppLabel(packageManager, cancelUiRequest.packageName) 133 if (!shouldShowCancellationUi) { 134 this.finish() 135 } 136 return Triple(true, shouldShowCancellationUi, appDisplayName) 137 } 138 139 140 @ExperimentalMaterialApi 141 @Composable CredentialManagerBottomSheetnull142 private fun CredentialManagerBottomSheet( 143 credManRepo: CredentialManagerRepo, 144 ) { 145 val viewModel: CredentialSelectorViewModel = viewModel { 146 CredentialSelectorViewModel(credManRepo) 147 } 148 val launcher = rememberLauncherForActivityResult( 149 StartBalIntentSenderForResultContract() 150 ) { 151 viewModel.onProviderActivityResult(ProviderActivityResult(it.resultCode, it.data)) 152 } 153 LaunchedEffect(viewModel.uiState.dialogState) { 154 handleDialogState(viewModel.uiState.dialogState) 155 } 156 157 val createCredentialUiState = viewModel.uiState.createCredentialUiState 158 val getCredentialUiState = viewModel.uiState.getCredentialUiState 159 val cancelRequestState = viewModel.uiState.cancelRequestState 160 if (cancelRequestState != null) { 161 if (cancelRequestState.appDisplayName == null) { 162 Log.d(Constants.LOG_TAG, "Received UI cancel request with an invalid package name.") 163 this.finish() 164 return 165 } else { 166 UiCancellationScreen(cancelRequestState.appDisplayName) 167 } 168 } else if ( 169 createCredentialUiState != null && hasContentToDisplay(createCredentialUiState)) { 170 CreateCredentialScreen( 171 viewModel = viewModel, 172 createCredentialUiState = createCredentialUiState, 173 providerActivityLauncher = launcher 174 ) 175 } else if (getCredentialUiState != null && hasContentToDisplay(getCredentialUiState)) { 176 GetCredentialScreen( 177 viewModel = viewModel, 178 getCredentialUiState = getCredentialUiState, 179 providerActivityLauncher = launcher 180 ) 181 } else { 182 Log.d(Constants.LOG_TAG, "UI wasn't able to render neither get nor create flow") 183 reportInstantiationErrorAndFinishActivity(credManRepo) 184 } 185 } 186 reportInstantiationErrorAndFinishActivitynull187 private fun reportInstantiationErrorAndFinishActivity(credManRepo: CredentialManagerRepo) { 188 Log.w(Constants.LOG_TAG, "Finishing the activity due to instantiation failure.") 189 credManRepo.onParsingFailureCancel() 190 this@CredentialSelectorActivity.finish() 191 } 192 handleDialogStatenull193 private fun handleDialogState(dialogState: DialogState) { 194 if (dialogState == DialogState.COMPLETE) { 195 Log.d(Constants.LOG_TAG, "Received signal to finish the activity.") 196 this@CredentialSelectorActivity.finish() 197 } else if (dialogState == DialogState.CANCELED_FOR_SETTINGS) { 198 Log.d(Constants.LOG_TAG, "Received signal to finish the activity and launch settings.") 199 val settingsIntent = Intent(ACTION_CREDENTIAL_PROVIDER) 200 settingsIntent.data = Uri.parse("package:" + this.getPackageName()) 201 this@CredentialSelectorActivity.startActivity(settingsIntent) 202 this@CredentialSelectorActivity.finish() 203 } 204 } 205 onInitializationErrornull206 private fun onInitializationError(e: Exception, intent: Intent) { 207 Log.e(Constants.LOG_TAG, "Failed to show the credential selector; closing the activity", e) 208 val resultReceiver = intent.getParcelableExtra( 209 android.credentials.selection.Constants.EXTRA_RESULT_RECEIVER, 210 ResultReceiver::class.java 211 ) 212 val requestInfo = intent.extras?.getParcelable( 213 RequestInfo.EXTRA_REQUEST_INFO, 214 RequestInfo::class.java 215 ) 216 CredentialManagerRepo.sendCancellationCode( 217 BaseDialogResult.RESULT_CODE_DATA_PARSING_FAILURE, 218 requestInfo?.token, resultReceiver 219 ) 220 this.finish() 221 } 222 223 @Composable UiCancellationScreennull224 private fun UiCancellationScreen(appDisplayName: String) { 225 Snackbar( 226 contentText = stringResource(R.string.request_cancelled_by, appDisplayName), 227 onDismiss = { this@CredentialSelectorActivity.finish() }, 228 dismissOnTimeout = true, 229 ) 230 } 231 232 companion object { 233 const val ACTION_CREDENTIAL_PROVIDER = "android.settings.CREDENTIAL_PROVIDER" 234 } 235 } 236