Merge "Use service info instead of app info and revamped data parsing"
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user