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
+ }
+}
diff --git a/packages/CredentialManager/wear/Android.bp b/packages/CredentialManager/wear/Android.bp
index c0dff16..e5f5cc2 100644
--- a/packages/CredentialManager/wear/Android.bp
+++ b/packages/CredentialManager/wear/Android.bp
@@ -37,6 +37,7 @@
"androidx.lifecycle_lifecycle-extensions",
"androidx.lifecycle_lifecycle-livedata",
"androidx.lifecycle_lifecycle-runtime-ktx",
+ "androidx.lifecycle_lifecycle-runtime-compose",
"androidx.lifecycle_lifecycle-viewmodel-compose",
"androidx.wear.compose_compose-foundation",
"androidx.wear.compose_compose-material",
diff --git a/packages/CredentialManager/wear/AndroidManifest.xml b/packages/CredentialManager/wear/AndroidManifest.xml
index 90248734..b480ac3 100644
--- a/packages/CredentialManager/wear/AndroidManifest.xml
+++ b/packages/CredentialManager/wear/AndroidManifest.xml
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
+
<!--
/*
* Copyright (c) 2023 Google Inc.
@@ -21,25 +22,27 @@
<uses-feature android:name="android.hardware.type.watch" />
- <uses-permission android:name="android.permission.LAUNCH_CREDENTIAL_SELECTOR"/>
- <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
- <uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS"/>
+ <uses-permission android:name="android.permission.LAUNCH_CREDENTIAL_SELECTOR" />
+ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+ <uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS" />
<application
- android:allowBackup="true"
- android:dataExtractionRules="@xml/data_extraction_rules"
- android:fullBackupContent="@xml/backup_rules"
- android:label="@string/app_name"
- android:supportsRtl="true">
+ android:name=".CredentialSelectorApp"
+ android:allowBackup="true"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ android:fullBackupContent="@xml/backup_rules"
+ android:label="@string/app_name"
+ android:supportsRtl="true">
+ <!-- Activity called by GMS has to be exactly:
+ com.android.credentialmanager.CredentialSelectorActivity -->
<activity
- android:name=".ui.CredentialSelectorActivity"
+ android:name=".CredentialSelectorActivity"
+ android:excludeFromRecents="true"
android:exported="true"
- android:permission="android.permission.LAUNCH_CREDENTIAL_SELECTOR"
- android:launchMode="singleTop"
android:label="@string/app_name"
- android:excludeFromRecents="true">
- </activity>
- </application>
+ android:launchMode="singleTop"
+ android:permission="android.permission.LAUNCH_CREDENTIAL_SELECTOR" />
+ </application>
</manifest>
diff --git a/packages/CredentialManager/wear/res/values/themes.xml b/packages/CredentialManager/wear/res/values/themes.xml
deleted file mode 100644
index 22329e9f..0000000
--- a/packages/CredentialManager/wear/res/values/themes.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ 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.
- -->
-<resources>
- <style name="Theme.CredentialSelector" parent="@*android:style/ThemeOverlay.DeviceDefault.Accent.DayNight">
- <item name="android:windowContentOverlay">@null</item>
- <item name="android:windowNoTitle">true</item>
- <item name="android:windowBackground">@android:color/transparent</item>
- <item name="android:windowIsTranslucent">true</item>
- </style>
-</resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt
new file mode 100644
index 0000000..273d0b1
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -0,0 +1,100 @@
+/*
+ * 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
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.wear.compose.material.MaterialTheme
+import com.android.credentialmanager.ui.WearApp
+import com.android.credentialmanager.ui.screens.single.password.SinglePasswordScreen
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.compose.layout.belowTimeTextPreview
+import kotlinx.coroutines.launch
+
+class CredentialSelectorActivity : ComponentActivity() {
+
+ private val viewModel: CredentialSelectorViewModel by viewModels {
+ CredentialSelectorViewModel.Factory
+ }
+
+ @OptIn(ExperimentalHorologistApi::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setTheme(android.R.style.Theme_DeviceDefault)
+
+ // TODO: b/301027810 due to this issue with compose in Main platform, we are implementing a
+ // workaround. Once the issue is fixed, remove the "else" bracket and leave only the
+ // contents of the "if" bracket.
+ if (false) {
+ setContent {
+ MaterialTheme {
+ WearApp(
+ viewModel = viewModel,
+ onCloseApp = ::finish,
+ )
+ }
+ }
+ } else {
+ // TODO: b/301027810 Remove the content of this "else" bracket fully once issue is fixed
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.uiState.collect { uiState ->
+ when (uiState) {
+ CredentialSelectorUiState.Idle -> {
+ // Don't display anything, assuming that there should be minimal latency
+ // to parse the Credential Manager intent and define the state of the
+ // app. If latency is big, then a "loading" screen should be displayed
+ // to the user.
+ }
+
+ is CredentialSelectorUiState.Get -> {
+ setContent {
+ MaterialTheme {
+ SinglePasswordScreen(
+ columnState = belowTimeTextPreview(),
+ onCloseApp = ::finish,
+ )
+ }
+ }
+ }
+
+ else -> finish()
+ }
+ }
+ }
+ }
+ }
+
+ viewModel.onNewIntent(intent)
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+
+ val previousIntent = getIntent()
+ setIntent(intent)
+
+ viewModel.onNewIntent(intent, previousIntent)
+ }
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorApp.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorApp.kt
new file mode 100644
index 0000000..7c81fd0
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorApp.kt
@@ -0,0 +1,32 @@
+/*
+ * 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
+
+import android.app.Application
+import com.android.credentialmanager.di.inject
+import com.android.credentialmanager.repository.RequestRepository
+
+class CredentialSelectorApp : Application() {
+
+ lateinit var requestRepository: RequestRepository
+
+ override fun onCreate() {
+ super.onCreate()
+
+ inject()
+ }
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
new file mode 100644
index 0000000..d557dc0
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
@@ -0,0 +1,89 @@
+/*
+ * 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
+
+import android.content.Intent
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.CreationExtras
+import com.android.credentialmanager.model.Request
+import com.android.credentialmanager.repository.RequestRepository
+import com.android.credentialmanager.ui.mappers.toGet
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+class CredentialSelectorViewModel(
+ private val requestRepository: RequestRepository,
+) : ViewModel() {
+
+ val uiState: StateFlow<CredentialSelectorUiState> = requestRepository.requests
+ .map { request ->
+ when (request) {
+ null -> CredentialSelectorUiState.Idle
+ is Request.Cancel -> CredentialSelectorUiState.Cancel(request.appName)
+ Request.Close -> CredentialSelectorUiState.Close
+ Request.Create -> CredentialSelectorUiState.Create
+ is Request.Get -> request.toGet()
+ }
+ }
+ .stateIn(
+ viewModelScope,
+ started = SharingStarted.WhileSubscribed(5000),
+ initialValue = CredentialSelectorUiState.Idle,
+ )
+
+ fun onNewIntent(intent: Intent, previousIntent: Intent? = null) {
+ viewModelScope.launch {
+ requestRepository.processRequest(intent = intent, previousIntent = previousIntent)
+ }
+ }
+
+ companion object {
+ val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : ViewModel> create(
+ modelClass: Class<T>,
+ extras: CreationExtras
+ ): T {
+ val application = checkNotNull(extras[APPLICATION_KEY])
+
+ return CredentialSelectorViewModel(
+ requestRepository = (application as CredentialSelectorApp).requestRepository,
+ ) as T
+ }
+ }
+ }
+}
+
+sealed class CredentialSelectorUiState {
+ data object Idle : CredentialSelectorUiState()
+ sealed class Get : CredentialSelectorUiState() {
+ data object SingleProviderSinglePasskey : Get()
+ data object SingleProviderSinglePassword : Get()
+
+ // TODO: b/301206470 add the remaining states
+ }
+
+ data object Create : CredentialSelectorUiState()
+ data class Cancel(val appName: String) : CredentialSelectorUiState()
+ data object Close : CredentialSelectorUiState()
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/di/DI.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/di/DI.kt
new file mode 100644
index 0000000..a11017b
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/di/DI.kt
@@ -0,0 +1,17 @@
+package com.android.credentialmanager.di
+
+import android.app.Application
+import com.android.credentialmanager.CredentialSelectorApp
+import com.android.credentialmanager.repository.RequestRepository
+
+// TODO b/301601582 add Hilt for dependency injection
+
+fun CredentialSelectorApp.inject() {
+ requestRepository = requestRepository(application = this)
+}
+
+private fun requestRepository(
+ application: Application,
+): RequestRepository = RequestRepository(
+ application = application,
+)
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorActivity.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorActivity.kt
deleted file mode 100644
index 53122ba..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorActivity.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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.ui
-
-import android.content.Intent
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.activity.viewModels
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import androidx.wear.compose.material.MaterialTheme
-import kotlinx.coroutines.launch
-
-class CredentialSelectorActivity : ComponentActivity() {
-
- private val viewModel: CredentialSelectorViewModel by viewModels()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setTheme(android.R.style.Theme_DeviceDefault)
-
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.uiState.collect { uiState ->
- when (uiState) {
- CredentialSelectorUiState.Idle -> {
- // Don't display anything, assuming that there should be minimal latency
- // to parse the Credential Manager intent and define the state of the
- // app. If latency is big, then a "loading" screen should be displayed
- // to the user.
- }
-
- is CredentialSelectorUiState.Get -> {
- setContent {
- MaterialTheme {
- WearApp()
- }
- }
- }
-
- CredentialSelectorUiState.Create -> {
- // TODO: b/301206624 - Implement create flow
- finish()
- }
-
- is CredentialSelectorUiState.Cancel -> {
- // TODO: b/300422310 - Implement cancel with message flow
- finish()
- }
-
- CredentialSelectorUiState.Finish -> {
- finish()
- }
- }
- }
- }
- }
-
- viewModel.onNewIntent(intent)
- }
-
- override fun onNewIntent(intent: Intent) {
- super.onNewIntent(intent)
-
- val previousIntent = getIntent()
- setIntent(intent)
-
- viewModel.onNewIntent(intent, previousIntent)
- }
-}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorViewModel.kt
deleted file mode 100644
index d22d5d1..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorViewModel.kt
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * 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.ui
-
-import android.app.Application
-import android.content.Intent
-import android.util.Log
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.viewModelScope
-import com.android.credentialmanager.TAG
-import com.android.credentialmanager.parse
-import com.android.credentialmanager.ktx.appLabel
-import com.android.credentialmanager.ktx.requestInfo
-import com.android.credentialmanager.mapper.toGet
-import com.android.credentialmanager.ui.model.PasskeyUiModel
-import com.android.credentialmanager.ui.model.PasswordUiModel
-import com.android.credentialmanager.model.Request
-import com.android.credentialmanager.ui.mapper.toGet
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.launch
-
-class CredentialSelectorViewModel(
- private val application: Application
-) : AndroidViewModel(application = application) {
-
- private val _uiState =
- MutableStateFlow<CredentialSelectorUiState>(CredentialSelectorUiState.Idle)
- val uiState: StateFlow<CredentialSelectorUiState> = _uiState
-
- fun onNewIntent(intent: Intent, previousIntent: Intent? = null) {
- viewModelScope.launch {
- val request = intent.parse()
- if (shouldFinishActivity(request = request, previousIntent = previousIntent)) {
- _uiState.value = CredentialSelectorUiState.Finish
- } else {
- when (request) {
- is Request.Cancel -> {
- request.appPackageName?.let { appPackageName ->
- application.packageManager.appLabel(appPackageName)?.let { appLabel ->
- _uiState.value = CredentialSelectorUiState.Cancel(appLabel)
- } ?: run {
- Log.d(TAG,
- "Received UI cancel request with an invalid package name.")
- _uiState.value = CredentialSelectorUiState.Finish
- }
- } ?: run {
- Log.d(TAG, "Received UI cancel request with an invalid package name.")
- _uiState.value = CredentialSelectorUiState.Finish
- }
- }
-
- Request.Create -> {
- _uiState.value = CredentialSelectorUiState.Create
- }
-
- is Request.Get -> {
- _uiState.value = request.toGet()
- }
- }
- }
- }
- }
-
- /**
- * Check if backend requested the UI activity to be cancelled. Different from the other
- * finishing flows, this one does not report anything back to the Credential Manager service
- * backend.
- */
- private fun shouldFinishActivity(request: Request, previousIntent: Intent? = null): Boolean {
- if (request !is Request.Cancel) {
- return false
- } else {
- Log.d(
- TAG, "Received UI cancellation intent. Should show cancellation" +
- " ui = ${request.showCancellationUi}")
-
- previousIntent?.let {
- val previousUiRequest = previousIntent.parse()
-
- if (previousUiRequest is Request.Cancel) {
- val previousToken = previousIntent.requestInfo?.token
- val currentToken = previousIntent.requestInfo?.token
-
- if (previousToken != currentToken) {
- // Cancellation was for a different request, don't cancel the current UI.
- return false
- }
- }
- }
-
- return !request.showCancellationUi
- }
- }
-}
-
-sealed class CredentialSelectorUiState {
- data object Idle : CredentialSelectorUiState()
- sealed class Get : CredentialSelectorUiState() {
- data class SingleProviderSinglePasskey(val passkeyUiModel: PasskeyUiModel) : Get()
- data class SingleProviderSinglePassword(val passwordUiModel: PasswordUiModel) : Get()
-
- // TODO: b/301206470 add the remaining states
- }
-
- data object Create : CredentialSelectorUiState()
- data class Cancel(val appName: String) : CredentialSelectorUiState()
- data object Finish : CredentialSelectorUiState()
-}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/Navigation.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/Navigation.kt
new file mode 100644
index 0000000..da5697d
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/Navigation.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.ui
+
+import androidx.navigation.NavController
+
+fun NavController.navigateToLoading() {
+ navigate(Screen.Loading.route)
+}
+
+fun NavController.navigateToSinglePasswordScreen() {
+ navigate(Screen.SinglePasswordScreen.route)
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/Screen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/Screen.kt
index 7d1a49b..c3919a0 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/Screen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/Screen.kt
@@ -19,5 +19,7 @@
sealed class Screen(
val route: String,
) {
- data object Main : Screen("main")
+ data object Loading : Screen("loading")
+
+ data object SinglePasswordScreen : Screen("singlePasswordScreen")
}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
index 19ea9ed..7e0ea30 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
@@ -19,28 +19,94 @@
package com.android.credentialmanager.ui
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.navigation.NavController
import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState
-import com.android.credentialmanager.ui.screens.MainScreen
+import com.android.credentialmanager.CredentialSelectorUiState
+import com.android.credentialmanager.CredentialSelectorViewModel
+import com.android.credentialmanager.ui.screens.LoadingScreen
+import com.android.credentialmanager.ui.screens.single.password.SinglePasswordScreen
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.navscaffold.WearNavScaffold
import com.google.android.horologist.compose.navscaffold.composable
+import com.google.android.horologist.compose.navscaffold.scrollable
@Composable
-fun WearApp() {
+fun WearApp(
+ viewModel: CredentialSelectorViewModel,
+ onCloseApp: () -> Unit,
+) {
val navController = rememberSwipeDismissableNavController()
val swipeToDismissBoxState = rememberSwipeToDismissBoxState()
val navHostState =
rememberSwipeDismissableNavHostState(swipeToDismissBoxState = swipeToDismissBoxState)
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
WearNavScaffold(
- startDestination = Screen.Main.route,
+ startDestination = Screen.Loading.route,
navController = navController,
state = navHostState,
) {
- composable(Screen.Main.route) {
- MainScreen()
+ composable(Screen.Loading.route) {
+ LoadingScreen()
+ }
+
+ scrollable(Screen.SinglePasswordScreen.route) {
+ SinglePasswordScreen(
+ columnState = it.columnState,
+ onCloseApp = onCloseApp,
+ )
+ }
+ }
+
+ when (val state = uiState) {
+ CredentialSelectorUiState.Idle -> {
+ if (navController.currentDestination?.route != Screen.Loading.route) {
+ navController.navigateToLoading()
+ }
+ }
+
+ is CredentialSelectorUiState.Get -> {
+ handleGetNavigation(
+ navController = navController,
+ state = state,
+ onCloseApp = onCloseApp,
+ )
+ }
+
+ CredentialSelectorUiState.Create -> {
+ // TODO: b/301206624 - Implement create flow
+ onCloseApp()
+ }
+
+ is CredentialSelectorUiState.Cancel -> {
+ // TODO: b/300422310 - Implement cancel with message flow
+ onCloseApp()
+ }
+
+ CredentialSelectorUiState.Close -> {
+ onCloseApp()
+ }
+ }
+}
+
+private fun handleGetNavigation(
+ navController: NavController,
+ state: CredentialSelectorUiState.Get,
+ onCloseApp: () -> Unit,
+) {
+ when (state) {
+ is CredentialSelectorUiState.Get.SingleProviderSinglePassword -> {
+ navController.navigateToSinglePasswordScreen()
+ }
+
+ else -> {
+ // TODO: b/301206470 - Implement other get flows
+ onCloseApp()
}
}
}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mapper/CredentialSelectorUiStateGetMapper.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mapper/CredentialSelectorUiStateGetMapper.kt
deleted file mode 100644
index 5ceec178..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mapper/CredentialSelectorUiStateGetMapper.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.android.credentialmanager.ui.mapper
-
-import androidx.credentials.provider.CustomCredentialEntry
-import androidx.credentials.provider.PasswordCredentialEntry
-import androidx.credentials.provider.PublicKeyCredentialEntry
-import com.android.credentialmanager.ui.CredentialSelectorUiState
-import com.android.credentialmanager.factory.fromSlice
-import com.android.credentialmanager.ui.model.PasswordUiModel
-import com.android.credentialmanager.model.Request
-
-fun Request.Get.toGet(): CredentialSelectorUiState.Get {
- if (this.providers.isEmpty()) {
- throw IllegalStateException("Invalid GetCredential request with empty list of providers.")
- }
-
- if (this.entries.isEmpty()) {
- throw IllegalStateException("Invalid GetCredential request with empty list of entries.")
- }
-
- if (this.providers.size == 1) {
- if (this.entries.size == 1) {
- val slice = this.entries.first().slice
- when (val credentialEntry = fromSlice(slice)) {
- is PasswordCredentialEntry -> {
- return CredentialSelectorUiState.Get.SingleProviderSinglePassword(
- PasswordUiModel(credentialEntry.displayName.toString())
- )
- }
-
- is PublicKeyCredentialEntry -> {
- TODO("b/301206470 - to be implemented")
- }
-
- is CustomCredentialEntry -> {
- TODO("b/301206470 - to be implemented")
- }
-
- else -> {
- throw IllegalStateException(
- "Encountered unrecognized credential entry (${slice.spec?.type}) for " +
- "GetCredential request with single account"
- )
- }
- }
- } else {
- TODO("b/301206470 - to be implemented")
- }
- } else {
- TODO("b/301206470 - to be implemented")
- }
-}
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt
new file mode 100644
index 0000000..f2f878e
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.ui.mappers
+
+import com.android.credentialmanager.model.Request
+import com.android.credentialmanager.CredentialSelectorUiState
+
+fun Request.Get.toGet(): CredentialSelectorUiState.Get {
+ // TODO: b/301206470 returning a hard coded state for MVP
+ if (true) return CredentialSelectorUiState.Get.SingleProviderSinglePassword
+
+ return if (providers.size == 1) {
+ if (passwordEntries.size == 1) {
+ CredentialSelectorUiState.Get.SingleProviderSinglePassword
+ } else {
+ TODO() // b/301206470 - Implement other get flows
+ }
+ } else {
+ TODO() // b/301206470 - Implement other get flows
+ }
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/MainScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/LoadingScreen.kt
similarity index 74%
rename from packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/MainScreen.kt
rename to packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/LoadingScreen.kt
index 94a671e..b3ab0c4 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/MainScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/LoadingScreen.kt
@@ -16,17 +16,15 @@
package com.android.credentialmanager.ui.screens
-import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.wear.compose.material.Text
@Composable
-fun MainScreen(
+fun LoadingScreen(
modifier: Modifier = Modifier
) {
- Box(modifier = modifier, contentAlignment = Alignment.Center) {
- Text("This is a placeholder for the main screen.")
- }
+ // Don't display anything, assuming that there should be minimal latency
+ // to parse the Credential Manager intent and define the state of the
+ // app. If latency is big, then a "loading" screen should be displayed
+ // to the user.
}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasswordScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasswordScreen.kt
deleted file mode 100644
index d863d3c..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasswordScreen.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 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
- *
- * https://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.
- */
-
-@file:OptIn(ExperimentalHorologistApi::class)
-
-package com.android.credentialmanager.ui.screens
-
-import androidx.compose.foundation.layout.padding
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import com.android.credentialmanager.R
-import com.android.credentialmanager.ui.components.DialogButtonsRow
-import com.android.credentialmanager.ui.components.PasswordRow
-import com.android.credentialmanager.ui.components.SignInHeader
-import com.google.android.horologist.annotations.ExperimentalHorologistApi
-import com.google.android.horologist.compose.layout.ScalingLazyColumnState
-import com.google.android.horologist.compose.layout.belowTimeTextPreview
-import com.google.android.horologist.compose.tools.WearPreview
-
-@Composable
-fun SinglePasswordScreen(
- email: String,
- onCancelClick: () -> Unit,
- onOKClick: () -> Unit,
- columnState: ScalingLazyColumnState,
- modifier: Modifier = Modifier,
-) {
- SingleAccountScreen(
- headerContent = {
- SignInHeader(
- icon = R.drawable.passkey_icon,
- title = stringResource(R.string.use_password_title),
- )
- },
- accountContent = {
- PasswordRow(
- email = email,
- modifier = Modifier.padding(top = 10.dp),
- )
- },
- columnState = columnState,
- modifier = modifier.padding(horizontal = 10.dp)
- ) {
- item {
- DialogButtonsRow(
- onCancelClick = onCancelClick,
- onOKClick = onOKClick,
- modifier = Modifier.padding(top = 10.dp)
- )
- }
- }
-}
-
-@WearPreview
-@Composable
-fun SinglePasswordScreenPreview() {
- SinglePasswordScreen(
- email = "beckett_bakery@gmail.com",
- onCancelClick = {},
- onOKClick = {},
- columnState = belowTimeTextPreview(),
- )
-}
-
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SingleAccountScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/SingleAccountScreen.kt
similarity index 97%
rename from packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SingleAccountScreen.kt
rename to packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/SingleAccountScreen.kt
index f344ad0..8532783 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SingleAccountScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/SingleAccountScreen.kt
@@ -16,7 +16,7 @@
@file:OptIn(ExperimentalHorologistApi::class)
-package com.android.credentialmanager.ui.screens
+package com.android.credentialmanager.ui.screens.single
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasskeyScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
similarity index 94%
rename from packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasskeyScreen.kt
rename to packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
index c8f871e..c9b0230 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasskeyScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
@@ -16,7 +16,7 @@
@file:OptIn(ExperimentalHorologistApi::class)
-package com.android.credentialmanager.ui.screens
+package com.android.credentialmanager.ui.screens.single.passkey
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
@@ -27,6 +27,7 @@
import com.android.credentialmanager.ui.components.AccountRow
import com.android.credentialmanager.ui.components.DialogButtonsRow
import com.android.credentialmanager.ui.components.SignInHeader
+import com.android.credentialmanager.ui.screens.single.SingleAccountScreen
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.layout.ScalingLazyColumnState
import com.google.android.horologist.compose.layout.belowTimeTextPreview
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
new file mode 100644
index 0000000..c885ec4
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 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
+ *
+ * https://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.
+ */
+
+@file:OptIn(ExperimentalHorologistApi::class)
+
+package com.android.credentialmanager.ui.screens.single.password
+
+import android.util.Log
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.android.credentialmanager.R
+import com.android.credentialmanager.TAG
+import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract
+import com.android.credentialmanager.ui.components.DialogButtonsRow
+import com.android.credentialmanager.ui.components.PasswordRow
+import com.android.credentialmanager.ui.components.SignInHeader
+import com.android.credentialmanager.ui.screens.single.SingleAccountScreen
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.compose.layout.ScalingLazyColumnState
+import com.google.android.horologist.compose.layout.belowTimeTextPreview
+import com.google.android.horologist.compose.tools.WearPreview
+
+@Composable
+fun SinglePasswordScreen(
+ columnState: ScalingLazyColumnState,
+ onCloseApp: () -> Unit,
+ modifier: Modifier = Modifier,
+ viewModel: SinglePasswordScreenViewModel =
+ viewModel(factory = SinglePasswordScreenViewModel.Factory),
+) {
+ viewModel.initialize()
+
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ when (val state = uiState) {
+ SinglePasswordScreenUiState.Idle -> {
+ // TODO: b/301206470 implement latency version of the screen
+ }
+
+ is SinglePasswordScreenUiState.Loaded -> {
+ val model = state.passwordUiModel
+ SinglePasswordScreen(
+ email = model.email,
+ onCancelClick = viewModel::onCancelClick,
+ onOKClick = viewModel::onOKClick,
+ columnState = columnState,
+ modifier = modifier
+ )
+ }
+
+ is SinglePasswordScreenUiState.PasswordSelected -> {
+ val launcher = rememberLauncherForActivityResult(
+ StartBalIntentSenderForResultContract()
+ ) {
+ viewModel.onPasswordInfoRetrieved(it.resultCode, it.data)
+ }
+
+ SideEffect {
+ launcher.launch(state.intentSenderRequest)
+ }
+ }
+
+ SinglePasswordScreenUiState.Cancel -> {
+ // TODO: b/301206470 implement navigation for when user taps cancel
+ }
+
+ SinglePasswordScreenUiState.Error -> {
+ // TODO: b/301206470 implement navigation for when there is an error to load screen
+ }
+
+ SinglePasswordScreenUiState.Completed -> {
+ Log.d(TAG, "Received signal to finish the activity.")
+ onCloseApp()
+ }
+ }
+}
+
+@Composable
+fun SinglePasswordScreen(
+ email: String,
+ onCancelClick: () -> Unit,
+ onOKClick: () -> Unit,
+ columnState: ScalingLazyColumnState,
+ modifier: Modifier = Modifier,
+) {
+ SingleAccountScreen(
+ headerContent = {
+ SignInHeader(
+ icon = R.drawable.passkey_icon,
+ title = stringResource(R.string.use_password_title),
+ )
+ },
+ accountContent = {
+ PasswordRow(
+ email = email,
+ modifier = Modifier.padding(top = 10.dp),
+ )
+ },
+ columnState = columnState,
+ modifier = modifier.padding(horizontal = 10.dp)
+ ) {
+ item {
+ DialogButtonsRow(
+ onCancelClick = onCancelClick,
+ onOKClick = onOKClick,
+ modifier = Modifier.padding(top = 10.dp)
+ )
+ }
+ }
+}
+
+@WearPreview
+@Composable
+fun SinglePasswordScreenPreview() {
+ SinglePasswordScreen(
+ email = "beckett_bakery@gmail.com",
+ onCancelClick = {},
+ onOKClick = {},
+ columnState = belowTimeTextPreview(),
+ )
+}
+
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt
new file mode 100644
index 0000000..9b06622
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.ui.screens.single.password
+
+import android.content.Intent
+import android.credentials.ui.BaseDialogResult
+import android.credentials.ui.ProviderPendingIntentResponse
+import android.credentials.ui.UserSelectionDialogResult
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.result.IntentSenderRequest
+import androidx.annotation.MainThread
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.CreationExtras
+import com.android.credentialmanager.CredentialSelectorApp
+import com.android.credentialmanager.IS_AUTO_SELECTED_KEY
+import com.android.credentialmanager.TAG
+import com.android.credentialmanager.model.Password
+import com.android.credentialmanager.model.Request
+import com.android.credentialmanager.repository.RequestRepository
+import com.android.credentialmanager.ui.model.PasswordUiModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+
+class SinglePasswordScreenViewModel(
+ private val requestRepository: RequestRepository,
+) : ViewModel() {
+
+ private var initializeCalled = false
+
+ private lateinit var requestGet: Request.Get
+ private lateinit var password: Password
+
+ private val _uiState =
+ MutableStateFlow<SinglePasswordScreenUiState>(SinglePasswordScreenUiState.Idle)
+ val uiState: StateFlow<SinglePasswordScreenUiState> = _uiState
+
+ @MainThread
+ fun initialize() {
+ if (initializeCalled) return
+ initializeCalled = true
+
+ viewModelScope.launch {
+ val request = requestRepository.requests.first()
+ Log.d(TAG, "request: $request")
+
+ if (request !is Request.Get) {
+ _uiState.value = SinglePasswordScreenUiState.Error
+ } else {
+ requestGet = request
+ if (requestGet.passwordEntries.isEmpty()) {
+ Log.d(TAG, "Empty passwordEntries")
+ _uiState.value = SinglePasswordScreenUiState.Error
+ } else {
+ password = requestGet.passwordEntries.first()
+ _uiState.value = SinglePasswordScreenUiState.Loaded(
+ PasswordUiModel(
+ email = password.passwordCredentialEntry.username.toString(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ fun onCancelClick() {
+ _uiState.value = SinglePasswordScreenUiState.Cancel
+ }
+
+ fun onOKClick() {
+ // TODO: b/301206470 move this code to shared module
+ val entryIntent = password.entry.frameworkExtrasIntent
+ entryIntent?.putExtra(IS_AUTO_SELECTED_KEY, false)
+ val intentSenderRequest = IntentSenderRequest.Builder(
+ pendingIntent = password.passwordCredentialEntry.pendingIntent
+ ).setFillInIntent(entryIntent).build()
+
+ _uiState.value = SinglePasswordScreenUiState.PasswordSelected(
+ intentSenderRequest = intentSenderRequest
+ )
+ }
+
+ fun onPasswordInfoRetrieved(
+ resultCode: Int? = null,
+ resultData: Intent? = null,
+ ) {
+ // TODO: b/301206470 move this code to shared module
+ Log.d(TAG, "credential selected: {provider=${password.providerId}" +
+ ", key=${password.entry.key}, subkey=${password.entry.subkey}}")
+
+ val userSelectionDialogResult = UserSelectionDialogResult(
+ requestGet.token,
+ password.providerId,
+ password.entry.key,
+ password.entry.subkey,
+ if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
+ )
+ val resultDataBundle = Bundle()
+ UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultDataBundle)
+ requestGet.resultReceiver?.send(
+ BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION,
+ resultDataBundle
+ )
+
+ _uiState.value = SinglePasswordScreenUiState.Completed
+ }
+
+ companion object {
+ val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : ViewModel> create(
+ modelClass: Class<T>,
+ extras: CreationExtras
+ ): T {
+ val application = checkNotNull(extras[APPLICATION_KEY])
+
+ return SinglePasswordScreenViewModel(
+ requestRepository = (application as CredentialSelectorApp).requestRepository,
+ ) as T
+ }
+ }
+ }
+}
+
+sealed class SinglePasswordScreenUiState {
+ data object Idle : SinglePasswordScreenUiState()
+ data class Loaded(val passwordUiModel: PasswordUiModel) : SinglePasswordScreenUiState()
+ data class PasswordSelected(
+ val intentSenderRequest: IntentSenderRequest
+ ) : SinglePasswordScreenUiState()
+
+ data object Cancel : SinglePasswordScreenUiState()
+ data object Error : SinglePasswordScreenUiState()
+ data object Completed : SinglePasswordScreenUiState()
+}