Merge "Use service info instead of app info and revamped data parsing"

This commit is contained in:
Helen Qin
2023-01-26 20:49:49 +00:00
committed by Android (Google) Code Review
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,