Implement cancellation request.

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

Change-Id: Ifedadd163e89b17a193de307cadf439c3a471bd9
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)