Use service info instead of app info and revamped data parsing

App info was introduced as a temporary solution for testing. It will be
offically removed soon. With this change, the UI will pick up actual
service info for non-testing queries.

Also revamped input data parsing and make sure the UI does not crashes
but instead propagates errors and exists gracefully upon parsing
failures.

Will do another pass to make sure the whole E2E can handle errors
gracefully and add anything missing as a follow up.

Bug: 264898201
Bug: 264906200
Fix: 264898201
Test: local deployment
Change-Id: I2ab3b0e7908171911399cacc6ebcd489997d4e69
This commit is contained in:
Helen Qin
2023-01-26 04:06:10 +00:00
parent fc16dc4b62
commit 57c45eeba2
8 changed files with 1003 additions and 783 deletions

View File

@@ -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;

View File

@@ -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<ProviderData>
private val providerDisabledList: List<DisabledProviderData>?
// TODO: require non-null.
val resultReceiver: ResultReceiver?
val requestInfo: RequestInfo
private val providerEnabledList: List<ProviderData>
private val providerDisabledList: List<DisabledProviderData>?
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<GetCredentialProviderData>, 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<GetCredentialProviderData>, context)
val requestDisplayInfo = GetFlowUtils.toRequestDisplayInfo(requestInfo, context)
return GetCredentialUiState(
providerEnabledList,
requestDisplayInfo,
)
}
fun getCreateProviderEnableListInitialUiState(): List<EnabledProviderInfo> {
val providerEnabledList = CreateFlowUtils.toEnabledProviderList(
// Handle runtime cast error
providerEnabledList as List<CreateCredentialProviderData>, context
)
return providerEnabledList
}
fun getCreateProviderEnableListInitialUiState(): List<EnabledProviderInfo> {
val providerEnabledList = CreateFlowUtils.toEnabledProviderList(
// Handle runtime cast error
providerEnabledList as List<CreateCredentialProviderData>, context)
return providerEnabledList
}
fun getCreateProviderDisableListInitialUiState(): List<DisabledProviderInfo> {
return CreateFlowUtils.toDisabledProviderList(
// Handle runtime cast error
providerDisabledList, context
)
}
fun getCreateProviderDisableListInitialUiState(): List<DisabledProviderInfo>? {
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<CreateCredentialProviderData> {
return listOf(
CreateCredentialProviderData
.Builder("io.enpass.app")
.setSaveEntries(
listOf<Entry>(
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<Entry>(
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<CreateCredentialProviderData> {
return listOf(
CreateCredentialProviderData
.Builder("io.enpass.app")
.setSaveEntries(
listOf<Entry>(
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<Entry>(
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<DisabledProviderData>? {
return listOf(
DisabledProviderData("com.lastpass.lpandroid"),
DisabledProviderData("com.google.android.youtube")
)
}
private fun testDisabledProviderList(): List<DisabledProviderData>? {
return listOf(
DisabledProviderData("com.lastpass.lpandroid"),
DisabledProviderData("com.google.android.youtube")
)
}
private fun testGetCredentialProviderList(): List<GetCredentialProviderData> {
return listOf(
GetCredentialProviderData.Builder("io.enpass.app")
.setCredentialEntries(
listOf<Entry>(
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<Entry>(
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<GetCredentialProviderData> {
return listOf(
GetCredentialProviderData.Builder("io.enpass.app")
.setCredentialEntries(
listOf<Entry>(
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<Entry>(
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"
)
}
}

View File

@@ -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()
}

View File

@@ -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<String, Drawable>? {
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<GetCredentialProviderData>,
context: Context,
): List<ProviderInfo> {
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<ProviderInfo> {
val providerList: MutableList<ProviderInfo> = 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<Entry>,
context: Context,
): List<CredentialEntryInfo> {
return credentialEntries.map {
// TODO: handle NPE gracefully
val credentialEntry = CredentialEntry.fromSlice(it.slice)!!
): List<CredentialEntryInfo> {
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<Entry>,
providerIcon: Drawable,
): List<ActionEntryInfo> {
return actionEntries.map {
// TODO: handle NPE gracefully
val actionEntryUi = Action.fromSlice(it.slice)!!
): List<ActionEntryInfo> {
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<CreateCredentialProviderData>,
context: Context,
): List<EnabledProviderInfo> {
// 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<EnabledProviderInfo> {
val providerList: MutableList<EnabledProviderInfo> = 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<DisabledProviderData>?,
context: Context,
): List<DisabledProviderInfo>? {
// 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<DisabledProviderInfo> {
val providerList: MutableList<DisabledProviderInfo> = 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<EnabledProviderInfo>,
disabledProviders: List<DisabledProviderInfo>?,
defaultProviderId: String?,
requestDisplayInfo: RequestDisplayInfo,
isOnPasskeyIntroStateAlready: Boolean,
isPasskeyFirstUse: Boolean,
): CreateCredentialUiState {
var lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo? = null
var remoteEntry: RemoteInfo? = null
var defaultProvider: EnabledProviderInfo? = null
var createOptionsPairs:
MutableList<Pair<CreateOptionInfo, EnabledProviderInfo>> = 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<Pair<CreateOptionInfo, EnabledProviderInfo>> = 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<Entry>,
context: Context,
): List<CreateOptionInfo> {
return creationEntries.map {
// TODO: handle NPE gracefully
val createEntry = CreateEntry.fromSlice(it.slice)!!
): List<CreateOptionInfo> {
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
}
}
}
}

View File

@@ -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"
}
}

View File

@@ -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<EnabledProviderInfo>,
val disabledProviders: List<DisabledProviderInfo>? = null,
val currentScreenState: CreateScreenState,
val requestDisplayInfo: RequestDisplayInfo,
val sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>,
// 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<EnabledProviderInfo>,
private val providerDisableListUiState: List<DisabledProviderInfo>,
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<EnabledProviderInfo>,
providerDisableListUiState: List<DisabledProviderInfo>,
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)
}
}
}

View File

@@ -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<EnabledProviderInfo>,
val disabledProviders: List<DisabledProviderInfo>? = null,
val currentScreenState: CreateScreenState,
val requestDisplayInfo: RequestDisplayInfo,
val sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>,
// 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<CreateOptionInfo>,
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,

View File

@@ -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,