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

Commit ccb6f37c authored by Helen Qin's avatar Helen Qin
Browse files

Support onNewIntent().

This allows UI updates upon new request / entries.

Bug: 265037946
Bug: 267669563
Test: manual
Change-Id: I55ab941fe00e7b1e827a27d5b54eeafbe986c312
parent 7f9d899a
Loading
Loading
Loading
Loading
+55 −11
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ import android.credentials.ui.BaseDialogResult
import android.credentials.ui.ProviderPendingIntentResponse
import android.credentials.ui.UserSelectionDialogResult
import android.net.Uri
import android.os.IBinder
import android.os.Binder
import android.os.Bundle
import android.os.ResultReceiver
@@ -52,14 +53,16 @@ import java.time.Instant
class CredentialManagerRepo(
    private val context: Context,
    intent: Intent,
    userConfigRepo: UserConfigRepo,
) {
    val requestInfo: RequestInfo
    private val providerEnabledList: List<ProviderData>
    private val providerDisabledList: List<DisabledProviderData>?

    // TODO: require non-null.
    val resultReceiver: ResultReceiver?

    var initialUiState: UiState

    init {
        requestInfo = intent.extras?.getParcelable(
            RequestInfo.EXTRA_REQUEST_INFO,
@@ -93,6 +96,35 @@ class CredentialManagerRepo(
            Constants.EXTRA_RESULT_RECEIVER,
            ResultReceiver::class.java
        )

        initialUiState = when (requestInfo.type) {
            RequestInfo.TYPE_CREATE -> {
                val defaultProviderId = userConfigRepo.getDefaultProviderId()
                val isPasskeyFirstUse = userConfigRepo.getIsPasskeyFirstUse()
                val providerEnableListUiState = getCreateProviderEnableListInitialUiState()
                val providerDisableListUiState = getCreateProviderDisableListInitialUiState()
                val requestDisplayInfoUiState = getCreateRequestDisplayInfoInitialUiState()!!
                UiState(
                    createCredentialUiState = CreateFlowUtils.toCreateCredentialUiState(
                        providerEnableListUiState,
                        providerDisableListUiState,
                        defaultProviderId,
                        requestDisplayInfoUiState,
                        /** isOnPasskeyIntroStateAlready = */ false,
                        isPasskeyFirstUse)!!,
                    getCredentialUiState = null,
                )
            }
            RequestInfo.TYPE_GET -> UiState(
                createCredentialUiState = null,
                getCredentialUiState = getCredentialInitialUiState()!!,
            )
            else -> throw IllegalStateException("Unrecognized request type: ${requestInfo.type}")
        }
    }

    fun initState(): UiState {
        return initialUiState
    }

    // The dialog is canceled by the user.
@@ -110,9 +142,7 @@ class CredentialManagerRepo(
    }

    fun onCancel(cancelCode: Int) {
        val resultData = Bundle()
        BaseDialogResult.addToBundle(BaseDialogResult(requestInfo.token), resultData)
        resultReceiver?.send(cancelCode, resultData)
        sendCancellationCode(cancelCode, requestInfo.token, resultReceiver)
    }

    fun onOptionSelected(
@@ -129,15 +159,15 @@ class CredentialManagerRepo(
            entrySubkey,
            if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
        )
        val resultData = Bundle()
        UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultData)
        val resultDataBundle = Bundle()
        UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultDataBundle)
        resultReceiver?.send(
            BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION,
            resultData
            resultDataBundle
        )
    }

    fun getCredentialInitialUiState(): GetCredentialUiState? {
    private fun getCredentialInitialUiState(): GetCredentialUiState? {
        val providerEnabledList = GetFlowUtils.toProviderList(
            // TODO: handle runtime cast error
            providerEnabledList as List<GetCredentialProviderData>, context
@@ -149,7 +179,7 @@ class CredentialManagerRepo(
        )
    }

    fun getCreateProviderEnableListInitialUiState(): List<EnabledProviderInfo> {
    private fun getCreateProviderEnableListInitialUiState(): List<EnabledProviderInfo> {
        val providerEnabledList = CreateFlowUtils.toEnabledProviderList(
            // Handle runtime cast error
            providerEnabledList as List<CreateCredentialProviderData>, context
@@ -157,17 +187,31 @@ class CredentialManagerRepo(
        return providerEnabledList
    }

    fun getCreateProviderDisableListInitialUiState(): List<DisabledProviderInfo> {
    private fun getCreateProviderDisableListInitialUiState(): List<DisabledProviderInfo> {
        return CreateFlowUtils.toDisabledProviderList(
            // Handle runtime cast error
            providerDisabledList, context
        )
    }

    fun getCreateRequestDisplayInfoInitialUiState(): RequestDisplayInfo? {
    private fun getCreateRequestDisplayInfoInitialUiState(): RequestDisplayInfo? {
        return CreateFlowUtils.toRequestDisplayInfo(requestInfo, context)
    }

    companion object {
        fun sendCancellationCode(
            cancelCode: Int,
            requestToken: IBinder?,
            resultReceiver: ResultReceiver?
        ) {
            if (requestToken != null && resultReceiver != null) {
                val resultData = Bundle()
                BaseDialogResult.addToBundle(BaseDialogResult(requestToken), resultData)
                resultReceiver.send(cancelCode, resultData)
            }
        }
    }

    // TODO: below are prototype functionalities. To be removed for productionization.
    private fun testCreateCredentialEnabledProviderList(): List<CreateCredentialProviderData> {
        return listOf(
+60 −68
Original line number Diff line number Diff line
@@ -17,8 +17,10 @@
package com.android.credentialmanager

import android.content.Intent
import android.credentials.ui.BaseDialogResult
import android.credentials.ui.RequestInfo
import android.os.Bundle
import android.os.ResultReceiver
import android.provider.Settings
import android.util.Log
import androidx.activity.ComponentActivity
@@ -28,103 +30,76 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.runtime.Composable
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
import com.android.credentialmanager.createflow.CreateCredentialViewModel
import com.android.credentialmanager.getflow.GetCredentialScreen
import com.android.credentialmanager.getflow.GetCredentialViewModel
import com.android.credentialmanager.ui.theme.CredentialSelectorTheme

@ExperimentalMaterialApi
class CredentialSelectorActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val credManRepo = CredentialManagerRepo(this, intent)
        UserConfigRepo.setup(this)
        Log.d(Constants.LOG_TAG, "Creating new CredentialSelectorActivity")
        init(intent)
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        Log.d(Constants.LOG_TAG, "Existing activity received new intent")
        init(intent)
    }

    fun init(intent: Intent) {
        try {
            val userConfigRepo = UserConfigRepo(this)
            val credManRepo = CredentialManagerRepo(this, intent, userConfigRepo)
            setContent {
                CredentialSelectorTheme {
                    CredentialManagerBottomSheet(credManRepo.requestInfo.type, credManRepo)
                    CredentialManagerBottomSheet(
                        credManRepo,
                        userConfigRepo
                    )
                }
            }
        } catch (e: Exception) {
            Log.e(Constants.LOG_TAG, "Failed to show the credential selector", e)
            reportInstantiationErrorAndFinishActivity(credManRepo)
            onInitializationError(e, intent)
        }
    }

    @ExperimentalMaterialApi
    @Composable
    fun CredentialManagerBottomSheet(requestType: String, credManRepo: CredentialManagerRepo) {
        val providerActivityResult = remember { mutableStateOf<ProviderActivityResult?>(null) }
    fun CredentialManagerBottomSheet(
        credManRepo: CredentialManagerRepo,
        userConfigRepo: UserConfigRepo
    ) {
        val viewModel: CredentialSelectorViewModel = viewModel {
            CredentialSelectorViewModel(credManRepo, userConfigRepo)
        }
        val launcher = rememberLauncherForActivityResult(
            ActivityResultContracts.StartIntentSenderForResult()
        ) {
            providerActivityResult.value = ProviderActivityResult(it.resultCode, it.data)
        }
        when (requestType) {
            RequestInfo.TYPE_CREATE -> {
                val viewModel: CreateCredentialViewModel = viewModel {
                    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
                    }
            viewModel.onProviderActivityResult(ProviderActivityResult(it.resultCode, it.data))
        }
        LaunchedEffect(viewModel.uiState.dialogState) {
            handleDialogState(viewModel.uiState.dialogState)
        }
                providerActivityResult.value?.let {
                    viewModel.onProviderActivityResult(it)
                    providerActivityResult.value = null
                }

        if (viewModel.uiState.createCredentialUiState != null) {
            CreateCredentialScreen(
                viewModel = viewModel,
                providerActivityLauncher = launcher
            )
            }
            RequestInfo.TYPE_GET -> {
                val viewModel: GetCredentialViewModel = viewModel {
                    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)
                }
                providerActivityResult.value?.let {
                    viewModel.onProviderActivityResult(it)
                    providerActivityResult.value = null
                }
        } else if (viewModel.uiState.getCredentialUiState != null) {
            GetCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
            }
            else -> {
                Log.d(Constants.LOG_TAG, "Unknown type, not rendering any UI")
        } else {
            Log.d(Constants.LOG_TAG, "UI wasn't able to render neither get nor create flow")
            reportInstantiationErrorAndFinishActivity(credManRepo)
        }
    }
    }

    private fun reportInstantiationErrorAndFinishActivity(credManRepo: CredentialManagerRepo) {
        Log.w(Constants.LOG_TAG, "Finishing the activity due to instantiation failure.")
@@ -142,4 +117,21 @@ class CredentialSelectorActivity : ComponentActivity() {
            this@CredentialSelectorActivity.finish()
        }
    }

    private fun onInitializationError(e: Exception, intent: Intent) {
        Log.e(Constants.LOG_TAG, "Failed to show the credential selector", e)
        val resultReceiver = intent.getParcelableExtra(
            android.credentials.ui.Constants.EXTRA_RESULT_RECEIVER,
            ResultReceiver::class.java
        )
        val requestInfo = intent.extras?.getParcelable(
            RequestInfo.EXTRA_REQUEST_INFO,
            RequestInfo::class.java
        )
        CredentialManagerRepo.sendCancellationCode(
            BaseDialogResult.RESULT_CODE_DATA_PARSING_FAILURE,
            requestInfo?.token, resultReceiver
        )
        this.finish()
    }
}
+340 −0

File changed and moved.

Preview size limit exceeded, changes collapsed.

+20 −23
Original line number Diff line number Diff line
@@ -261,22 +261,22 @@ class GetFlowUtils {
            providerIcon: Drawable,
            authEntryList: List<Entry>,
        ): List<AuthenticationEntryInfo> {
            if (authEntryList.isEmpty()) {
                return listOf()
      }
            val authEntry = authEntryList[0]
            val result: MutableList<AuthenticationEntryInfo> = mutableListOf()
            authEntryList.forEach {
                val structuredAuthEntry =
                AuthenticationAction.fromSlice(authEntry.slice) ?: return listOf()
            return listOf(AuthenticationEntryInfo(
                    AuthenticationAction.fromSlice(it.slice) ?: return@forEach
                result.add(AuthenticationEntryInfo(
                    providerId = providerId,
                entryKey = authEntry.key,
                entrySubkey = authEntry.subkey,
                    entryKey = it.key,
                    entrySubkey = it.subkey,
                    pendingIntent = structuredAuthEntry.pendingIntent,
                fillInIntent = authEntry.frameworkExtrasIntent,
                    fillInIntent = it.frameworkExtrasIntent,
                    title = providerDisplayName,
                    icon = providerIcon,
                ))
            }
            return result
        }

        private fun getRemoteEntry(providerId: String, remoteEntry: Entry?): RemoteEntryInfo? {
            // TODO: should also call fromSlice after getting the official jetpack code.
@@ -459,10 +459,7 @@ class CreateFlowUtils {
                /*requestDisplayInfo=*/requestDisplayInfo,
                /*defaultProvider=*/defaultProvider, /*remoteEntry=*/remoteEntry,
                /*isPasskeyFirstUse=*/isPasskeyFirstUse
            )
            if (initialScreenState == null) {
                return null
            }
            ) ?: return null
            return CreateCredentialUiState(
                enabledProviders = enabledProviders,
                disabledProviders = disabledProviders,
+0 −12
Original line number Diff line number Diff line
@@ -50,21 +50,9 @@ class UserConfigRepo(context: Context) {
    }

    companion object {
        lateinit var repo: UserConfigRepo

        const val DEFAULT_PROVIDER = "default_provider"
        // This first use value only applies to passkeys, not related with if generally
        // credential manager is first use or not
        const val IS_PASSKEY_FIRST_USE = "is_passkey_first_use"

        fun setup(
            context: Context,
        ) {
            repo = UserConfigRepo(context)
        }

        fun getInstance(): UserConfigRepo {
            return repo
        }
    }
}
Loading