diff options
12 files changed, 392 insertions, 11 deletions
diff --git a/packages/CredentialManager/shared/Android.bp b/packages/CredentialManager/shared/Android.bp new file mode 100644 index 000000000000..ae4281e5561c --- /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 000000000000..a46088783024 --- /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 000000000000..6627af526dee --- /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 000000000000..f49bb33d2e8a --- /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 000000000000..a646851bf570 --- /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 000000000000..7fa0ca918e21 --- /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 000000000000..89766c2ec61b --- /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 000000000000..3d835bebc06b --- /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 639e8d18b306..36340fac1760 100644 --- a/packages/CredentialManager/wear/Android.bp +++ b/packages/CredentialManager/wear/Android.bp @@ -21,6 +21,7 @@ android_app { }, 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 77fffaa1e04c..2c0575547162 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 000000000000..e46fcae78f6a --- /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 5ec0c8cd9292..19ea9ede9d98 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.WearNavScaffold 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) |