Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3e944957 authored by Helen Qin's avatar Helen Qin
Browse files

Launch provider pending intent from the CredentialManager UI.

Test: locally
Bug: 246564035
Bug: 253156924
Bug: 253156958

Change-Id: Id56070db3fbbbef642fa126e23fdbfe619ef69d7
parent 42051221
Loading
Loading
Loading
Loading
+41 −9
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.credentialmanager

import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
import android.app.PendingIntent
import android.app.slice.Slice
import android.app.slice.SliceSpec
import android.content.Context
@@ -32,6 +33,7 @@ import android.credentials.ui.DisabledProviderData
import android.credentials.ui.ProviderData
import android.credentials.ui.RequestInfo
import android.credentials.ui.BaseDialogResult
import android.credentials.ui.ProviderPendingIntentResponse
import android.credentials.ui.UserSelectionDialogResult
import android.graphics.drawable.Icon
import android.os.Binder
@@ -54,7 +56,7 @@ class CredentialManagerRepo(
  private val context: Context,
  intent: Intent,
) {
  private val requestInfo: RequestInfo
  val requestInfo: RequestInfo
  private val providerEnabledList: List<ProviderData>
  private val providerDisabledList: List<DisabledProviderData>
  // TODO: require non-null.
@@ -75,7 +77,7 @@ class CredentialManagerRepo(
      RequestInfo.TYPE_GET ->
        intent.extras?.getParcelableArrayList(
          ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
          DisabledProviderData::class.java
          GetCredentialProviderData::class.java
        ) ?: testGetCredentialProviderList()
      else -> {
        // TODO: fail gracefully
@@ -101,12 +103,19 @@ class CredentialManagerRepo(
    resultReceiver?.send(BaseDialogResult.RESULT_CODE_DIALOG_CANCELED, resultData)
  }

  fun onOptionSelected(providerPackageName: String, entryKey: String, entrySubkey: String) {
  fun onOptionSelected(
    providerPackageName: String,
    entryKey: String,
    entrySubkey: String,
    resultCode: Int? = null,
    resultData: Intent? = null,
  ) {
    val userSelectionDialogResult = UserSelectionDialogResult(
      requestInfo.token,
      providerPackageName,
      entryKey,
      entrySubkey
      entrySubkey,
      if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
    )
    val resultData = Bundle()
    UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultData)
@@ -328,6 +337,14 @@ class CredentialManagerRepo(
    userDisplayName: String?,
    lastUsedTimeMillis: Long?,
  ): 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 slice = Slice.Builder(
      Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(credentialType, 1)
    ).addText(
@@ -347,7 +364,9 @@ class CredentialManagerRepo(
    return Entry(
      key,
      subkey,
      slice.build()
      slice.build(),
      pendingIntent,
      null
    )
  }

@@ -360,10 +379,22 @@ class CredentialManagerRepo(
    totalCredentialCount: Int,
    lastUsedTimeMillis: Long,
  ): 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(
      context.applicationInfo.packageName,
      "PASSWORD",
      toBundle("beckett-bakert@gmail.com", "password123")
    )
    val fillInIntent = Intent().putExtra("create_request_params", createPasswordRequest)

    val slice = Slice.Builder(
      Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(Entry.VERSION, 1)
    )
      .addText(
    ).addText(
        providerDisplayName, null, listOf(Entry.HINT_USER_PROVIDER_ACCOUNT_NAME))
      .addIcon(
        Icon.createWithResource(context, R.drawable.ic_passkey),
@@ -384,7 +415,9 @@ class CredentialManagerRepo(
    return Entry(
      key,
      subkey,
      slice
      slice,
      pendingIntent,
      fillInIntent,
    )
  }

@@ -415,7 +448,6 @@ class CredentialManagerRepo(
  }

  private fun testGetRequestInfo(): RequestInfo {
    val data = Bundle()
    return RequestInfo.newGetRequestInfo(
      Binder(),
      GetCredentialRequest.Builder()
+24 −19
Original line number Diff line number Diff line
@@ -16,17 +16,21 @@

package com.android.credentialmanager

import android.credentials.ui.RequestInfo
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.lifecycle.Observer
import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.credentialmanager.common.DialogType
import com.android.credentialmanager.common.DialogResult
import com.android.credentialmanager.common.ProviderActivityResult
import com.android.credentialmanager.common.ResultState
import com.android.credentialmanager.createflow.CreateCredentialScreen
import com.android.credentialmanager.createflow.CreateCredentialViewModel
@@ -39,28 +43,23 @@ class CredentialSelectorActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    CredentialManagerRepo.setup(this, intent)
    val requestInfo = intent.extras?.getParcelable<RequestInfo>(RequestInfo.EXTRA_REQUEST_INFO)
    if (requestInfo != null) {
      val requestType = requestInfo.type
    val requestInfo = CredentialManagerRepo.getInstance().requestInfo
    setContent {
      CredentialSelectorTheme {
          CredentialManagerBottomSheet(requestType)
        }
      }
    } else {
      // TODO: prototype only code to be removed. In production should exit.
      setContent {
        CredentialSelectorTheme {
          CredentialManagerBottomSheet(RequestInfo.TYPE_CREATE)
        }
        CredentialManagerBottomSheet(DialogType.toDialogType(requestInfo.type))
      }
    }
  }

  @ExperimentalMaterialApi
  @Composable
  fun CredentialManagerBottomSheet(operationType: String) {
    val dialogType = DialogType.toDialogType(operationType)
  fun CredentialManagerBottomSheet(dialogType: DialogType) {
    val providerActivityResult = remember { mutableStateOf<ProviderActivityResult?>(null) }
    val launcher = rememberLauncherForActivityResult(
      ActivityResultContracts.StartIntentSenderForResult()
    ) {
      providerActivityResult.value = ProviderActivityResult(it.resultCode, it.data)
    }
    when (dialogType) {
      DialogType.CREATE_PASSKEY -> {
        val viewModel: CreateCredentialViewModel = viewModel()
@@ -68,7 +67,10 @@ class CredentialSelectorActivity : ComponentActivity() {
          this@CredentialSelectorActivity,
          onCancel
        )
        CreateCredentialScreen(viewModel = viewModel)
        providerActivityResult.value?.let {
          viewModel.onProviderActivityResult(it)
        }
        CreateCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
      }
      DialogType.GET_CREDENTIALS -> {
        val viewModel: GetCredentialViewModel = viewModel()
@@ -76,7 +78,10 @@ class CredentialSelectorActivity : ComponentActivity() {
          this@CredentialSelectorActivity,
          onCancel
        )
        GetCredentialScreen(viewModel = viewModel)
        providerActivityResult.value?.let {
          viewModel.onProviderActivityResult(it)
        }
        GetCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
      }
      else -> {
        Log.w("AccountSelector", "Unknown type, not rendering any UI")
+20 −1
Original line number Diff line number Diff line
@@ -46,8 +46,15 @@ class GetFlowUtils {
      val packageManager = context.packageManager
      return providerDataList.map {
        // TODO: get from the actual service info
        val componentName = ComponentName.unflattenFromString(it.providerFlattenedComponentName)
        var packageName = componentName?.packageName
        if (componentName == null) {
          // TODO: Remove once test data is fixed
          packageName = it.providerFlattenedComponentName
        }

        val pkgInfo = packageManager
          .getPackageInfo(it.providerFlattenedComponentName,
          .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
@@ -86,6 +93,8 @@ class GetFlowUtils {
          providerId = providerId,
          entryKey = it.key,
          entrySubkey = it.subkey,
          pendingIntent = it.pendingIntent,
          fillInIntent = it.frameworkExtrasIntent,
          credentialType = credentialEntryUi.credentialType.toString(),
          credentialTypeDisplayName = credentialEntryUi.credentialTypeDisplayName.toString(),
          userName = credentialEntryUi.userName.toString(),
@@ -113,6 +122,8 @@ class GetFlowUtils {
        providerId = providerId,
        entryKey = authEntry.key,
        entrySubkey = authEntry.subkey,
        pendingIntent = authEntry.pendingIntent,
        fillInIntent = authEntry.frameworkExtrasIntent,
        title = providerDisplayName,
        icon = providerIcon,
      )
@@ -127,6 +138,8 @@ class GetFlowUtils {
        providerId = providerId,
        entryKey = remoteEntry.key,
        entrySubkey = remoteEntry.subkey,
        pendingIntent = remoteEntry.pendingIntent,
        fillInIntent = remoteEntry.frameworkExtrasIntent,
      )
    }

@@ -142,6 +155,8 @@ class GetFlowUtils {
          providerId = providerId,
          entryKey = it.key,
          entrySubkey = it.subkey,
          pendingIntent = it.pendingIntent,
          fillInIntent = it.frameworkExtrasIntent,
          title = actionEntryUi.text.toString(),
          // TODO: gracefully fail
          icon = actionEntryUi.icon.loadDrawable(context)!!,
@@ -214,6 +229,8 @@ class CreateFlowUtils {
          // TODO: remove fallbacks
          entryKey = it.key,
          entrySubkey = it.subkey,
          pendingIntent = it.pendingIntent,
          fillInIntent = it.frameworkExtrasIntent,
          userProviderDisplayName = saveEntryUi.userProviderAccountName as String,
          credentialTypeIcon = saveEntryUi.credentialTypeIcon?.loadDrawable(context)
            ?: context.getDrawable(R.drawable.ic_passkey)!!,
@@ -235,6 +252,8 @@ class CreateFlowUtils {
        RemoteInfo(
          entryKey = remoteEntry.key,
          entrySubkey = remoteEntry.subkey,
          pendingIntent = remoteEntry.pendingIntent,
          fillInIntent = remoteEntry.frameworkExtrasIntent,
        )
      } else null
    }
+24 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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

import android.content.Intent

data class ProviderActivityResult(
    val resultCode: Int,
    val data: Intent?,
)
 No newline at end of file
+9 −2
Original line number Diff line number Diff line
@@ -3,6 +3,9 @@
package com.android.credentialmanager.createflow

import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.result.ActivityResult
import androidx.activity.result.IntentSenderRequest
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -50,7 +53,11 @@ import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Compa
@Composable
fun CreateCredentialScreen(
  viewModel: CreateCredentialViewModel,
  providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
) {
  val primaryEntryCallback: () -> Unit = {
    viewModel.onPrimaryCreateOptionInfoSelected(providerActivityLauncher)
  }
  val state = rememberModalBottomSheetState(
    initialValue = ModalBottomSheetValue.Expanded,
    skipHalfExpanded = true
@@ -73,8 +80,8 @@ fun CreateCredentialScreen(
          requestDisplayInfo = uiState.requestDisplayInfo,
          providerInfo = uiState.activeEntry?.activeProvider!!,
          createOptionInfo = uiState.activeEntry.activeEntryInfo as CreateOptionInfo,
          onOptionSelected = viewModel::onPrimaryCreateOptionInfoSelected,
          onConfirm = viewModel::onPrimaryCreateOptionInfoSelected,
          onOptionSelected = primaryEntryCallback,
          onConfirm = primaryEntryCallback,
          onCancel = viewModel::onCancel,
          multiProvider = uiState.enabledProviders.size > 1,
          onMoreOptionsSelected = viewModel::onMoreOptionsSelected
Loading