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