Implement MVP for Single Provider - Single Password flow for get credential request.
https://screencast.googleplex.com/cast/NjUwOTM5ODQ4MTQzNjY3MnwyNTQ3Nzk4NS03Mg
Bug: 301206470
Test: N/A - implementing MVP code
Change-Id: If2b6880656f620c0859138551144b830e3233c46
diff --git a/packages/CredentialManager/shared/Android.bp b/packages/CredentialManager/shared/Android.bp
index 38d98a9..0d4af2a 100644
--- a/packages/CredentialManager/shared/Android.bp
+++ b/packages/CredentialManager/shared/Android.bp
@@ -12,6 +12,7 @@
manifest: "AndroidManifest.xml",
srcs: ["src/**/*.kt"],
static_libs: [
+ "androidx.activity_activity-compose",
"androidx.core_core-ktx",
"androidx.credentials_credentials",
"guava",
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ApiConstants.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ApiConstants.kt
new file mode 100644
index 0000000..6498ff7
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ApiConstants.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.0N
+ *
+ * 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
+
+const val IS_AUTO_SELECTED_KEY = "IS_AUTO_SELECTED"
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt
index defba8d..8986e52 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt
@@ -17,14 +17,25 @@
package com.android.credentialmanager
import android.content.Intent
+import android.content.pm.PackageManager
import android.credentials.ui.RequestInfo
import com.android.credentialmanager.ktx.requestInfo
import com.android.credentialmanager.mapper.toGet
import com.android.credentialmanager.mapper.toRequestCancel
+import com.android.credentialmanager.mapper.toRequestClose
import com.android.credentialmanager.model.Request
-fun Intent.parse(): Request {
- this.toRequestCancel()?.let { return it }
+fun Intent.parse(
+ packageManager: PackageManager,
+ previousIntent: Intent? = null,
+): Request {
+ this.toRequestClose(packageManager, previousIntent)?.let { closeRequest ->
+ return closeRequest
+ }
+
+ this.toRequestCancel(packageManager)?.let { cancelRequest ->
+ return cancelRequest
+ }
return when (requestInfo?.type) {
RequestInfo.TYPE_CREATE -> {
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/activity/StartBalIntentSenderForResultContract.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/activity/StartBalIntentSenderForResultContract.kt
new file mode 100644
index 0000000..ef083fd
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/activity/StartBalIntentSenderForResultContract.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.activity
+
+import android.app.ActivityOptions
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.IntentSenderRequest
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.activity.result.contract.ActivityResultContracts
+
+/**
+ * A custom StartIntentSenderForResult contract implementation that attaches an [ActivityOptions]
+ * that opts in for background activity launch.
+ */
+class StartBalIntentSenderForResultContract :
+ ActivityResultContract<IntentSenderRequest, ActivityResult>() {
+ override fun createIntent(context: Context, input: IntentSenderRequest): Intent {
+ val activityOptionBundle =
+ ActivityOptions.makeBasic().setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+ ).toBundle()
+ return Intent(
+ ActivityResultContracts.StartIntentSenderForResult.ACTION_INTENT_SENDER_REQUEST
+ ).putExtra(
+ ActivityResultContracts.StartActivityForResult.EXTRA_ACTIVITY_OPTIONS_BUNDLE,
+ activityOptionBundle
+ ).putExtra(
+ ActivityResultContracts.StartIntentSenderForResult.EXTRA_INTENT_SENDER_REQUEST,
+ input
+ )
+ }
+
+ override fun parseResult(
+ resultCode: Int,
+ intent: Intent?
+ ): ActivityResult = ActivityResult(resultCode, intent)
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt
index a4c20bf..4533db6 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt
@@ -18,10 +18,12 @@
import android.content.Intent
import android.credentials.ui.CancelUiRequest
+import android.credentials.ui.Constants
import android.credentials.ui.CreateCredentialProviderData
import android.credentials.ui.GetCredentialProviderData
import android.credentials.ui.ProviderData
import android.credentials.ui.RequestInfo
+import android.os.ResultReceiver
val Intent.cancelUiRequest: CancelUiRequest?
get() = this.extras?.getParcelable(
@@ -46,3 +48,9 @@
ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
CreateCredentialProviderData::class.java
) ?: emptyList()
+
+val Intent.resultReceiver: ResultReceiver?
+ get() = this.getParcelableExtra(
+ Constants.EXTRA_RESULT_RECEIVER,
+ ResultReceiver::class.java
+ )
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestCancelMapper.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestCancelMapper.kt
index 86a6d23..555a86f 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestCancelMapper.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestCancelMapper.kt
@@ -17,13 +17,23 @@
package com.android.credentialmanager.mapper
import android.content.Intent
+import android.content.pm.PackageManager
+import android.util.Log
+import com.android.credentialmanager.TAG
+import com.android.credentialmanager.ktx.appLabel
import com.android.credentialmanager.ktx.cancelUiRequest
import com.android.credentialmanager.model.Request
-fun Intent.toRequestCancel(): Request.Cancel? =
+fun Intent.toRequestCancel(packageManager: PackageManager): Request.Cancel? =
this.cancelUiRequest?.let { cancelUiRequest ->
- Request.Cancel(
- showCancellationUi = cancelUiRequest.shouldShowCancellationUi(),
- appPackageName = cancelUiRequest.appPackageName
- )
+ val appLabel = packageManager.appLabel(cancelUiRequest.appPackageName)
+ if (appLabel == null) {
+ Log.d(TAG, "Received UI cancel request with an invalid package name.")
+ null
+ } else {
+ Request.Cancel(
+ showCancellationUi = cancelUiRequest.shouldShowCancellationUi(),
+ appName = appLabel
+ )
+ }
}
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestCloseMapper.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestCloseMapper.kt
new file mode 100644
index 0000000..6de3e7d
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestCloseMapper.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.0N
+ *
+ * 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.mapper
+
+import android.content.Intent
+import android.content.pm.PackageManager
+import com.android.credentialmanager.ktx.requestInfo
+import com.android.credentialmanager.model.Request
+
+fun Intent.toRequestClose(
+ packageManager: PackageManager,
+ previousIntent: Intent? = null,
+): Request.Close? {
+ // Close request comes as "Cancel" request from Credential Manager API
+ val currentRequest = toRequestCancel(packageManager = packageManager) ?: return null
+
+ if (currentRequest.showCancellationUi) {
+ // Current request is to Cancel and not to Close
+ return null
+ }
+
+ previousIntent?.let {
+ val previousToken = previousIntent.requestInfo?.token
+ val currentToken = this.requestInfo?.token
+
+ if (previousToken != currentToken) {
+ // Current cancellation is for a different request, don't close the current flow.
+ return null
+ }
+ }
+
+ return Request.Close
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestGetMapper.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestGetMapper.kt
index ed9d563..ee45fbb 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestGetMapper.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestGetMapper.kt
@@ -1,22 +1,66 @@
+/*
+ * 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.0N
+ *
+ * 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.mapper
import android.content.Intent
+import android.credentials.ui.Entry
import android.credentials.ui.GetCredentialProviderData
+import androidx.credentials.provider.PasswordCredentialEntry
+import com.android.credentialmanager.factory.fromSlice
import com.android.credentialmanager.ktx.getCredentialProviderDataList
+import com.android.credentialmanager.ktx.requestInfo
+import com.android.credentialmanager.ktx.resultReceiver
+import com.android.credentialmanager.model.Password
import com.android.credentialmanager.model.Request
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
-fun Intent.toGet() = Request.Get(
- providers = ImmutableMap.copyOf(
- getCredentialProviderDataList.associateBy { it.providerFlattenedComponentName }
- ),
- entries = ImmutableList.copyOf(
- getCredentialProviderDataList.map { providerData ->
- check(providerData is GetCredentialProviderData) {
- "Invalid provider data type for GetCredentialRequest"
+fun Intent.toGet(): Request.Get {
+ val credentialEntries = mutableListOf<Pair<String, Entry>>()
+ for (providerData in getCredentialProviderDataList) {
+ if (providerData is GetCredentialProviderData) {
+ for (credentialEntry in providerData.credentialEntries) {
+ credentialEntries.add(
+ Pair(providerData.providerFlattenedComponentName, credentialEntry)
+ )
}
- providerData
- }.flatMap { it.credentialEntries }
+ }
+ }
+
+ val passwordEntries = mutableListOf<Password>()
+ for ((providerId, entry) in credentialEntries) {
+ val slice = fromSlice(entry.slice)
+ if (slice is PasswordCredentialEntry) {
+ passwordEntries.add(
+ Password(
+ providerId = providerId,
+ entry = entry,
+ passwordCredentialEntry = slice
+ )
+ )
+ }
+ }
+
+ return Request.Get(
+ token = requestInfo?.token,
+ resultReceiver = this.resultReceiver,
+ providers = ImmutableMap.copyOf(
+ getCredentialProviderDataList.associateBy { it.providerFlattenedComponentName }
+ ),
+ passwordEntries = ImmutableList.copyOf(passwordEntries)
)
-)
\ No newline at end of file
+}
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Password.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Password.kt
new file mode 100644
index 0000000..2fe4fd5
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Password.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.0N
+ *
+ * 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.model
+
+import android.credentials.ui.Entry
+import androidx.credentials.provider.PasswordCredentialEntry
+
+data class Password(
+ val providerId: String,
+ val entry: Entry,
+ val passwordCredentialEntry: PasswordCredentialEntry,
+)
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Request.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Request.kt
index bc07310..6011a1c 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Request.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Request.kt
@@ -16,8 +16,9 @@
package com.android.credentialmanager.model
-import android.credentials.ui.Entry
import android.credentials.ui.ProviderData
+import android.os.IBinder
+import android.os.ResultReceiver
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
@@ -25,15 +26,33 @@
* Represents the request made by the CredentialManager API.
*/
sealed class Request {
+
+ /**
+ * Request to close the app without displaying a message to the user and without reporting
+ * anything back to the Credential Manager service.
+ */
+ data object Close : Request()
+
+ /**
+ * Request to close the app, displaying a message to the user.
+ */
data class Cancel(
val showCancellationUi: Boolean,
- val appPackageName: String?
+ val appName: String
) : Request()
+ /**
+ * Request to start the get credentials flow.
+ */
data class Get(
+ val token: IBinder?,
+ val resultReceiver: ResultReceiver?,
val providers: ImmutableMap<String, ProviderData>,
- val entries: ImmutableList<Entry>,
+ val passwordEntries: ImmutableList<Password>,
) : Request()
+ /**
+ * Request to start the create credentials flow.
+ */
data object Create : Request()
}
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/repository/RequestRepository.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/repository/RequestRepository.kt
new file mode 100644
index 0000000..5ab5ab9
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/repository/RequestRepository.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.0N
+ *
+ * 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.repository
+
+import android.app.Application
+import android.content.Intent
+import android.util.Log
+import com.android.credentialmanager.TAG
+import com.android.credentialmanager.model.Request
+import com.android.credentialmanager.parse
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class RequestRepository(
+ private val application: Application,
+) {
+
+ private val _requests = MutableStateFlow<Request?>(null)
+ val requests: StateFlow<Request?> = _requests
+
+ suspend fun processRequest(intent: Intent, previousIntent: Intent? = null) {
+ val request = intent.parse(
+ packageManager = application.packageManager,
+ previousIntent = previousIntent
+ )
+
+ Log.d(TAG, "Request parsed: $request")
+
+ _requests.value = request
+ }
+}