Implement cancellation request.

Bug: 300426308
Test: N/A - will manual test once phone is set up.

Change-Id: Ifedadd163e89b17a193de307cadf439c3a471bd9
diff --git a/packages/CredentialManager/shared/Android.bp b/packages/CredentialManager/shared/Android.bp
new file mode 100644
index 0000000..ae4281e
--- /dev/null
+++ b/packages/CredentialManager/shared/Android.bp
@@ -0,0 +1,18 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_library {
+    name: "CredentialManagerShared",
+    manifest: "AndroidManifest.xml",
+    srcs: ["src/**/*.kt"],
+    static_libs: [
+        "androidx.core_core-ktx",
+        "androidx.credentials_credentials",
+    ],
+}
diff --git a/packages/CredentialManager/shared/AndroidManifest.xml b/packages/CredentialManager/shared/AndroidManifest.xml
new file mode 100644
index 0000000..a460887
--- /dev/null
+++ b/packages/CredentialManager/shared/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (c) 2023 Google Inc.
+ *
+ * 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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.credentialmanager">
+
+</manifest>
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/IntentParser.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/IntentParser.kt
new file mode 100644
index 0000000..6627af5
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/IntentParser.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.credentials.ui.RequestInfo
+import com.android.credentialmanager.ui.ktx.cancelUiRequest
+import com.android.credentialmanager.ui.ktx.requestInfo
+import com.android.credentialmanager.ui.mapper.toCancel
+import com.android.credentialmanager.ui.model.Request
+
+fun Intent.parse(): Request {
+    cancelUiRequest?.let {
+        return it.toCancel()
+    }
+
+    return when (requestInfo?.type) {
+        RequestInfo.TYPE_CREATE -> {
+            Request.Create
+        }
+        RequestInfo.TYPE_GET -> {
+            Request.Get
+        }
+        else -> {
+            throw IllegalStateException("Unrecognized request type: ${requestInfo?.type}")
+        }
+    }
+}
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/LogConstants.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/LogConstants.kt
new file mode 100644
index 0000000..f49bb33
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/LogConstants.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.ui
+
+const val TAG = "CredentialSelector"
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/ktx/IntentKtx.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/ktx/IntentKtx.kt
new file mode 100644
index 0000000..a646851
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/ktx/IntentKtx.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.ktx
+
+import android.content.Intent
+import android.credentials.ui.CancelUiRequest
+import android.credentials.ui.RequestInfo
+
+val Intent.cancelUiRequest: CancelUiRequest?
+    get() = this.extras?.getParcelable(
+        CancelUiRequest.EXTRA_CANCEL_UI_REQUEST,
+        CancelUiRequest::class.java
+    )
+
+val Intent.requestInfo: RequestInfo?
+    get() = this.extras?.getParcelable(
+        RequestInfo.EXTRA_REQUEST_INFO,
+        RequestInfo::class.java
+    )
\ No newline at end of file
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/ktx/PackageManagerKtx.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/ktx/PackageManagerKtx.kt
new file mode 100644
index 0000000..7fa0ca9
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/ktx/PackageManagerKtx.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.ktx
+
+import android.content.pm.PackageManager
+import android.text.TextUtils
+import android.util.Log
+import com.android.credentialmanager.ui.TAG
+
+fun PackageManager.appLabel(appPackageName: String): String? =
+    try {
+        val pkgInfo = this.getPackageInfo(appPackageName, PackageManager.PackageInfoFlags.of(0))
+        val applicationInfo = checkNotNull(pkgInfo.applicationInfo)
+        applicationInfo.loadSafeLabel(
+            this, 0f,
+            TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
+        ).toString()
+    } catch (e: Exception) {
+        Log.e(TAG, "Caller app not found", e)
+        null
+    }
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/mapper/RequestMapper.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/mapper/RequestMapper.kt
new file mode 100644
index 0000000..89766c2
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/mapper/RequestMapper.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.mapper
+
+import android.credentials.ui.CancelUiRequest
+import com.android.credentialmanager.ui.model.Request
+
+fun CancelUiRequest.toCancel() = Request.Cancel(
+    showCancellationUi = this.shouldShowCancellationUi(),
+    appPackageName = this.appPackageName
+)
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/model/Request.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/model/Request.kt
new file mode 100644
index 0000000..3d835be
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/model/Request.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.model
+
+/**
+ * Represents the request made by the CredentialManager API.
+ */
+sealed class Request {
+    data class Cancel(
+        val showCancellationUi: Boolean,
+        val appPackageName: String?
+    ) : Request()
+
+    data object Get : Request()
+
+    data object Create : Request()
+}
diff --git a/packages/CredentialManager/wear/Android.bp b/packages/CredentialManager/wear/Android.bp
index 639e8d1..36340fa 100644
--- a/packages/CredentialManager/wear/Android.bp
+++ b/packages/CredentialManager/wear/Android.bp
@@ -21,6 +21,7 @@
     },
 
     static_libs: [
+        "CredentialManagerShared",
         "Horologist",
         "PlatformComposeCore",
         "androidx.activity_activity-compose",
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorActivity.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorActivity.kt
index 77fffaa..2c05755 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorActivity.kt
@@ -16,28 +16,73 @@
 
 package com.android.credentialmanager.ui
 
+import android.content.Intent
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
-import androidx.navigation.NavHostController
+import androidx.activity.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
 import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
+import kotlinx.coroutines.launch
 
 class CredentialSelectorActivity : ComponentActivity() {
 
-    lateinit var navController: NavHostController
+    private val viewModel: CredentialSelectorViewModel by viewModels()
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
         setTheme(android.R.style.Theme_DeviceDefault)
 
-        setContent {
-            navController = rememberSwipeDismissableNavController()
+        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.
+                        }
 
-            MaterialTheme {
-                WearApp(navController = navController)
+                        CredentialSelectorUiState.Get -> {
+                            // TODO: b/301206470 - Implement get flow
+                            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
new file mode 100644
index 0000000..e46fcae
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorViewModel.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.ui.ktx.appLabel
+import com.android.credentialmanager.ui.ktx.requestInfo
+import com.android.credentialmanager.ui.model.Request
+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
+                    }
+
+                    Request.Get -> {
+                        _uiState.value = CredentialSelectorUiState.Get
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 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 {
+    object Idle : CredentialSelectorUiState()
+    object Get : CredentialSelectorUiState()
+    object Create : CredentialSelectorUiState()
+    data class Cancel(val appName: String) : CredentialSelectorUiState()
+    object Finish : CredentialSelectorUiState()
+}
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 5ec0c8c..19ea9ed 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
@@ -19,8 +19,8 @@
 package com.android.credentialmanager.ui
 
 import androidx.compose.runtime.Composable
-import androidx.navigation.NavHostController
 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.google.android.horologist.annotations.ExperimentalHorologistApi
@@ -28,9 +28,8 @@
 import com.google.android.horologist.compose.navscaffold.composable
 
 @Composable
-fun WearApp(
-    navController: NavHostController
-) {
+fun WearApp() {
+    val navController = rememberSwipeDismissableNavController()
     val swipeToDismissBoxState = rememberSwipeToDismissBoxState()
     val navHostState =
         rememberSwipeDismissableNavHostState(swipeToDismissBoxState = swipeToDismissBoxState)