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.ui.BaseDialogResult 22 import android.credentials.ui.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.credentialmanager.common.Constants 38 import com.android.credentialmanager.common.DialogState 39 import com.android.credentialmanager.common.ProviderActivityResult 40 import com.android.credentialmanager.common.StartBalIntentSenderForResultContract 41 import com.android.credentialmanager.common.ui.Snackbar 42 import com.android.credentialmanager.createflow.CreateCredentialScreen 43 import com.android.credentialmanager.createflow.hasContentToDisplay 44 import com.android.credentialmanager.getflow.GetCredentialScreen 45 import com.android.credentialmanager.getflow.hasContentToDisplay 46 import com.android.credentialmanager.ui.theme.PlatformTheme 47 48 @ExperimentalMaterialApi 49 class CredentialSelectorActivity : ComponentActivity() { 50 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 userConfigRepo = UserConfigRepo(this) 65 val credManRepo = CredentialManagerRepo( 66 this, intent, userConfigRepo, isNewActivity = true) 67 68 val backPressedCallback = object : OnBackPressedCallback( 69 true // default to enabled 70 ) { 71 override fun handleOnBackPressed() { 72 credManRepo.onUserCancel() 73 Log.d(Constants.LOG_TAG, "Activity back triggered: finish the activity.") 74 this@CredentialSelectorActivity.finish() 75 } 76 } 77 onBackPressedDispatcher.addCallback(this, backPressedCallback) 78 79 setContent { 80 PlatformTheme { 81 CredentialManagerBottomSheet( 82 credManRepo, 83 userConfigRepo 84 ) 85 } 86 } 87 } catch (e: Exception) { 88 onInitializationError(e, intent) 89 } 90 } 91 92 override fun onNewIntent(intent: Intent) { 93 super.onNewIntent(intent) 94 setIntent(intent) 95 try { 96 val viewModel: CredentialSelectorViewModel by viewModels() 97 val (isCancellationRequest, shouldShowCancellationUi, appDisplayName) = 98 maybeCancelUIUponRequest(intent, viewModel) 99 if (isCancellationRequest) { 100 if (shouldShowCancellationUi) { 101 viewModel.onCancellationUiRequested(appDisplayName) 102 } else { 103 return 104 } 105 } else { 106 val userConfigRepo = UserConfigRepo(this) 107 val credManRepo = CredentialManagerRepo( 108 this, intent, userConfigRepo, isNewActivity = false) 109 viewModel.onNewCredentialManagerRepo(credManRepo) 110 } 111 } catch (e: Exception) { 112 onInitializationError(e, intent) 113 } 114 } 115 116 /** 117 * Cancels the UI activity if requested by the backend. Different from the other finishing 118 * helpers, this does not report anything back to the Credential Manager service backend. 119 * 120 * Can potentially show a transient snackbar before finishing, if the request specifies so. 121 * 122 * Returns <isCancellationRequest, shouldShowCancellationUi, appDisplayName>. 123 */ 124 private fun maybeCancelUIUponRequest( 125 intent: Intent, 126 viewModel: CredentialSelectorViewModel? = null 127 ): Triple<Boolean, Boolean, String?> { 128 val cancelUiRequest = CredentialManagerRepo.getCancelUiRequest(intent) 129 ?: return Triple(false, false, null) 130 if (viewModel != null && !viewModel.shouldCancelCurrentUi(cancelUiRequest.token)) { 131 // Cancellation was for a different request, don't cancel the current UI. 132 return Triple(true, false, null) 133 } 134 val shouldShowCancellationUi = cancelUiRequest.shouldShowCancellationUi() 135 Log.d( 136 Constants.LOG_TAG, "Received UI cancellation intent. Should show cancellation" + 137 " ui = $shouldShowCancellationUi") 138 val appDisplayName = getAppLabel(packageManager, cancelUiRequest.appPackageName) 139 if (!shouldShowCancellationUi) { 140 this.finish() 141 } 142 return Triple(true, shouldShowCancellationUi, appDisplayName) 143 } 144 145 146 @ExperimentalMaterialApi 147 @Composable 148 private fun CredentialManagerBottomSheet( 149 credManRepo: CredentialManagerRepo, 150 userConfigRepo: UserConfigRepo, 151 ) { 152 val viewModel: CredentialSelectorViewModel = viewModel { 153 CredentialSelectorViewModel(credManRepo, userConfigRepo) 154 } 155 val launcher = rememberLauncherForActivityResult( 156 StartBalIntentSenderForResultContract() 157 ) { 158 viewModel.onProviderActivityResult(ProviderActivityResult(it.resultCode, it.data)) 159 } 160 LaunchedEffect(viewModel.uiState.dialogState) { 161 handleDialogState(viewModel.uiState.dialogState) 162 } 163 164 val createCredentialUiState = viewModel.uiState.createCredentialUiState 165 val getCredentialUiState = viewModel.uiState.getCredentialUiState 166 val cancelRequestState = viewModel.uiState.cancelRequestState 167 if (cancelRequestState != null) { 168 if (cancelRequestState.appDisplayName == null) { 169 Log.d(Constants.LOG_TAG, "Received UI cancel request with an invalid package name.") 170 this.finish() 171 return 172 } else { 173 UiCancellationScreen(cancelRequestState.appDisplayName) 174 } 175 } else if ( 176 createCredentialUiState != null && hasContentToDisplay(createCredentialUiState)) { 177 CreateCredentialScreen( 178 viewModel = viewModel, 179 createCredentialUiState = createCredentialUiState, 180 providerActivityLauncher = launcher 181 ) 182 } else if (getCredentialUiState != null && hasContentToDisplay(getCredentialUiState)) { 183 GetCredentialScreen( 184 viewModel = viewModel, 185 getCredentialUiState = getCredentialUiState, 186 providerActivityLauncher = launcher 187 ) 188 } else { 189 Log.d(Constants.LOG_TAG, "UI wasn't able to render neither get nor create flow") 190 reportInstantiationErrorAndFinishActivity(credManRepo) 191 } 192 } 193 194 private fun reportInstantiationErrorAndFinishActivity(credManRepo: CredentialManagerRepo) { 195 Log.w(Constants.LOG_TAG, "Finishing the activity due to instantiation failure.") 196 credManRepo.onParsingFailureCancel() 197 this@CredentialSelectorActivity.finish() 198 } 199 200 private fun handleDialogState(dialogState: DialogState) { 201 if (dialogState == DialogState.COMPLETE) { 202 Log.d(Constants.LOG_TAG, "Received signal to finish the activity.") 203 this@CredentialSelectorActivity.finish() 204 } else if (dialogState == DialogState.CANCELED_FOR_SETTINGS) { 205 Log.d(Constants.LOG_TAG, "Received signal to finish the activity and launch settings.") 206 val settingsIntent = Intent(ACTION_CREDENTIAL_PROVIDER) 207 settingsIntent.data = Uri.parse("package:" + this.getPackageName()) 208 this@CredentialSelectorActivity.startActivity(settingsIntent) 209 this@CredentialSelectorActivity.finish() 210 } 211 } 212 213 private fun onInitializationError(e: Exception, intent: Intent) { 214 Log.e(Constants.LOG_TAG, "Failed to show the credential selector; closing the activity", e) 215 val resultReceiver = intent.getParcelableExtra( 216 android.credentials.ui.Constants.EXTRA_RESULT_RECEIVER, 217 ResultReceiver::class.java 218 ) 219 val requestInfo = intent.extras?.getParcelable( 220 RequestInfo.EXTRA_REQUEST_INFO, 221 RequestInfo::class.java 222 ) 223 CredentialManagerRepo.sendCancellationCode( 224 BaseDialogResult.RESULT_CODE_DATA_PARSING_FAILURE, 225 requestInfo?.token, resultReceiver 226 ) 227 this.finish() 228 } 229 230 @Composable 231 private fun UiCancellationScreen(appDisplayName: String) { 232 Snackbar( 233 contentText = stringResource(R.string.request_cancelled_by, appDisplayName), 234 onDismiss = { this@CredentialSelectorActivity.finish() }, 235 dismissOnTimeout = true, 236 ) 237 } 238 239 companion object { 240 const val ACTION_CREDENTIAL_PROVIDER = "android.settings.CREDENTIAL_PROVIDER" 241 } 242 } 243