diff --git a/core/java/android/credentials/ui/CreateCredentialProviderData.java b/core/java/android/credentials/ui/CreateCredentialProviderData.java index 98157d73ad427..044427894d016 100644 --- a/core/java/android/credentials/ui/CreateCredentialProviderData.java +++ b/core/java/android/credentials/ui/CreateCredentialProviderData.java @@ -131,6 +131,13 @@ public class CreateCredentialProviderData extends ProviderData implements Parcel return this; } + /** Sets the remote entry of the provider. */ + @NonNull + public Builder setRemoteEntry(@Nullable Entry remoteEntry) { + mRemoteEntry = remoteEntry; + return this; + } + /** Builds a {@link CreateCredentialProviderData}. */ @NonNull public CreateCredentialProviderData build() { diff --git a/packages/CredentialManager/res/drawable/ic_other_devices.xml b/packages/CredentialManager/res/drawable/ic_other_devices.xml new file mode 100644 index 0000000000000..754648cbca1df --- /dev/null +++ b/packages/CredentialManager/res/drawable/ic_other_devices.xml @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index cdd1a8964b2b4..d47e3f49242c2 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -28,7 +28,10 @@ passkey password sign-ins + Another device Other password manager + + This password manager will store your passwords and passkeys to help you easily sign in. "Close sheet" diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index b71af2b654e4b..0d5009a3bf800 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -181,6 +181,9 @@ class CredentialManagerRepo( 20, 7, 27, 11000), ) ) + .setRemoteEntry( + newRemoteEntry("key1", "subkey-1") + ) .setIsDefaultProvider(true) .build(), CreateCredentialProviderData @@ -308,6 +311,19 @@ class CredentialManagerRepo( ) } + private fun newRemoteEntry( + key: String, + subkey: String, + ): Entry { + return Entry( + key, + subkey, + Slice.Builder( + Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(Entry.VERSION, 1) + ).build() + ) + } + private fun testCreateRequestInfo(): RequestInfo { val data = Bundle() return RequestInfo.newCreateRequestInfo( diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index 53e817e1a03ab..1e43d541ac131 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -23,6 +23,7 @@ import android.credentials.ui.GetCredentialProviderData import android.credentials.ui.CreateCredentialProviderData import android.credentials.ui.DisabledProviderData import com.android.credentialmanager.createflow.CreateOptionInfo +import com.android.credentialmanager.createflow.RemoteInfo import com.android.credentialmanager.getflow.ActionEntryInfo import com.android.credentialmanager.getflow.AuthenticationEntryInfo import com.android.credentialmanager.getflow.CredentialEntryInfo @@ -120,6 +121,7 @@ class CreateFlowUtils { displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(), createOptions = toCreationOptionInfoList(it.saveEntries, context), isDefault = it.isDefaultProvider, + remoteEntry = toRemoteInfo(it.remoteEntry), ) } } @@ -164,5 +166,17 @@ class CreateFlowUtils { ) } } + + private fun toRemoteInfo( + remoteEntry: Entry?, + ): RemoteInfo? { + // TODO: should also call fromSlice after getting the official jetpack code. + return if (remoteEntry != null) { + RemoteInfo( + entryKey = remoteEntry.key, + entrySubkey = remoteEntry.subkey, + ) + } else null + } } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt index 0c3447f02b147..123c3d454905d 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt @@ -30,6 +30,7 @@ class EnabledProviderInfo( displayName: String, var createOptions: List, val isDefault: Boolean, + var remoteEntry: RemoteInfo?, ) : ProviderInfo(icon, name, displayName) class DisabledProviderInfo( @@ -55,6 +56,11 @@ class CreateOptionInfo( val lastUsedTimeMillis: Long?, ) : EntryInfo(entryKey, entrySubkey) +class RemoteInfo( + entryKey: String, + entrySubkey: String, +) : EntryInfo(entryKey, entrySubkey) + data class RequestDisplayInfo( val userName: String, val displayName: String, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt index 4769a26b8eb18..67b704f5d787c 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.outlined.NewReleases import androidx.compose.material.icons.filled.Add import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -80,7 +81,8 @@ fun CreatePasskeyScreen( disabledProviderList = uiState.disabledProviders, onBackButtonSelected = viewModel::onBackButtonSelected, onOptionSelected = viewModel::onMoreOptionsRowSelected, - onDisabledPasswordManagerSelected = viewModel::onDisabledPasswordManagerSelected + onDisabledPasswordManagerSelected = viewModel::onDisabledPasswordManagerSelected, + onRemoteEntrySelected = viewModel::onRemoteEntrySelected ) CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard( providerInfo = uiState.activeEntry?.activeProvider!!, @@ -216,10 +218,11 @@ fun ProviderSelectionCard( fun MoreOptionsSelectionCard( requestDisplayInfo: RequestDisplayInfo, enabledProviderList: List, - disabledProviderList: List, + disabledProviderList: List?, onBackButtonSelected: () -> Unit, onOptionSelected: (ActiveEntry) -> Unit, onDisabledPasswordManagerSelected: () -> Unit, + onRemoteEntrySelected: () -> Unit, ) { Card() { Column() { @@ -267,11 +270,26 @@ fun MoreOptionsSelectionCard( } } } - item { - MoreOptionsDisabledProvidersRow( - disabledProviders = disabledProviderList, - onDisabledPasswordManagerSelected = onDisabledPasswordManagerSelected, - ) + if (disabledProviderList != null) { + item { + MoreOptionsDisabledProvidersRow( + disabledProviders = disabledProviderList, + onDisabledPasswordManagerSelected = onDisabledPasswordManagerSelected, + ) + } + } + var hasRemoteInfo = false + enabledProviderList.forEach { + if (it.remoteEntry != null) { + hasRemoteInfo = true + } + } + if (hasRemoteInfo) { + item { + RemoteEntryRow( + onRemoteEntrySelected = onRemoteEntrySelected, + ) + } } } } @@ -292,9 +310,21 @@ fun MoreOptionsRowIntroCard( ) { Card() { Column() { + Icon( + Icons.Outlined.NewReleases, + contentDescription = null, + modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(all = 24.dp) + ) Text( text = stringResource(R.string.use_provider_for_all_title, providerInfo.displayName), style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(horizontal = 24.dp) + .align(alignment = Alignment.CenterHorizontally), + textAlign = TextAlign.Center, + ) + Text( + text = stringResource(R.string.confirm_default_or_use_once_description), + style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) ) Row( @@ -498,7 +528,7 @@ fun MoreOptionsInfoRow( modifier = Modifier.fillMaxWidth(), onClick = onOptionSelected, icon = { - Image(modifier = Modifier.size(32.dp, 32.dp).padding(start = 10.dp), + Image(modifier = Modifier.size(32.dp, 32.dp).padding(start = 16.dp), bitmap = providerInfo.icon.toBitmap().asImageBitmap(), contentDescription = null) }, @@ -508,12 +538,14 @@ fun MoreOptionsInfoRow( Text( text = providerInfo.displayName, style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(top = 16.dp) + modifier = Modifier.padding(top = 16.dp, start = 16.dp) ) if (createOptionInfo.userProviderDisplayName != null) { Text( text = createOptionInfo.userProviderDisplayName, - style = MaterialTheme.typography.bodyMedium) + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 16.dp) + ) } if (createOptionInfo.passwordCount != null && createOptionInfo.passkeyCount != null) { Text( @@ -524,7 +556,7 @@ fun MoreOptionsInfoRow( createOptionInfo.passkeyCount ), style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 16.dp) + modifier = Modifier.padding(bottom = 16.dp, start = 16.dp) ) } else if (createOptionInfo.passwordCount != null) { Text( @@ -534,7 +566,7 @@ fun MoreOptionsInfoRow( createOptionInfo.passwordCount ), style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 16.dp) + modifier = Modifier.padding(bottom = 16.dp, start = 16.dp) ) } else if (createOptionInfo.passkeyCount != null) { Text( @@ -544,7 +576,7 @@ fun MoreOptionsInfoRow( createOptionInfo.passkeyCount ), style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 16.dp) + modifier = Modifier.padding(bottom = 16.dp, start = 16.dp) ) } else if (createOptionInfo.totalCredentialCount != null) { // TODO: Handle the case when there is total count @@ -567,7 +599,8 @@ fun MoreOptionsDisabledProvidersRow( icon = { Icon( Icons.Filled.Add, - contentDescription = null + contentDescription = null, + modifier = Modifier.padding(start = 16.dp) ) }, shape = MaterialTheme.shapes.large, @@ -576,12 +609,42 @@ fun MoreOptionsDisabledProvidersRow( Text( text = stringResource(R.string.other_password_manager), style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(top = 16.dp) + modifier = Modifier.padding(top = 16.dp, start = 16.dp) ) Text( text = disabledProviders.joinToString(separator = ", "){ it.displayName }, style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 16.dp) + modifier = Modifier.padding(bottom = 16.dp, start = 16.dp) + ) + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RemoteEntryRow( + onRemoteEntrySelected: () -> Unit, +) { + SuggestionChip( + modifier = Modifier.fillMaxWidth(), + onClick = onRemoteEntrySelected, + icon = { + Icon( + painter = painterResource(R.drawable.ic_other_devices), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.padding(start = 18.dp) + ) + }, + shape = MaterialTheme.shapes.large, + label = { + Column() { + Text( + text = stringResource(R.string.another_device), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(start = 16.dp, top = 18.dp, bottom = 18.dp) + .align(alignment = Alignment.CenterHorizontally) ) } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt index 8b94201ad787d..af74b8ea4de16 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt @@ -29,7 +29,7 @@ import com.android.credentialmanager.common.ResultState data class CreatePasskeyUiState( val enabledProviders: List, - val disabledProviders: List, + val disabledProviders: List? = null, val currentScreenState: CreateScreenState, val requestDisplayInfo: RequestDisplayInfo, val activeEntry: ActiveEntry? = null, @@ -103,6 +103,10 @@ class CreatePasskeyViewModel( // TODO: Complete this function } + fun onRemoteEntrySelected() { + // TODO: Complete this function + } + fun onCancel() { CredentialManagerRepo.getInstance().onCancel() dialogResult.value = DialogResult(ResultState.CANCELED)