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