diff --git a/core/java/android/credentials/ui/BaseDialogResult.java b/core/java/android/credentials/ui/BaseDialogResult.java index 5223314635a72..f0442de74e9eb 100644 --- a/core/java/android/credentials/ui/BaseDialogResult.java +++ b/core/java/android/credentials/ui/BaseDialogResult.java @@ -56,14 +56,14 @@ public class BaseDialogResult implements Parcelable { * The intent extra key for the {@code BaseDialogResult} object when the credential * selector activity finishes. */ - private static final String EXTRA_BASE_RESULT = - "android.credentials.ui.extra.BASE_RESULT"; + private static final String EXTRA_BASE_RESULT = "android.credentials.ui.extra.BASE_RESULT"; /** @hide **/ @IntDef(prefix = {"RESULT_CODE_"}, value = { RESULT_CODE_DIALOG_USER_CANCELED, RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS, RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION, + RESULT_CODE_DATA_PARSING_FAILURE, }) @Retention(RetentionPolicy.SOURCE) public @interface ResultCode {} @@ -80,6 +80,10 @@ public class BaseDialogResult implements Parcelable { * {@code resultData}. */ public static final int RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION = 2; + /** + * The UI was canceled because it failed to parse the incoming data. + */ + public static final int RESULT_CODE_DATA_PARSING_FAILURE = 3; @NonNull private final IBinder mRequestToken; diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 0761b64eb811d..a48cd2b164516 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -55,422 +55,457 @@ import com.android.credentialmanager.jetpack.provider.CredentialEntry // Consider repo per screen, similar to view model? class CredentialManagerRepo( - private val context: Context, - intent: Intent, + private val context: Context, + intent: Intent, ) { - val requestInfo: RequestInfo - private val providerEnabledList: List - private val providerDisabledList: List? - // TODO: require non-null. - val resultReceiver: ResultReceiver? + val requestInfo: RequestInfo + private val providerEnabledList: List + private val providerDisabledList: List? - init { - requestInfo = intent.extras?.getParcelable( - RequestInfo.EXTRA_REQUEST_INFO, - RequestInfo::class.java - ) ?: testCreatePasskeyRequestInfo() + // TODO: require non-null. + val resultReceiver: ResultReceiver? - providerEnabledList = when (requestInfo.type) { - RequestInfo.TYPE_CREATE -> - intent.extras?.getParcelableArrayList( - ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, - CreateCredentialProviderData::class.java - ) ?: testCreateCredentialEnabledProviderList() - RequestInfo.TYPE_GET -> - intent.extras?.getParcelableArrayList( - ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, - GetCredentialProviderData::class.java - ) ?: testGetCredentialProviderList() - else -> { - // TODO: fail gracefully - throw IllegalStateException("Unrecognized request type: ${requestInfo.type}") - } + init { + requestInfo = intent.extras?.getParcelable( + RequestInfo.EXTRA_REQUEST_INFO, + RequestInfo::class.java + ) ?: testCreatePasskeyRequestInfo() + + providerEnabledList = when (requestInfo.type) { + RequestInfo.TYPE_CREATE -> + intent.extras?.getParcelableArrayList( + ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, + CreateCredentialProviderData::class.java + ) ?: testCreateCredentialEnabledProviderList() + RequestInfo.TYPE_GET -> + intent.extras?.getParcelableArrayList( + ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, + GetCredentialProviderData::class.java + ) ?: testGetCredentialProviderList() + else -> { + // TODO: fail gracefully + throw IllegalStateException("Unrecognized request type: ${requestInfo.type}") + } + } + + providerDisabledList = + intent.extras?.getParcelableArrayList( + ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, + DisabledProviderData::class.java + ) ?: testDisabledProviderList() + + resultReceiver = intent.getParcelableExtra( + Constants.EXTRA_RESULT_RECEIVER, + ResultReceiver::class.java + ) } - providerDisabledList = - intent.extras?.getParcelableArrayList( - ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, - DisabledProviderData::class.java - ) ?: testDisabledProviderList() + // The dialog is canceled by the user. + fun onUserCancel() { + onCancel(BaseDialogResult.RESULT_CODE_DIALOG_USER_CANCELED) + } - resultReceiver = intent.getParcelableExtra( - Constants.EXTRA_RESULT_RECEIVER, - ResultReceiver::class.java - ) - } + // The dialog is canceled because we launched into settings. + fun onSettingLaunchCancel() { + onCancel(BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION) + } - // The dialog is canceled by the user. - fun onUserCancel() { - onCancel(BaseDialogResult.RESULT_CODE_DIALOG_USER_CANCELED) - } + fun onParsingFailureCancel() { + onCancel(BaseDialogResult.RESULT_CODE_DATA_PARSING_FAILURE) + } - // The dialog is canceled because we launched into settings. - fun onSettingLaunchCancel() { - onCancel(BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION) - } + fun onCancel(cancelCode: Int) { + val resultData = Bundle() + BaseDialogResult.addToBundle(BaseDialogResult(requestInfo.token), resultData) + resultReceiver?.send(cancelCode, resultData) + } - private fun onCancel(cancelCode: Int) { - val resultData = Bundle() - BaseDialogResult.addToBundle(BaseDialogResult(requestInfo.token), resultData) - resultReceiver?.send(cancelCode, resultData) - } + fun onOptionSelected( + providerId: String, + entryKey: String, + entrySubkey: String, + resultCode: Int? = null, + resultData: Intent? = null, + ) { + val userSelectionDialogResult = UserSelectionDialogResult( + requestInfo.token, + providerId, + entryKey, + entrySubkey, + if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null + ) + val resultData = Bundle() + UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultData) + resultReceiver?.send( + BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION, + resultData + ) + } - fun onOptionSelected( - providerId: String, - entryKey: String, - entrySubkey: String, - resultCode: Int? = null, - resultData: Intent? = null, - ) { - val userSelectionDialogResult = UserSelectionDialogResult( - requestInfo.token, - providerId, - entryKey, - entrySubkey, - if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null - ) - val resultData = Bundle() - UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultData) - resultReceiver?.send(BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION, resultData) - } + fun getCredentialInitialUiState(): GetCredentialUiState? { + val providerEnabledList = GetFlowUtils.toProviderList( + // TODO: handle runtime cast error + providerEnabledList as List, context + ) + val requestDisplayInfo = GetFlowUtils.toRequestDisplayInfo(requestInfo, context) + return GetCredentialUiState( + providerEnabledList, + requestDisplayInfo ?: return null, + ) + } - fun getCredentialInitialUiState(): GetCredentialUiState { - val providerEnabledList = GetFlowUtils.toProviderList( - // TODO: handle runtime cast error - providerEnabledList as List, context) - val requestDisplayInfo = GetFlowUtils.toRequestDisplayInfo(requestInfo, context) - return GetCredentialUiState( - providerEnabledList, - requestDisplayInfo, - ) - } + fun getCreateProviderEnableListInitialUiState(): List { + val providerEnabledList = CreateFlowUtils.toEnabledProviderList( + // Handle runtime cast error + providerEnabledList as List, context + ) + return providerEnabledList + } - fun getCreateProviderEnableListInitialUiState(): List { - val providerEnabledList = CreateFlowUtils.toEnabledProviderList( - // Handle runtime cast error - providerEnabledList as List, context) - return providerEnabledList - } + fun getCreateProviderDisableListInitialUiState(): List { + return CreateFlowUtils.toDisabledProviderList( + // Handle runtime cast error + providerDisabledList, context + ) + } - fun getCreateProviderDisableListInitialUiState(): List? { - return CreateFlowUtils.toDisabledProviderList( - // Handle runtime cast error - providerDisabledList, context) - } + fun getCreateRequestDisplayInfoInitialUiState(): RequestDisplayInfo? { + return CreateFlowUtils.toRequestDisplayInfo(requestInfo, context) + } - fun getCreateRequestDisplayInfoInitialUiState(): RequestDisplayInfo { - return CreateFlowUtils.toRequestDisplayInfo(requestInfo, context) - } + // TODO: below are prototype functionalities. To be removed for productionization. + private fun testCreateCredentialEnabledProviderList(): List { + return listOf( + CreateCredentialProviderData + .Builder("io.enpass.app") + .setSaveEntries( + listOf( + newCreateEntry( + "key1", "subkey-1", "elisa.beckett@gmail.com", + 20, 7, 27, 10L, + "Optional footer description" + ), + newCreateEntry( + "key1", "subkey-2", "elisa.work@google.com", + 20, 7, 27, 12L, + null + ), + ) + ) + .setRemoteEntry( + newRemoteEntry("key2", "subkey-1") + ) + .build(), + CreateCredentialProviderData + .Builder("com.dashlane") + .setSaveEntries( + listOf( + newCreateEntry( + "key1", "subkey-3", "elisa.beckett@dashlane.com", + 20, 7, 27, 11L, + null + ), + newCreateEntry( + "key1", "subkey-4", "elisa.work@dashlane.com", + 20, 7, 27, 14L, + null + ), + ) + ) + .build(), + ) + } - // TODO: below are prototype functionalities. To be removed for productionization. - private fun testCreateCredentialEnabledProviderList(): List { - return listOf( - CreateCredentialProviderData - .Builder("io.enpass.app") - .setSaveEntries( - listOf( - newCreateEntry("key1", "subkey-1", "elisa.beckett@gmail.com", - 20, 7, 27, 10L, - "Optional footer description"), - newCreateEntry("key1", "subkey-2", "elisa.work@google.com", - 20, 7, 27, 12L, - null), - ) - ) - .setRemoteEntry( - newRemoteEntry("key2", "subkey-1") - ) - .build(), - CreateCredentialProviderData - .Builder("com.dashlane") - .setSaveEntries( - listOf( - newCreateEntry("key1", "subkey-3", "elisa.beckett@dashlane.com", - 20, 7, 27, 11L, - null), - newCreateEntry("key1", "subkey-4", "elisa.work@dashlane.com", - 20, 7, 27, 14L, - null), - ) - ) - .build(), - ) - } + private fun testDisabledProviderList(): List? { + return listOf( + DisabledProviderData("com.lastpass.lpandroid"), + DisabledProviderData("com.google.android.youtube") + ) + } - private fun testDisabledProviderList(): List? { - return listOf( - DisabledProviderData("com.lastpass.lpandroid"), - DisabledProviderData("com.google.android.youtube") - ) - } - - private fun testGetCredentialProviderList(): List { - return listOf( - GetCredentialProviderData.Builder("io.enpass.app") - .setCredentialEntries( - listOf( - newGetEntry( - "key1", "subkey-1", TYPE_PASSWORD_CREDENTIAL, "Password", - "elisa.family@outlook.com", null, 3L - ), - newGetEntry( - "key1", "subkey-1", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey", - "elisa.bakery@gmail.com", "Elisa Beckett", 0L - ), - newGetEntry( - "key1", "subkey-2", TYPE_PASSWORD_CREDENTIAL, "Password", - "elisa.bakery@gmail.com", null, 10L - ), - newGetEntry( - "key1", "subkey-3", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey", - "elisa.family@outlook.com", "Elisa Beckett", 1L - ), - ) - ).setAuthenticationEntry( - newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL) - ).setActionChips( - listOf( - newActionEntry( - "key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL, - "Open Google Password Manager", "elisa.beckett@gmail.com" - ), - newActionEntry( - "key3", "subkey-2", TYPE_PASSWORD_CREDENTIAL, - "Open Google Password Manager", "beckett-family@gmail.com" - ), - ) - ).setRemoteEntry( - newRemoteEntry("key4", "subkey-1") - ).build(), - GetCredentialProviderData.Builder("com.dashlane") - .setCredentialEntries( - listOf( - newGetEntry( - "key1", "subkey-2", TYPE_PASSWORD_CREDENTIAL, "Password", - "elisa.family@outlook.com", null, 4L - ), - newGetEntry( - "key1", "subkey-3", TYPE_PASSWORD_CREDENTIAL, "Password", - "elisa.work@outlook.com", null, 11L - ), - ) - ).setAuthenticationEntry( - newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL) - ).setActionChips( - listOf( - newActionEntry( - "key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL, - "Open Enpass" - ), - ) - ).build(), - ) - } + private fun testGetCredentialProviderList(): List { + return listOf( + GetCredentialProviderData.Builder("io.enpass.app") + .setCredentialEntries( + listOf( + newGetEntry( + "key1", "subkey-1", TYPE_PASSWORD_CREDENTIAL, "Password", + "elisa.family@outlook.com", null, 3L + ), + newGetEntry( + "key1", "subkey-1", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey", + "elisa.bakery@gmail.com", "Elisa Beckett", 0L + ), + newGetEntry( + "key1", "subkey-2", TYPE_PASSWORD_CREDENTIAL, "Password", + "elisa.bakery@gmail.com", null, 10L + ), + newGetEntry( + "key1", "subkey-3", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey", + "elisa.family@outlook.com", "Elisa Beckett", 1L + ), + ) + ).setAuthenticationEntry( + newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL) + ).setActionChips( + listOf( + newActionEntry( + "key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL, + "Open Google Password Manager", "elisa.beckett@gmail.com" + ), + newActionEntry( + "key3", "subkey-2", TYPE_PASSWORD_CREDENTIAL, + "Open Google Password Manager", "beckett-family@gmail.com" + ), + ) + ).setRemoteEntry( + newRemoteEntry("key4", "subkey-1") + ).build(), + GetCredentialProviderData.Builder("com.dashlane") + .setCredentialEntries( + listOf( + newGetEntry( + "key1", "subkey-2", TYPE_PASSWORD_CREDENTIAL, "Password", + "elisa.family@outlook.com", null, 4L + ), + newGetEntry( + "key1", "subkey-3", TYPE_PASSWORD_CREDENTIAL, "Password", + "elisa.work@outlook.com", null, 11L + ), + ) + ).setAuthenticationEntry( + newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL) + ).setActionChips( + listOf( + newActionEntry( + "key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL, + "Open Enpass" + ), + ) + ).build(), + ) + } private fun newActionEntry( - key: String, - subkey: String, - credentialType: String, - text: String, - subtext: String? = null, + key: String, + subkey: String, + credentialType: String, + text: String, + subtext: String? = null, ): Entry { val action = Action(text, subtext, null) return Entry( - key, - subkey, - Action.toSlice(action) + key, + subkey, + Action.toSlice(action) ) } private fun newAuthenticationEntry( - key: String, - subkey: String, - credentialType: String, + key: String, + subkey: String, + credentialType: String, ): Entry { val slice = Slice.Builder( - Uri.EMPTY, SliceSpec(credentialType, 1) + Uri.EMPTY, SliceSpec(credentialType, 1) ) return Entry( - key, - subkey, - slice.build() + key, + subkey, + slice.build() ) } private fun newGetEntry( - key: String, - subkey: String, - credentialType: String, - credentialTypeDisplayName: String, - userName: String, - userDisplayName: String?, - lastUsedTimeMillis: Long?, + key: String, + subkey: String, + credentialType: String, + credentialTypeDisplayName: String, + userName: String, + userDisplayName: String?, + lastUsedTimeMillis: Long?, ): Entry { val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD") - .setPackage("com.androidauth.androidvault") + .setPackage("com.androidauth.androidvault") intent.putExtra("provider_extra_sample", "testprovider") - val pendingIntent = PendingIntent.getActivity(context, 1, - intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - or PendingIntent.FLAG_ONE_SHOT)) + val pendingIntent = PendingIntent.getActivity( + context, 1, + intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + or PendingIntent.FLAG_ONE_SHOT) + ) - val credentialEntry = CredentialEntry(credentialType, credentialTypeDisplayName, userName, - userDisplayName, pendingIntent, lastUsedTimeMillis - ?: 0L, null, false) + val credentialEntry = CredentialEntry( + credentialType, credentialTypeDisplayName, userName, + userDisplayName, pendingIntent, lastUsedTimeMillis + ?: 0L, null, false + ) return Entry( - key, - subkey, - CredentialEntry.toSlice(credentialEntry), - Intent() - ) - } - - private fun newCreateEntry( - key: String, - subkey: String, - providerDisplayName: String, - passwordCount: Int, - passkeyCount: Int, - totalCredentialCount: Int, - lastUsedTimeMillis: Long, - footerDescription: String?, - ): Entry { - val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD") - .setPackage("com.androidauth.androidvault") - intent.putExtra("provider_extra_sample", "testprovider") - val pendingIntent = PendingIntent.getActivity(context, 1, - intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - or PendingIntent.FLAG_ONE_SHOT)) - val createPasswordRequest = android.service.credentials.CreateCredentialRequest( - android.service.credentials.CallingAppInfo( - context.applicationInfo.packageName, SigningInfo()), - TYPE_PASSWORD_CREDENTIAL, - toCredentialDataBundle("beckett-bakert@gmail.com", "password123") - ) - val fillInIntent = Intent().putExtra( - CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST, - createPasswordRequest) - - val createEntry = CreateEntry( - providerDisplayName, pendingIntent, - null, lastUsedTimeMillis, - listOf( - CredentialCountInformation.createPasswordCountInformation(passwordCount), - CredentialCountInformation.createPublicKeyCountInformation(passkeyCount), - ), footerDescription) - return Entry( - key, - subkey, - CreateEntry.toSlice(createEntry), - fillInIntent + key, + subkey, + CredentialEntry.toSlice(credentialEntry), + Intent() ) } - private fun newRemoteEntry( - key: String, - subkey: String, - ): Entry { - return Entry( - key, - subkey, - Slice.Builder( - Uri.EMPTY, SliceSpec("type", 1) - ).build() - ) - } - - private fun testCreatePasskeyRequestInfo(): RequestInfo { - val request = CreatePublicKeyCredentialRequest("{\"extensions\": {\n" + - " \"webauthn.loc\": true\n" + - " },\n" + - " \"attestation\": \"direct\",\n" + - " \"challenge\": \"-rSQHXSQUdaK1N-La5bE-JPt6EVAW4SxX1K_tXhZ_Gk\",\n" + - " \"user\": {\n" + - " \"displayName\": \"testName\",\n" + - " \"name\": \"credManTesting@gmail.com\",\n" + - " \"id\": \"eD4o2KoXLpgegAtnM5cDhhUPvvk2\"\n" + - " },\n" + - " \"excludeCredentials\": [],\n" + - " \"rp\": {\n" + - " \"name\": \"Address Book\",\n" + - " \"id\": \"addressbook-c7876.uc.r.appspot.com\"\n" + - " },\n" + - " \"timeout\": 60000,\n" + - " \"pubKeyCredParams\": [\n" + - " {\n" + - " \"type\": \"public-key\",\n" + - " \"alg\": -7\n" + - " },\n" + - " {\n" + - " \"type\": \"public-key\",\n" + - " \"alg\": -257\n" + - " },\n" + - " {\n" + - " \"type\": \"public-key\",\n" + - " \"alg\": -37\n" + - " }\n" + - " ],\n" + - " \"authenticatorSelection\": {\n" + - " \"residentKey\": \"required\",\n" + - " \"requireResidentKey\": true\n" + - " }}") - val credentialData = request.credentialData - return RequestInfo.newCreateRequestInfo( - Binder(), - CreateCredentialRequest( - TYPE_PUBLIC_KEY_CREDENTIAL, - credentialData, - // TODO: populate with actual data - /*candidateQueryData=*/ Bundle(), - /*isSystemProviderRequired=*/ false - ), - "com.google.android.youtube" - ) - } - - private fun testCreatePasswordRequestInfo(): RequestInfo { - val data = toCredentialDataBundle("beckett-bakert@gmail.com", "password123") - return RequestInfo.newCreateRequestInfo( - Binder(), - CreateCredentialRequest( - TYPE_PASSWORD_CREDENTIAL, - data, - // TODO: populate with actual data - /*candidateQueryData=*/ Bundle(), - /*isSystemProviderRequired=*/ false - ), - "com.google.android.youtube" - ) - } - - private fun testCreateOtherCredentialRequestInfo(): RequestInfo { - val data = Bundle() - return RequestInfo.newCreateRequestInfo( - Binder(), - CreateCredentialRequest( - "other-sign-ins", - data, - /*candidateQueryData=*/ Bundle(), - /*isSystemProviderRequired=*/ false - ), - "com.google.android.youtube" - ) - } - - private fun testGetRequestInfo(): RequestInfo { - return RequestInfo.newGetRequestInfo( - Binder(), - GetCredentialRequest.Builder( - Bundle() - ) - .addGetCredentialOption( - GetCredentialOption( - TYPE_PUBLIC_KEY_CREDENTIAL, Bundle(), Bundle(), /*isSystemProviderRequired=*/ false) + private fun newCreateEntry( + key: String, + subkey: String, + providerDisplayName: String, + passwordCount: Int, + passkeyCount: Int, + totalCredentialCount: Int, + lastUsedTimeMillis: Long, + footerDescription: String?, + ): Entry { + val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD") + .setPackage("com.androidauth.androidvault") + intent.putExtra("provider_extra_sample", "testprovider") + val pendingIntent = PendingIntent.getActivity( + context, 1, + intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + or PendingIntent.FLAG_ONE_SHOT) ) - .build(), - "com.google.android.youtube" - ) - } + val createPasswordRequest = android.service.credentials.CreateCredentialRequest( + android.service.credentials.CallingAppInfo( + context.applicationInfo.packageName, SigningInfo() + ), + TYPE_PASSWORD_CREDENTIAL, + toCredentialDataBundle("beckett-bakert@gmail.com", "password123") + ) + val fillInIntent = Intent().putExtra( + CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST, + createPasswordRequest + ) + + val createEntry = CreateEntry( + providerDisplayName, pendingIntent, + null, lastUsedTimeMillis, + listOf( + CredentialCountInformation.createPasswordCountInformation(passwordCount), + CredentialCountInformation.createPublicKeyCountInformation(passkeyCount), + ), footerDescription + ) + return Entry( + key, + subkey, + CreateEntry.toSlice(createEntry), + fillInIntent + ) + } + + private fun newRemoteEntry( + key: String, + subkey: String, + ): Entry { + return Entry( + key, + subkey, + Slice.Builder( + Uri.EMPTY, SliceSpec("type", 1) + ).build() + ) + } + + private fun testCreatePasskeyRequestInfo(): RequestInfo { + val request = CreatePublicKeyCredentialRequest( + "{\"extensions\": {\n" + + " \"webauthn.loc\": true\n" + + " },\n" + + " \"attestation\": \"direct\",\n" + + " \"challenge\":" + + " \"-rSQHXSQUdaK1N-La5bE-JPt6EVAW4SxX1K_tXhZ_Gk\",\n" + + " \"user\": {\n" + + " \"displayName\": \"testName\",\n" + + " \"name\": \"credManTesting@gmail.com\",\n" + + " \"id\": \"eD4o2KoXLpgegAtnM5cDhhUPvvk2\"\n" + + " },\n" + + " \"excludeCredentials\": [],\n" + + " \"rp\": {\n" + + " \"name\": \"Address Book\",\n" + + " \"id\": \"addressbook-c7876.uc.r.appspot.com\"\n" + + " },\n" + + " \"timeout\": 60000,\n" + + " \"pubKeyCredParams\": [\n" + + " {\n" + + " \"type\": \"public-key\",\n" + + " \"alg\": -7\n" + + " },\n" + + " {\n" + + " \"type\": \"public-key\",\n" + + " \"alg\": -257\n" + + " },\n" + + " {\n" + + " \"type\": \"public-key\",\n" + + " \"alg\": -37\n" + + " }\n" + + " ],\n" + + " \"authenticatorSelection\": {\n" + + " \"residentKey\": \"required\",\n" + + " \"requireResidentKey\": true\n" + + " }}" + ) + val credentialData = request.credentialData + return RequestInfo.newCreateRequestInfo( + Binder(), + CreateCredentialRequest( + TYPE_PUBLIC_KEY_CREDENTIAL, + credentialData, + // TODO: populate with actual data + /*candidateQueryData=*/ Bundle(), + /*isSystemProviderRequired=*/ false + ), + "com.google.android.youtube" + ) + } + + private fun testCreatePasswordRequestInfo(): RequestInfo { + val data = toCredentialDataBundle("beckett-bakert@gmail.com", "password123") + return RequestInfo.newCreateRequestInfo( + Binder(), + CreateCredentialRequest( + TYPE_PASSWORD_CREDENTIAL, + data, + // TODO: populate with actual data + /*candidateQueryData=*/ Bundle(), + /*isSystemProviderRequired=*/ false + ), + "com.google.android.youtube" + ) + } + + private fun testCreateOtherCredentialRequestInfo(): RequestInfo { + val data = Bundle() + return RequestInfo.newCreateRequestInfo( + Binder(), + CreateCredentialRequest( + "other-sign-ins", + data, + /*candidateQueryData=*/ Bundle(), + /*isSystemProviderRequired=*/ false + ), + "com.google.android.youtube" + ) + } + + private fun testGetRequestInfo(): RequestInfo { + return RequestInfo.newGetRequestInfo( + Binder(), + GetCredentialRequest.Builder( + Bundle() + ) + .addGetCredentialOption( + GetCredentialOption( + TYPE_PUBLIC_KEY_CREDENTIAL, + Bundle(), + Bundle(), /*isSystemProviderRequired=*/ + false + ) + ) + .build(), + "com.google.android.youtube" + ) + } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt index df8574560e2ca..3b9c02adde841 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -31,6 +31,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember 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.createflow.CreateCredentialScreen @@ -45,11 +46,15 @@ class CredentialSelectorActivity : ComponentActivity() { super.onCreate(savedInstanceState) val credManRepo = CredentialManagerRepo(this, intent) UserConfigRepo.setup(this) - val requestInfo = credManRepo.requestInfo - setContent { - CredentialSelectorTheme { - CredentialManagerBottomSheet(requestInfo.type, credManRepo) + try { + setContent { + CredentialSelectorTheme { + CredentialManagerBottomSheet(credManRepo.requestInfo.type, credManRepo) + } } + } catch (e: Exception) { + Log.e(Constants.LOG_TAG, "Failed to show the credential selector", e) + reportInstantiationErrorAndFinishActivity(credManRepo) } } @@ -65,7 +70,22 @@ class CredentialSelectorActivity : ComponentActivity() { when (requestType) { RequestInfo.TYPE_CREATE -> { val viewModel: CreateCredentialViewModel = viewModel { - CreateCredentialViewModel(credManRepo) + val vm = CreateCredentialViewModel.newInstance( + credManRepo = credManRepo, + providerEnableListUiState = + credManRepo.getCreateProviderEnableListInitialUiState(), + providerDisableListUiState = + credManRepo.getCreateProviderDisableListInitialUiState(), + requestDisplayInfoUiState = + credManRepo.getCreateRequestDisplayInfoInitialUiState() + ) + if (vm == null) { + // Input parsing failed. Close the activity. + reportInstantiationErrorAndFinishActivity(credManRepo) + throw IllegalStateException() + } else { + vm + } } LaunchedEffect(viewModel.uiState.dialogState) { handleDialogState(viewModel.uiState.dialogState) @@ -74,11 +94,21 @@ class CredentialSelectorActivity : ComponentActivity() { viewModel.onProviderActivityResult(it) providerActivityResult.value = null } - CreateCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher) + CreateCredentialScreen( + viewModel = viewModel, + providerActivityLauncher = launcher + ) } RequestInfo.TYPE_GET -> { val viewModel: GetCredentialViewModel = viewModel { - GetCredentialViewModel(credManRepo) + val initialUiState = credManRepo.getCredentialInitialUiState() + if (initialUiState == null) { + // Input parsing failed. Close the activity. + reportInstantiationErrorAndFinishActivity(credManRepo) + throw IllegalStateException() + } else { + GetCredentialViewModel(credManRepo, initialUiState) + } } LaunchedEffect(viewModel.uiState.dialogState) { handleDialogState(viewModel.uiState.dialogState) @@ -90,18 +120,24 @@ class CredentialSelectorActivity : ComponentActivity() { GetCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher) } else -> { - Log.w("AccountSelector", "Unknown type, not rendering any UI") - this.finish() + Log.d(Constants.LOG_TAG, "Unknown type, not rendering any UI") + 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.i("AccountSelector", "Received signal to finish the activity.") + Log.d(Constants.LOG_TAG, "Received signal to finish the activity.") this@CredentialSelectorActivity.finish() } else if (dialogState == DialogState.CANCELED_FOR_SETTINGS) { - Log.i("AccountSelector", "Received signal to finish the activity and launch settings.") + Log.d(Constants.LOG_TAG, "Received signal to finish the activity and launch settings.") this@CredentialSelectorActivity.startActivity(Intent(Settings.ACTION_SYNC_SETTINGS)) this@CredentialSelectorActivity.finish() } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index d9e4dc85fd776..3f705d648ec47 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -19,21 +19,23 @@ package com.android.credentialmanager import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager -import android.credentials.ui.Entry -import android.credentials.ui.GetCredentialProviderData import android.credentials.ui.CreateCredentialProviderData import android.credentials.ui.DisabledProviderData +import android.credentials.ui.Entry +import android.credentials.ui.GetCredentialProviderData import android.credentials.ui.RequestInfo import android.graphics.drawable.Drawable import android.text.TextUtils +import android.util.Log +import com.android.credentialmanager.common.Constants +import com.android.credentialmanager.createflow.ActiveEntry +import com.android.credentialmanager.createflow.CreateCredentialUiState import com.android.credentialmanager.createflow.CreateOptionInfo +import com.android.credentialmanager.createflow.CreateScreenState +import com.android.credentialmanager.createflow.DisabledProviderInfo +import com.android.credentialmanager.createflow.EnabledProviderInfo import com.android.credentialmanager.createflow.RemoteInfo import com.android.credentialmanager.createflow.RequestDisplayInfo -import com.android.credentialmanager.createflow.EnabledProviderInfo -import com.android.credentialmanager.createflow.CreateScreenState -import com.android.credentialmanager.createflow.ActiveEntry -import com.android.credentialmanager.createflow.DisabledProviderInfo -import com.android.credentialmanager.createflow.CreateCredentialUiState import com.android.credentialmanager.getflow.ActionEntryInfo import com.android.credentialmanager.getflow.AuthenticationEntryInfo import com.android.credentialmanager.getflow.CredentialEntryInfo @@ -45,416 +47,480 @@ import com.android.credentialmanager.jetpack.developer.CreatePublicKeyCredential import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL import com.android.credentialmanager.jetpack.provider.Action import com.android.credentialmanager.jetpack.provider.AuthenticationAction +import com.android.credentialmanager.jetpack.provider.CreateEntry import com.android.credentialmanager.jetpack.provider.CredentialCountInformation import com.android.credentialmanager.jetpack.provider.CredentialEntry -import com.android.credentialmanager.jetpack.provider.CreateEntry import org.json.JSONObject +private fun getAppLabel( + pm: PackageManager, + appPackageName: String +): String? { + return try { + val pkgInfo = pm.getPackageInfo(appPackageName, PackageManager.PackageInfoFlags.of(0)) + pkgInfo.applicationInfo.loadSafeLabel( + pm, 0f, + TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM + ).toString() + } catch (e: PackageManager.NameNotFoundException) { + Log.e(Constants.LOG_TAG, "Caller app not found", e) + null + } +} + +private fun getServiceLabelAndIcon( + pm: PackageManager, + providerFlattenedComponentName: String +): Pair? { + var providerLabel: String? = null + var providerIcon: Drawable? = null + val component = ComponentName.unflattenFromString(providerFlattenedComponentName) + if (component == null) { + // Test data has only package name not component name. + // TODO: remove once test data is removed + try { + val pkgInfo = pm.getPackageInfo( + providerFlattenedComponentName, + PackageManager.PackageInfoFlags.of(0) + ) + providerLabel = + pkgInfo.applicationInfo.loadSafeLabel( + pm, 0f, + TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM + ).toString() + providerIcon = pkgInfo.applicationInfo.loadIcon(pm) + } catch (e: PackageManager.NameNotFoundException) { + Log.e(Constants.LOG_TAG, "Provider info not found", e) + } + } else { + try { + val si = pm.getServiceInfo(component, PackageManager.ComponentInfoFlags.of(0)) + providerLabel = si.loadSafeLabel( + pm, 0f, + TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM + ).toString() + providerIcon = si.loadIcon(pm) + } catch (e: PackageManager.NameNotFoundException) { + Log.e(Constants.LOG_TAG, "Provider info not found", e) + } + } + return if (providerLabel == null || providerIcon == null) { + Log.d( + Constants.LOG_TAG, + "Failed to load provider label/icon for provider $providerFlattenedComponentName" + ) + null + } else { + Pair(providerLabel, providerIcon) + } +} + /** Utility functions for converting CredentialManager data structures to or from UI formats. */ class GetFlowUtils { - companion object { - - fun toProviderList( + companion object { + // Returns the list (potentially empty) of enabled provider. + fun toProviderList( providerDataList: List, context: Context, - ): List { - val packageManager = context.packageManager - return providerDataList.map { - val componentName = ComponentName.unflattenFromString(it.providerFlattenedComponentName) - var packageName = componentName?.packageName - if (componentName == null) { - // TODO: Remove once test data is fixed - packageName = it.providerFlattenedComponentName + ): List { + val providerList: MutableList = mutableListOf() + providerDataList.forEach { + val providerLabelAndIcon = getServiceLabelAndIcon( + context.packageManager, + it.providerFlattenedComponentName + ) ?: return@forEach + val (providerLabel, providerIcon) = providerLabelAndIcon + providerList.add( + ProviderInfo( + id = it.providerFlattenedComponentName, + icon = providerIcon, + displayName = providerLabel, + credentialEntryList = getCredentialOptionInfoList( + it.providerFlattenedComponentName, it.credentialEntries, context + ), + authenticationEntry = getAuthenticationEntry( + it.providerFlattenedComponentName, + providerLabel, + providerIcon, + it.authenticationEntry + ), + remoteEntry = getRemoteEntry( + it.providerFlattenedComponentName, + it.remoteEntry + ), + actionEntryList = getActionEntryList( + it.providerFlattenedComponentName, it.actionChips, providerIcon + ), + ) + ) + } + return providerList } - val pkgInfo = packageManager - .getPackageInfo(packageName!!, - PackageManager.PackageInfoFlags.of(0)) - val providerDisplayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString() - // TODO: decide what to do when failed to load a provider icon - val providerIcon = pkgInfo.applicationInfo.loadIcon(packageManager)!! - ProviderInfo( - id = it.providerFlattenedComponentName, - // TODO: decide what to do when failed to load a provider icon - icon = providerIcon, - displayName = providerDisplayName, - credentialEntryList = getCredentialOptionInfoList( - it.providerFlattenedComponentName, it.credentialEntries, context), - authenticationEntry = getAuthenticationEntry( - it.providerFlattenedComponentName, - providerDisplayName, - providerIcon, - it.authenticationEntry), - remoteEntry = getRemoteEntry(it.providerFlattenedComponentName, it.remoteEntry), - actionEntryList = getActionEntryList( - it.providerFlattenedComponentName, it.actionChips, providerIcon), - ) - } - } - - fun toRequestDisplayInfo( + fun toRequestDisplayInfo( requestInfo: RequestInfo, context: Context, - ): com.android.credentialmanager.getflow.RequestDisplayInfo { - val packageName = requestInfo.appPackageName - val pkgInfo = context.packageManager.getPackageInfo(packageName, - PackageManager.PackageInfoFlags.of(0)) - val appLabel = pkgInfo.applicationInfo.loadSafeLabel(context.packageManager, 0f, - TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM) - return com.android.credentialmanager.getflow.RequestDisplayInfo( - appName = appLabel.toString() - ) - } + ): com.android.credentialmanager.getflow.RequestDisplayInfo? { + return com.android.credentialmanager.getflow.RequestDisplayInfo( + appName = getAppLabel(context.packageManager, requestInfo.appPackageName) + ?: return null + ) + } - /* From service data structure to UI credential entry list representation. */ - private fun getCredentialOptionInfoList( + /* From service data structure to UI credential entry list representation. */ + private fun getCredentialOptionInfoList( providerId: String, credentialEntries: List, context: Context, - ): List { - return credentialEntries.map { - // TODO: handle NPE gracefully - val credentialEntry = CredentialEntry.fromSlice(it.slice)!! + ): List { + return credentialEntries.map { + // TODO: handle NPE gracefully + val credentialEntry = CredentialEntry.fromSlice(it.slice)!! - // Consider directly move the UI object into the class. - return@map CredentialEntryInfo( - providerId = providerId, - entryKey = it.key, - entrySubkey = it.subkey, - pendingIntent = credentialEntry.pendingIntent, - fillInIntent = it.frameworkExtrasIntent, - credentialType = credentialEntry.type.toString(), - credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(), - userName = credentialEntry.username.toString(), - displayName = credentialEntry.displayName?.toString(), - // TODO: proper fallback - icon = credentialEntry.icon?.loadDrawable(context), - lastUsedTimeMillis = credentialEntry.lastUsedTimeMillis, - ) - } - } + // Consider directly move the UI object into the class. + return@map CredentialEntryInfo( + providerId = providerId, + entryKey = it.key, + entrySubkey = it.subkey, + pendingIntent = credentialEntry.pendingIntent, + fillInIntent = it.frameworkExtrasIntent, + credentialType = credentialEntry.type, + credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(), + userName = credentialEntry.username.toString(), + displayName = credentialEntry.displayName?.toString(), + // TODO: proper fallback + icon = credentialEntry.icon?.loadDrawable(context), + lastUsedTimeMillis = credentialEntry.lastUsedTimeMillis, + ) + } + } - private fun getAuthenticationEntry( + private fun getAuthenticationEntry( providerId: String, providerDisplayName: String, providerIcon: Drawable, authEntry: Entry?, - ): AuthenticationEntryInfo? { - if (authEntry == null) { - return null - } - val authStructuredEntry = AuthenticationAction.fromSlice( - authEntry!!.slice) - if (authStructuredEntry == null) { - return null - } + ): AuthenticationEntryInfo? { + if (authEntry == null) { + return null + } + val authStructuredEntry = AuthenticationAction.fromSlice( + authEntry!!.slice + ) + if (authStructuredEntry == null) { + return null + } - return AuthenticationEntryInfo( - providerId = providerId, - entryKey = authEntry.key, - entrySubkey = authEntry.subkey, - pendingIntent = authStructuredEntry.pendingIntent, - fillInIntent = authEntry.frameworkExtrasIntent, - title = providerDisplayName, - icon = providerIcon, - ) - } + return AuthenticationEntryInfo( + providerId = providerId, + entryKey = authEntry.key, + entrySubkey = authEntry.subkey, + pendingIntent = authStructuredEntry.pendingIntent, + fillInIntent = authEntry.frameworkExtrasIntent, + title = providerDisplayName, + icon = providerIcon, + ) + } - private fun getRemoteEntry(providerId: String, remoteEntry: Entry?): RemoteEntryInfo? { - // TODO: should also call fromSlice after getting the official jetpack code. - if (remoteEntry == null) { - return null - } - return RemoteEntryInfo( - providerId = providerId, - entryKey = remoteEntry.key, - entrySubkey = remoteEntry.subkey, - pendingIntent = remoteEntry.pendingIntent, - fillInIntent = remoteEntry.frameworkExtrasIntent, - ) - } + private fun getRemoteEntry(providerId: String, remoteEntry: Entry?): RemoteEntryInfo? { + // TODO: should also call fromSlice after getting the official jetpack code. + if (remoteEntry == null) { + return null + } + return RemoteEntryInfo( + providerId = providerId, + entryKey = remoteEntry.key, + entrySubkey = remoteEntry.subkey, + pendingIntent = remoteEntry.pendingIntent, + fillInIntent = remoteEntry.frameworkExtrasIntent, + ) + } - private fun getActionEntryList( + private fun getActionEntryList( providerId: String, actionEntries: List, providerIcon: Drawable, - ): List { - return actionEntries.map { - // TODO: handle NPE gracefully - val actionEntryUi = Action.fromSlice(it.slice)!! + ): List { + return actionEntries.map { + // TODO: handle NPE gracefully + val actionEntryUi = Action.fromSlice(it.slice)!! - return@map ActionEntryInfo( - providerId = providerId, - entryKey = it.key, - entrySubkey = it.subkey, - pendingIntent = actionEntryUi.pendingIntent, - fillInIntent = it.frameworkExtrasIntent, - title = actionEntryUi.title.toString(), - // TODO: gracefully fail - icon = providerIcon, - subTitle = actionEntryUi.subTitle?.toString(), - ) - } + return@map ActionEntryInfo( + providerId = providerId, + entryKey = it.key, + entrySubkey = it.subkey, + pendingIntent = actionEntryUi.pendingIntent, + fillInIntent = it.frameworkExtrasIntent, + title = actionEntryUi.title.toString(), + // TODO: gracefully fail + icon = providerIcon, + subTitle = actionEntryUi.subTitle?.toString(), + ) + } + } } - } } class CreateFlowUtils { - companion object { - - fun toEnabledProviderList( + companion object { + // Returns the list (potentially empty) of enabled provider. + fun toEnabledProviderList( providerDataList: List, context: Context, - ): List { - // TODO: get from the actual service info - val packageManager = context.packageManager - - return providerDataList.map { - val componentName = ComponentName.unflattenFromString(it.providerFlattenedComponentName) - var packageName = componentName?.packageName - if (componentName == null) { - // TODO: Remove once test data is fixed - packageName = it.providerFlattenedComponentName + ): List { + val providerList: MutableList = mutableListOf() + providerDataList.forEach { + val providerLabelAndIcon = getServiceLabelAndIcon( + context.packageManager, + it.providerFlattenedComponentName + ) ?: return@forEach + val (providerLabel, providerIcon) = providerLabelAndIcon + providerList.add(EnabledProviderInfo( + id = it.providerFlattenedComponentName, + displayName = providerLabel, + icon = providerIcon, + createOptions = toCreationOptionInfoList( + it.providerFlattenedComponentName, it.saveEntries, context + ), + remoteEntry = toRemoteInfo(it.providerFlattenedComponentName, it.remoteEntry), + )) + } + return providerList } - val pkgInfo = packageManager - .getPackageInfo(packageName!!, - PackageManager.PackageInfoFlags.of(0)) - EnabledProviderInfo( - // TODO: decide what to do when failed to load a provider icon - icon = pkgInfo.applicationInfo.loadIcon(packageManager)!!, - name = it.providerFlattenedComponentName, - displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(), - createOptions = toCreationOptionInfoList( - it.providerFlattenedComponentName, it.saveEntries, context), - remoteEntry = toRemoteInfo(it.providerFlattenedComponentName, it.remoteEntry), - ) - } - } - - fun toDisabledProviderList( + // Returns the list (potentially empty) of disabled provider. + fun toDisabledProviderList( providerDataList: List?, context: Context, - ): List? { - // TODO: get from the actual service info - val packageManager = context.packageManager - return providerDataList?.map { - val componentName = ComponentName.unflattenFromString(it.providerFlattenedComponentName) - var packageName = componentName?.packageName - if (componentName == null) { - // TODO: Remove once test data is fixed - packageName = it.providerFlattenedComponentName + ): List { + val providerList: MutableList = mutableListOf() + providerDataList?.forEach { + val providerLabelAndIcon = getServiceLabelAndIcon( + context.packageManager, + it.providerFlattenedComponentName + ) ?: return@forEach + val (providerLabel, providerIcon) = providerLabelAndIcon + providerList.add(DisabledProviderInfo( + icon = providerIcon, + id = it.providerFlattenedComponentName, + displayName = providerLabel, + )) + } + return providerList } - val pkgInfo = packageManager - .getPackageInfo(packageName!!, - PackageManager.PackageInfoFlags.of(0)) - DisabledProviderInfo( - icon = pkgInfo.applicationInfo.loadIcon(packageManager)!!, - name = it.providerFlattenedComponentName, - displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(), - ) - } - } - fun toRequestDisplayInfo( + fun toRequestDisplayInfo( requestInfo: RequestInfo, context: Context, - ): RequestDisplayInfo { - val packageName = requestInfo.appPackageName - val pkgInfo = context.packageManager.getPackageInfo(packageName, - PackageManager.PackageInfoFlags.of(0)) - val appLabel = pkgInfo.applicationInfo.loadSafeLabel(context.packageManager, 0f, - TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM) - val createCredentialRequest = requestInfo.createCredentialRequest - val createCredentialRequestJetpack = createCredentialRequest?.let { - CreateCredentialRequest.createFrom( - it.type, it.credentialData, it.candidateQueryData, it.isSystemProviderRequired() - ) - } - when (createCredentialRequestJetpack) { - is CreatePasswordRequest -> { - return RequestDisplayInfo( - createCredentialRequestJetpack.id, - createCredentialRequestJetpack.password, - createCredentialRequestJetpack.type, - appLabel.toString(), - context.getDrawable(R.drawable.ic_password)!! - ) + ): RequestDisplayInfo? { + val appLabel = getAppLabel(context.packageManager, requestInfo.appPackageName) + ?: return null + val createCredentialRequest = requestInfo.createCredentialRequest + val createCredentialRequestJetpack = createCredentialRequest?.let { + CreateCredentialRequest.createFrom( + it.type, it.credentialData, it.candidateQueryData, it.isSystemProviderRequired + ) + } + when (createCredentialRequestJetpack) { + is CreatePasswordRequest -> { + return RequestDisplayInfo( + createCredentialRequestJetpack.id, + createCredentialRequestJetpack.password, + createCredentialRequestJetpack.type, + appLabel, + context.getDrawable(R.drawable.ic_password)!! + ) + } + is CreatePublicKeyCredentialRequest -> { + val requestJson = createCredentialRequestJetpack.requestJson + val json = JSONObject(requestJson) + var name = "" + var displayName = "" + if (json.has("user")) { + val user: JSONObject = json.getJSONObject("user") + name = user.getString("name") + displayName = user.getString("displayName") + } + return RequestDisplayInfo( + name, + displayName, + createCredentialRequestJetpack.type, + appLabel, + context.getDrawable(R.drawable.ic_passkey)!! + ) + } + // TODO: correctly parsing for other sign-ins + else -> { + return RequestDisplayInfo( + "beckett-bakert@gmail.com", + "Elisa Beckett", + "other-sign-ins", + appLabel.toString(), + context.getDrawable(R.drawable.ic_other_sign_in)!! + ) + } + } } - is CreatePublicKeyCredentialRequest -> { - val requestJson = createCredentialRequestJetpack.requestJson - val json = JSONObject(requestJson) - var name = "" - var displayName = "" - if (json.has("user")) { - val user: JSONObject = json.getJSONObject("user") - name = user.getString("name") - displayName = user.getString("displayName") - } - return RequestDisplayInfo( - name, - displayName, - createCredentialRequestJetpack.type, - appLabel.toString(), - context.getDrawable(R.drawable.ic_passkey)!!) - } - // TODO: correctly parsing for other sign-ins - else -> { - return RequestDisplayInfo( - "beckett-bakert@gmail.com", - "Elisa Beckett", - "other-sign-ins", - appLabel.toString(), - context.getDrawable(R.drawable.ic_other_sign_in)!!) - } - } - } - fun toCreateCredentialUiState( + fun toCreateCredentialUiState( enabledProviders: List, disabledProviders: List?, defaultProviderId: String?, requestDisplayInfo: RequestDisplayInfo, isOnPasskeyIntroStateAlready: Boolean, isPasskeyFirstUse: Boolean, - ): CreateCredentialUiState { - var lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo? = null - var remoteEntry: RemoteInfo? = null - var defaultProvider: EnabledProviderInfo? = null - var createOptionsPairs: - MutableList> = mutableListOf() - enabledProviders.forEach { - enabledProvider -> - if (defaultProviderId != null) { - if (enabledProvider.id == defaultProviderId) { - defaultProvider = enabledProvider - } + ): CreateCredentialUiState? { + var lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo? = null + var remoteEntry: RemoteInfo? = null + var defaultProvider: EnabledProviderInfo? = null + var createOptionsPairs: + MutableList> = mutableListOf() + enabledProviders.forEach { enabledProvider -> + if (defaultProviderId != null) { + if (enabledProvider.id == defaultProviderId) { + defaultProvider = enabledProvider + } + } + if (enabledProvider.createOptions.isNotEmpty()) { + lastSeenProviderWithNonEmptyCreateOptions = enabledProvider + enabledProvider.createOptions.forEach { + createOptionsPairs.add(Pair(it, enabledProvider)) + } + } + if (enabledProvider.remoteEntry != null) { + remoteEntry = enabledProvider.remoteEntry!! + } + } + val initialScreenState = toCreateScreenState( + /*createOptionSize=*/createOptionsPairs.size, + /*isOnPasskeyIntroStateAlready=*/isOnPasskeyIntroStateAlready, + /*requestDisplayInfo=*/requestDisplayInfo, + /*defaultProvider=*/defaultProvider, /*remoteEntry=*/remoteEntry, + /*isPasskeyFirstUse=*/isPasskeyFirstUse + ) + if (initialScreenState == null) { + return null + } + return CreateCredentialUiState( + enabledProviders = enabledProviders, + disabledProviders = disabledProviders, + currentScreenState = initialScreenState, + requestDisplayInfo = requestDisplayInfo, + sortedCreateOptionsPairs = createOptionsPairs.sortedWith( + compareByDescending { it.first.lastUsedTimeMillis } + ), + hasDefaultProvider = defaultProvider != null, + activeEntry = toActiveEntry( + /*defaultProvider=*/defaultProvider, + /*createOptionSize=*/createOptionsPairs.size, + /*lastSeenProviderWithNonEmptyCreateOptions=*/ + lastSeenProviderWithNonEmptyCreateOptions, + /*remoteEntry=*/remoteEntry + ), + ) } - if (enabledProvider.createOptions.isNotEmpty()) { - lastSeenProviderWithNonEmptyCreateOptions = enabledProvider - enabledProvider.createOptions.forEach { - createOptionsPairs.add(Pair(it, enabledProvider)) - } - } - if (enabledProvider.remoteEntry != null) { - remoteEntry = enabledProvider.remoteEntry!! - } - } - return CreateCredentialUiState( - enabledProviders = enabledProviders, - disabledProviders = disabledProviders, - toCreateScreenState( - /*createOptionSize=*/createOptionsPairs.size, - /*isOnPasskeyIntroStateAlready=*/isOnPasskeyIntroStateAlready, - /*requestDisplayInfo=*/requestDisplayInfo, - /*defaultProvider=*/defaultProvider, /*remoteEntry=*/remoteEntry, - /*isPasskeyFirstUse=*/isPasskeyFirstUse), - requestDisplayInfo, - createOptionsPairs.sortedWith(compareByDescending{ it.first.lastUsedTimeMillis }), - defaultProvider != null, - toActiveEntry( - /*defaultProvider=*/defaultProvider, - /*createOptionSize=*/createOptionsPairs.size, - /*lastSeenProviderWithNonEmptyCreateOptions=*/ - lastSeenProviderWithNonEmptyCreateOptions, - /*remoteEntry=*/remoteEntry), - ) - } - private fun toCreateScreenState( + private fun toCreateScreenState( createOptionSize: Int, isOnPasskeyIntroStateAlready: Boolean, requestDisplayInfo: RequestDisplayInfo, defaultProvider: EnabledProviderInfo?, remoteEntry: RemoteInfo?, isPasskeyFirstUse: Boolean, - ): CreateScreenState { - return if ( - isPasskeyFirstUse && requestDisplayInfo - .type == TYPE_PUBLIC_KEY_CREDENTIAL && !isOnPasskeyIntroStateAlready) { - CreateScreenState.PASSKEY_INTRO - } else if ( - (defaultProvider == null || defaultProvider.createOptions.isEmpty() - ) && createOptionSize > 1) { - CreateScreenState.PROVIDER_SELECTION - } else if ( - ((defaultProvider == null || defaultProvider.createOptions.isEmpty() - ) && createOptionSize == 1) || ( - defaultProvider != null && defaultProvider.createOptions.isNotEmpty())) { - CreateScreenState.CREATION_OPTION_SELECTION - } else if (createOptionSize == 0 && remoteEntry != null) { - CreateScreenState.EXTERNAL_ONLY_SELECTION - } else { - // TODO: properly handle error and gracefully finish itself - throw java.lang.IllegalStateException("Empty provider list.") - } - } + ): CreateScreenState? { + return if (isPasskeyFirstUse && requestDisplayInfo.type == + TYPE_PUBLIC_KEY_CREDENTIAL && !isOnPasskeyIntroStateAlready) { + CreateScreenState.PASSKEY_INTRO + } else if ((defaultProvider == null || defaultProvider.createOptions.isEmpty()) && + createOptionSize > 1) { + CreateScreenState.PROVIDER_SELECTION + } else if (((defaultProvider == null || defaultProvider.createOptions.isEmpty()) && + createOptionSize == 1) || (defaultProvider != null && + defaultProvider.createOptions.isNotEmpty())) { + CreateScreenState.CREATION_OPTION_SELECTION + } else if (createOptionSize == 0 && remoteEntry != null) { + CreateScreenState.EXTERNAL_ONLY_SELECTION + } else { + Log.d( + Constants.LOG_TAG, + "Unexpected failure: the screen state failed to instantiate" + + " because the provider list is empty." + ) + null + } + } - private fun toActiveEntry( + private fun toActiveEntry( defaultProvider: EnabledProviderInfo?, createOptionSize: Int, lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo?, remoteEntry: RemoteInfo?, - ): ActiveEntry? { - return if ( - defaultProvider != null && defaultProvider.createOptions.isEmpty() && - remoteEntry != null) { - ActiveEntry(defaultProvider, remoteEntry) - } else if ( - defaultProvider != null && defaultProvider.createOptions.isNotEmpty() - ) { - ActiveEntry(defaultProvider, defaultProvider.createOptions.first()) - } else if (createOptionSize == 1) { - ActiveEntry(lastSeenProviderWithNonEmptyCreateOptions!!, - lastSeenProviderWithNonEmptyCreateOptions.createOptions.first()) - } else null - } + ): ActiveEntry? { + return if ( + defaultProvider != null && defaultProvider.createOptions.isEmpty() && + remoteEntry != null + ) { + ActiveEntry(defaultProvider, remoteEntry) + } else if ( + defaultProvider != null && defaultProvider.createOptions.isNotEmpty() + ) { + ActiveEntry(defaultProvider, defaultProvider.createOptions.first()) + } else if (createOptionSize == 1) { + ActiveEntry( + lastSeenProviderWithNonEmptyCreateOptions!!, + lastSeenProviderWithNonEmptyCreateOptions.createOptions.first() + ) + } else null + } - private fun toCreationOptionInfoList( + private fun toCreationOptionInfoList( providerId: String, creationEntries: List, context: Context, - ): List { - return creationEntries.map { - // TODO: handle NPE gracefully - val createEntry = CreateEntry.fromSlice(it.slice)!! + ): List { + return creationEntries.map { + // TODO: handle NPE gracefully + val createEntry = CreateEntry.fromSlice(it.slice)!! - return@map CreateOptionInfo( - // TODO: remove fallbacks - providerId = providerId, - entryKey = it.key, - entrySubkey = it.subkey, - pendingIntent = createEntry.pendingIntent, - fillInIntent = it.frameworkExtrasIntent, - userProviderDisplayName = createEntry.accountName.toString(), - profileIcon = createEntry.icon?.loadDrawable(context), - passwordCount = CredentialCountInformation.getPasswordCount( - createEntry.credentialCountInformationList) ?: 0, - passkeyCount = CredentialCountInformation.getPasskeyCount( - createEntry.credentialCountInformationList) ?: 0, - totalCredentialCount = CredentialCountInformation.getTotalCount( - createEntry.credentialCountInformationList) ?: 0, - lastUsedTimeMillis = createEntry.lastUsedTimeMillis ?: 0, - footerDescription = createEntry.footerDescription?.toString() - ) - } - } + return@map CreateOptionInfo( + // TODO: remove fallbacks + providerId = providerId, + entryKey = it.key, + entrySubkey = it.subkey, + pendingIntent = createEntry.pendingIntent, + fillInIntent = it.frameworkExtrasIntent, + userProviderDisplayName = createEntry.accountName.toString(), + profileIcon = createEntry.icon?.loadDrawable(context), + passwordCount = CredentialCountInformation.getPasswordCount( + createEntry.credentialCountInformationList + ) ?: 0, + passkeyCount = CredentialCountInformation.getPasskeyCount( + createEntry.credentialCountInformationList + ) ?: 0, + totalCredentialCount = CredentialCountInformation.getTotalCount( + createEntry.credentialCountInformationList + ) ?: 0, + lastUsedTimeMillis = createEntry.lastUsedTimeMillis ?: 0, + footerDescription = createEntry.footerDescription?.toString() + ) + } + } - private fun toRemoteInfo( + private fun toRemoteInfo( providerId: String, remoteEntry: Entry?, - ): RemoteInfo? { - // TODO: should also call fromSlice after getting the official jetpack code. - return if (remoteEntry != null) { - RemoteInfo( - providerId = providerId, - entryKey = remoteEntry.key, - entrySubkey = remoteEntry.subkey, - pendingIntent = remoteEntry.pendingIntent, - fillInIntent = remoteEntry.frameworkExtrasIntent, - ) - } else null + ): RemoteInfo? { + // TODO: should also call fromSlice after getting the official jetpack code. + return if (remoteEntry != null) { + RemoteInfo( + providerId = providerId, + entryKey = remoteEntry.key, + entrySubkey = remoteEntry.subkey, + pendingIntent = remoteEntry.pendingIntent, + fillInIntent = remoteEntry.frameworkExtrasIntent, + ) + } else null + } } - } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt new file mode 100644 index 0000000000000..37e21a8fc161b --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 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.0 + * + * 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.common + +class Constants { + companion object Constants { + const val LOG_TAG = "CredentialSelector" + } +} \ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt index d3cf241240354..01318b1edcccc 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import com.android.credentialmanager.common.Constants import com.android.credentialmanager.CreateFlowUtils import com.android.credentialmanager.CredentialManagerRepo import com.android.credentialmanager.UserConfigRepo @@ -32,35 +33,16 @@ import com.android.credentialmanager.common.DialogState import com.android.credentialmanager.common.ProviderActivityResult import com.android.credentialmanager.common.ProviderActivityState -data class CreateCredentialUiState( - val enabledProviders: List, - val disabledProviders: List? = null, - val currentScreenState: CreateScreenState, - val requestDisplayInfo: RequestDisplayInfo, - val sortedCreateOptionsPairs: List>, - // Should not change with the real time update of default provider, only determine whether - // we're showing provider selection page at the beginning - val hasDefaultProvider: Boolean, - val activeEntry: ActiveEntry? = null, - val selectedEntry: EntryInfo? = null, - val providerActivityState: ProviderActivityState = - ProviderActivityState.NOT_APPLICABLE, - val isFromProviderSelection: Boolean? = null, - val dialogState: DialogState = DialogState.ACTIVE, -) class CreateCredentialViewModel( private val credManRepo: CredentialManagerRepo, + private val providerEnableListUiState: List, + private val providerDisableListUiState: List, + private val requestDisplayInfoUiState: RequestDisplayInfo, userConfigRepo: UserConfigRepo = UserConfigRepo.getInstance(), ) : ViewModel() { - val providerEnableListUiState = credManRepo.getCreateProviderEnableListInitialUiState() - - val providerDisableListUiState = credManRepo.getCreateProviderDisableListInitialUiState() - - val requestDisplayInfoUiState = credManRepo.getCreateRequestDisplayInfoInitialUiState() val defaultProviderId = userConfigRepo.getDefaultProviderId() - val isPasskeyFirstUse = userConfigRepo.getIsPasskeyFirstUse() var uiState by mutableStateOf( @@ -70,13 +52,18 @@ class CreateCredentialViewModel( defaultProviderId, requestDisplayInfoUiState, false, - isPasskeyFirstUse)) + isPasskeyFirstUse)!!) private set fun onConfirmIntro() { - uiState = CreateFlowUtils.toCreateCredentialUiState( + val newUiState = CreateFlowUtils.toCreateCredentialUiState( providerEnableListUiState, providerDisableListUiState, defaultProviderId, requestDisplayInfoUiState, true, isPasskeyFirstUse) + if (newUiState == null) { + onInternalError() + return + } + uiState = newUiState UserConfigRepo.getInstance().setIsPasskeyFirstUse(false) } @@ -143,6 +130,14 @@ class CreateCredentialViewModel( uiState = uiState.copy(dialogState = DialogState.CANCELED_FOR_SETTINGS) } + // When the view model runs into unexpected illegal state, reports the error back and close + // the activity gracefully. + private fun onInternalError() { + Log.w(Constants.LOG_TAG, "UI closed due to illegal internal state") + credManRepo.onParsingFailureCancel() + uiState = uiState.copy(dialogState = DialogState.COMPLETE) + } + fun onCancel() { credManRepo.onUserCancel() uiState = uiState.copy(dialogState = DialogState.COMPLETE) @@ -171,11 +166,11 @@ class CreateCredentialViewModel( fun onDefaultChanged(providerId: String?) { if (providerId != null) { Log.d( - "Account Selector", "Default provider changed to: " + + Constants.LOG_TAG, "Default provider changed to: " + " {provider=$providerId") UserConfigRepo.getInstance().setDefaultProvider(providerId) } else { - Log.w("Account Selector", "Null provider is being changed") + Log.w(Constants.LOG_TAG, "Null provider is being changed") } } @@ -184,7 +179,7 @@ class CreateCredentialViewModel( val entryKey = selectedEntry.entryKey val entrySubkey = selectedEntry.entrySubkey Log.d( - "Account Selector", "Option selected for entry: " + + Constants.LOG_TAG, "Option selected for entry: " + " {provider=$providerId, key=$entryKey, subkey=$entrySubkey") if (selectedEntry.pendingIntent != null) { uiState = uiState.copy( @@ -211,7 +206,8 @@ class CreateCredentialViewModel( .setFillInIntent(entry.fillInIntent).build() launcher.launch(intentSenderRequest) } else { - Log.w("Account Selector", "No provider UI to launch") + Log.d(Constants.LOG_TAG, "Unexpected: no provider UI to launch") + onInternalError() } } @@ -220,9 +216,9 @@ class CreateCredentialViewModel( if (selectedEntry != null) { onEntrySelected(selectedEntry) } else { - Log.w("Account Selector", - "Illegal state: confirm is pressed but activeEntry isn't set.") - uiState = uiState.copy(dialogState = DialogState.COMPLETE) + Log.d(Constants.LOG_TAG, + "Unexpected: confirm is pressed but no active entry exists.") + onInternalError() } } @@ -232,7 +228,7 @@ class CreateCredentialViewModel( val resultData = providerActivityResult.data if (resultCode == Activity.RESULT_CANCELED) { // Re-display the CredMan UI if the user canceled from the provider UI. - Log.d("Account Selector", "The provider activity was cancelled," + + Log.d(Constants.LOG_TAG, "The provider activity was cancelled," + " re-displaying our UI.") uiState = uiState.copy( selectedEntry = null, @@ -241,18 +237,44 @@ class CreateCredentialViewModel( } else { if (entry != null) { val providerId = entry.providerId - Log.d("Account Selector", "Got provider activity result: {provider=" + + Log.d(Constants.LOG_TAG, "Got provider activity result: {provider=" + "$providerId, key=${entry.entryKey}, subkey=${entry.entrySubkey}, " + "resultCode=$resultCode, resultData=$resultData}" ) credManRepo.onOptionSelected( providerId, entry.entryKey, entry.entrySubkey, resultCode, resultData, ) + uiState = uiState.copy(dialogState = DialogState.COMPLETE) } else { - Log.w("Account Selector", + Log.d(Constants.LOG_TAG, "Illegal state: received a provider result but found no matching entry.") + onInternalError() + } + } + } + + companion object Factory { + // Validates the input and returns null if the input is invalid. + fun newInstance( + credManRepo: CredentialManagerRepo, + providerEnableListUiState: List, + providerDisableListUiState: List, + requestDisplayInfoUiState: RequestDisplayInfo?, + ): CreateCredentialViewModel? { + if (providerEnableListUiState.isEmpty() || requestDisplayInfoUiState == null) { + return null + } + return try { + val result = CreateCredentialViewModel( + credManRepo = credManRepo, + providerEnableListUiState = providerEnableListUiState, + providerDisableListUiState = providerDisableListUiState, + requestDisplayInfoUiState = requestDisplayInfoUiState + ) + result + } catch (e: Exception) { + null } - uiState = uiState.copy(dialogState = DialogState.COMPLETE) } } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt index 957488ffcbcc1..12a5085d44bd8 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt @@ -19,6 +19,25 @@ package com.android.credentialmanager.createflow import android.app.PendingIntent import android.content.Intent import android.graphics.drawable.Drawable +import com.android.credentialmanager.common.DialogState +import com.android.credentialmanager.common.ProviderActivityState + +data class CreateCredentialUiState( + val enabledProviders: List, + val disabledProviders: List? = null, + val currentScreenState: CreateScreenState, + val requestDisplayInfo: RequestDisplayInfo, + val sortedCreateOptionsPairs: List>, + // Should not change with the real time update of default provider, only determine whether + // we're showing provider selection page at the beginning + val hasDefaultProvider: Boolean, + val activeEntry: ActiveEntry? = null, + val selectedEntry: EntryInfo? = null, + val providerActivityState: ProviderActivityState = + ProviderActivityState.NOT_APPLICABLE, + val isFromProviderSelection: Boolean? = null, + val dialogState: DialogState = DialogState.ACTIVE, +) open class ProviderInfo( val icon: Drawable, @@ -28,17 +47,17 @@ open class ProviderInfo( class EnabledProviderInfo( icon: Drawable, - name: String, + id: String, displayName: String, var createOptions: List, var remoteEntry: RemoteInfo?, -) : ProviderInfo(icon, name, displayName) +) : ProviderInfo(icon, id, displayName) class DisabledProviderInfo( icon: Drawable, - name: String, + id: String, displayName: String, -) : ProviderInfo(icon, name, displayName) +) : ProviderInfo(icon, id, displayName) open class EntryInfo ( val providerId: String, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt index 065a2deb31035..7d2f0dab66a54 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import com.android.credentialmanager.CredentialManagerRepo +import com.android.credentialmanager.common.Constants import com.android.credentialmanager.common.DialogState import com.android.credentialmanager.common.ProviderActivityResult import com.android.credentialmanager.common.ProviderActivityState @@ -45,13 +46,16 @@ data class GetCredentialUiState( val dialogState: DialogState = DialogState.ACTIVE, ) -class GetCredentialViewModel(private val credManRepo: CredentialManagerRepo) : ViewModel() { +class GetCredentialViewModel( + private val credManRepo: CredentialManagerRepo, + initialUiState: GetCredentialUiState, +) : ViewModel() { - var uiState by mutableStateOf(credManRepo.getCredentialInitialUiState()) + var uiState by mutableStateOf(initialUiState) private set fun onEntrySelected(entry: EntryInfo) { - Log.d("Account Selector", "credential selected: {provider=${entry.providerId}" + + Log.d(Constants.LOG_TAG, "credential selected: {provider=${entry.providerId}" + ", key=${entry.entryKey}, subkey=${entry.entrySubkey}}") if (entry.pendingIntent != null) { uiState = uiState.copy( @@ -69,9 +73,9 @@ class GetCredentialViewModel(private val credManRepo: CredentialManagerRepo) : V if (activeEntry != null) { onEntrySelected(activeEntry) } else { - Log.w("Account Selector", + Log.d(Constants.LOG_TAG, "Illegal state: confirm is pressed but activeEntry isn't set.") - uiState = uiState.copy(dialogState = DialogState.COMPLETE) + onInternalError() } } @@ -80,23 +84,32 @@ class GetCredentialViewModel(private val credManRepo: CredentialManagerRepo) : V ) { val entry = uiState.selectedEntry if (entry != null && entry.pendingIntent != null) { - Log.d("credentials", "Launching provider activity") + Log.d(Constants.LOG_TAG, "Launching provider activity") uiState = uiState.copy(providerActivityState = ProviderActivityState.PENDING) val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent) .setFillInIntent(entry.fillInIntent).build() launcher.launch(intentSenderRequest) } else { - Log.w("Account Selector", "No provider UI to launch") + Log.d(Constants.LOG_TAG, "No provider UI to launch") + onInternalError() } } + // When the view model runs into unexpected illegal state, reports the error back and close + // the activity gracefully. + private fun onInternalError() { + Log.w(Constants.LOG_TAG, "UI closed due to illegal internal state") + credManRepo.onParsingFailureCancel() + uiState = uiState.copy(dialogState = DialogState.COMPLETE) + } + fun onProviderActivityResult(providerActivityResult: ProviderActivityResult) { val entry = uiState.selectedEntry val resultCode = providerActivityResult.resultCode val resultData = providerActivityResult.data if (resultCode == Activity.RESULT_CANCELED) { // Re-display the CredMan UI if the user canceled from the provider UI. - Log.d("Account Selector", "The provider activity was cancelled," + + Log.d(Constants.LOG_TAG, "The provider activity was cancelled," + " re-displaying our UI.") uiState = uiState.copy( selectedEntry = null, @@ -104,7 +117,8 @@ class GetCredentialViewModel(private val credManRepo: CredentialManagerRepo) : V ) } else { if (entry != null) { - Log.d("Account Selector", "Got provider activity result: {provider=" + + Log.d( + Constants.LOG_TAG, "Got provider activity result: {provider=" + "${entry.providerId}, key=${entry.entryKey}, subkey=${entry.entrySubkey}" + ", resultCode=$resultCode, resultData=$resultData}" ) @@ -112,23 +126,24 @@ class GetCredentialViewModel(private val credManRepo: CredentialManagerRepo) : V entry.providerId, entry.entryKey, entry.entrySubkey, resultCode, resultData, ) + uiState = uiState.copy(dialogState = DialogState.COMPLETE) } else { - Log.w("Account Selector", + Log.w(Constants.LOG_TAG, "Illegal state: received a provider result but found no matching entry.") + onInternalError() } - uiState = uiState.copy(dialogState = DialogState.COMPLETE) } } fun onMoreOptionSelected() { - Log.d("Account Selector", "More Option selected") + Log.d(Constants.LOG_TAG, "More Option selected") uiState = uiState.copy( currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS ) } fun onMoreOptionOnSnackBarSelected(isNoAccount: Boolean) { - Log.d("Account Selector", "More Option on snackBar selected") + Log.d(Constants.LOG_TAG, "More Option on snackBar selected") uiState = uiState.copy( currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS, isNoAccount = isNoAccount,