Merge "bt: Fix displaying late bond device name" into udc-dev
diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java
index 6b044fc..82694ee 100644
--- a/core/java/android/hardware/biometrics/BiometricManager.java
+++ b/core/java/android/hardware/biometrics/BiometricManager.java
@@ -97,27 +97,6 @@
public @interface BiometricError {}
/**
- * Single sensor or unspecified multi-sensor behavior (prefer an explicit choice if the
- * device is multi-sensor).
- * @hide
- */
- public static final int BIOMETRIC_MULTI_SENSOR_DEFAULT = 0;
-
- /**
- * Use face and fingerprint sensors together.
- * @hide
- */
- public static final int BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE = 1;
-
- /**
- * @hide
- */
- @IntDef({BIOMETRIC_MULTI_SENSOR_DEFAULT,
- BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE})
- @Retention(RetentionPolicy.SOURCE)
- public @interface BiometricMultiSensorMode {}
-
- /**
* Types of authenticators, defined at a level of granularity supported by
* {@link BiometricManager} and {@link BiometricPrompt}.
*
diff --git a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
index 450c5ce..45f1c8a 100644
--- a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
+++ b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
@@ -29,5 +29,7 @@
// Notifies the client that an internal event, e.g. back button has occurred.
void onSystemEvent(int event);
// Notifies that the dialog has finished animating.
- void onDialogAnimatedIn();
+ void onDialogAnimatedIn(boolean startFingerprintNow);
+ // Notifies that the fingerprint should start now (after onDialogAnimatedIn(false)).
+ void onStartFingerprintNow();
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 5c79f69..d695c0c 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -15310,18 +15310,6 @@
public static final String ANGLE_EGL_FEATURES = "angle_egl_features";
/**
- * Comma-separated list of package names that ANGLE may have issues with
- * @hide
- */
- public static final String ANGLE_DEFERLIST = "angle_deferlist";
-
- /**
- * Integer mode of the logic for applying `angle_deferlist`
- * @hide
- */
- public static final String ANGLE_DEFERLIST_MODE = "angle_deferlist_mode";
-
- /**
* Show the "ANGLE In Use" dialog box to the user when ANGLE is the OpenGL driver.
* The value is a boolean (1 or 0).
* @hide
diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java
index 02cd037..99a4f6b 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -23,6 +23,7 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
+import android.graphics.HardwareRenderer;
import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
@@ -550,6 +551,11 @@
ThreadedRenderer.trimMemory(level);
}
+ /** @hide */
+ public void trimCaches(@HardwareRenderer.CacheTrimLevel int level) {
+ ThreadedRenderer.trimCaches(level);
+ }
+
public void dumpGfxInfo(FileDescriptor fd, String[] args) {
FileOutputStream fout = new FileOutputStream(fd);
PrintWriter pw = new FastPrintWriter(fout);
diff --git a/core/java/com/android/internal/config/appcloning/AppCloningDeviceConfigHelper.java b/core/java/com/android/internal/config/appcloning/AppCloningDeviceConfigHelper.java
deleted file mode 100644
index ddd3d2c..0000000
--- a/core/java/com/android/internal/config/appcloning/AppCloningDeviceConfigHelper.java
+++ /dev/null
@@ -1,107 +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.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.internal.config.appcloning;
-
-import android.content.Context;
-import android.provider.DeviceConfig;
-
-import com.android.internal.annotations.GuardedBy;
-
-/**
- * Helper class that holds the flags related to the app_cloning namespace in {@link DeviceConfig}.
- *
- * @hide
- */
-public class AppCloningDeviceConfigHelper {
-
- @GuardedBy("sLock")
- private static AppCloningDeviceConfigHelper sInstance;
-
- private static final Object sLock = new Object();
-
- private DeviceConfig.OnPropertiesChangedListener mDeviceConfigChangeListener;
-
- /**
- * This flag is defined inside {@link DeviceConfig#NAMESPACE_APP_CLONING}. Please check
- * {@link #mEnableAppCloningBuildingBlocks} for details.
- */
- public static final String ENABLE_APP_CLONING_BUILDING_BLOCKS =
- "enable_app_cloning_building_blocks";
-
- /**
- * Checks whether the support for app-cloning building blocks (like contacts
- * sharing/intent redirection), which are available starting from the U release, is turned on.
- * The default value is true to ensure the features are always enabled going forward.
- *
- * TODO:(b/253449368) Add information about the app-cloning config and mention that the devices
- * that do not support app-cloning should use the app-cloning config to disable all app-cloning
- * features.
- */
- private volatile boolean mEnableAppCloningBuildingBlocks = true;
-
- private AppCloningDeviceConfigHelper() {}
-
- /**
- * @hide
- */
- public static AppCloningDeviceConfigHelper getInstance(Context context) {
- synchronized (sLock) {
- if (sInstance == null) {
- sInstance = new AppCloningDeviceConfigHelper();
- sInstance.init(context);
- }
- return sInstance;
- }
- }
-
- private void init(Context context) {
- initializeDeviceConfigChangeListener();
- DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_APP_CLONING,
- context.getMainExecutor(),
- mDeviceConfigChangeListener);
- }
-
- private void initializeDeviceConfigChangeListener() {
- mDeviceConfigChangeListener = properties -> {
- if (!DeviceConfig.NAMESPACE_APP_CLONING.equals(properties.getNamespace())) {
- return;
- }
- for (String name : properties.getKeyset()) {
- if (name == null) {
- return;
- }
- if (ENABLE_APP_CLONING_BUILDING_BLOCKS.equals(name)) {
- updateEnableAppCloningBuildingBlocks();
- }
- }
- };
- }
-
- private void updateEnableAppCloningBuildingBlocks() {
- mEnableAppCloningBuildingBlocks = DeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_APP_CLONING, ENABLE_APP_CLONING_BUILDING_BLOCKS, true);
- }
-
- /**
- * Fetch the feature flag to check whether the support for the app-cloning building blocks
- * (like contacts sharing/intent redirection) is enabled on the device.
- * @hide
- */
- public boolean getEnableAppCloningBuildingBlocks() {
- return mEnableAppCloningBuildingBlocks;
- }
-}
diff --git a/core/java/com/android/internal/config/appcloning/OWNERS b/core/java/com/android/internal/config/appcloning/OWNERS
deleted file mode 100644
index 0645a8c5..0000000
--- a/core/java/com/android/internal/config/appcloning/OWNERS
+++ /dev/null
@@ -1,3 +0,0 @@
-# Bug component: 1207885
-jigarthakkar@google.com
-saumyap@google.com
\ No newline at end of file
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index ae58626..d2564fb 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -157,7 +157,7 @@
*/
void showAuthenticationDialog(in PromptInfo promptInfo, IBiometricSysuiReceiver sysuiReceiver,
in int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation, int userId,
- long operationId, String opPackageName, long requestId, int multiSensorConfig);
+ long operationId, String opPackageName, long requestId);
/**
* Used to notify the authentication dialog that a biometric has been authenticated.
*/
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index 3708859..3977666 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -123,8 +123,7 @@
// Used to show the authentication dialog (Biometrics, Device Credential)
void showAuthenticationDialog(in PromptInfo promptInfo, IBiometricSysuiReceiver sysuiReceiver,
in int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation,
- int userId, long operationId, String opPackageName, long requestId,
- int multiSensorConfig);
+ int userId, long operationId, String opPackageName, long requestId);
// Used to notify the authentication dialog that a biometric has been authenticated
void onBiometricAuthenticated(int modality);
diff --git a/core/proto/android/providers/settings/global.proto b/core/proto/android/providers/settings/global.proto
index 128de8b..052e2f2 100644
--- a/core/proto/android/providers/settings/global.proto
+++ b/core/proto/android/providers/settings/global.proto
@@ -471,10 +471,6 @@
optional SettingProto updatable_driver_prerelease_opt_in_apps = 18;
optional SettingProto angle_egl_features = 19;
- // ANGLE - List of Apps that ANGLE may have issues with
- optional SettingProto angle_deferlist = 20;
- // ANGLE - Integer mode of the logic for applying `angle_deferlist`
- optional SettingProto angle_deferlist_mode = 21;
}
optional Gpu gpu = 59;
diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java
index 9ed3d9c..9cde187 100644
--- a/graphics/java/android/graphics/HardwareRenderer.java
+++ b/graphics/java/android/graphics/HardwareRenderer.java
@@ -144,6 +144,32 @@
public @interface DumpFlags {
}
+
+ /**
+ * Trims all Skia caches.
+ * @hide
+ */
+ public static final int CACHE_TRIM_ALL = 0;
+ /**
+ * Trims Skia font caches.
+ * @hide
+ */
+ public static final int CACHE_TRIM_FONT = 1;
+ /**
+ * Trims Skia resource caches.
+ * @hide
+ */
+ public static final int CACHE_TRIM_RESOURCES = 2;
+
+ /** @hide */
+ @IntDef(prefix = {"CACHE_TRIM_"}, value = {
+ CACHE_TRIM_ALL,
+ CACHE_TRIM_FONT,
+ CACHE_TRIM_RESOURCES
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CacheTrimLevel {}
+
/**
* Name of the file that holds the shaders cache.
*/
@@ -1131,6 +1157,20 @@
nTrimMemory(level);
}
+ /**
+ * Invoke this when all font caches should be flushed. This can cause jank on next render
+ * commands so use it only after expensive font allocation operations which would
+ * allocate large amount of temporary memory.
+ *
+ * @param level Hint about which caches to trim. See {@link #CACHE_TRIM_ALL},
+ * {@link #CACHE_TRIM_FONT}, {@link #CACHE_TRIM_RESOURCES}
+ *
+ * @hide
+ */
+ public static void trimCaches(@CacheTrimLevel int level) {
+ nTrimCaches(level);
+ }
+
/** @hide */
public static void overrideProperty(@NonNull String name, @NonNull String value) {
if (name == null || value == null) {
@@ -1497,6 +1537,8 @@
private static native void nTrimMemory(int level);
+ private static native void nTrimCaches(int level);
+
private static native void nOverrideProperty(String name, String value);
private static native void nFence(long nativeProxy);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index a48be5e..91c7cc0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -1346,7 +1346,7 @@
// Recreates & shows the education views. Call when a theme/config change happens.
private void updateUserEdu() {
- if (isStackEduVisible()) {
+ if (isStackEduVisible() && !mStackEduView.isHiding()) {
removeView(mStackEduView);
mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController);
addView(mStackEduView);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
index 627273f..d0598cd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
@@ -37,8 +37,7 @@
context: Context,
positioner: BubblePositioner,
controller: BubbleController
-)
- : LinearLayout(context) {
+) : LinearLayout(context) {
private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView"
else BubbleDebugConfig.TAG_BUBBLES
@@ -53,7 +52,8 @@
private val titleTextView by lazy { findViewById<TextView>(R.id.stack_education_title) }
private val descTextView by lazy { findViewById<TextView>(R.id.stack_education_description) }
- private var isHiding = false
+ var isHiding = false
+ private set
init {
LayoutInflater.from(context).inflate(R.layout.bubble_stack_user_education, this)
diff --git a/libs/hwui/MemoryPolicy.h b/libs/hwui/MemoryPolicy.h
index 139cdde..347daf34 100644
--- a/libs/hwui/MemoryPolicy.h
+++ b/libs/hwui/MemoryPolicy.h
@@ -31,6 +31,12 @@
RUNNING_MODERATE = 5,
};
+enum class CacheTrimLevel {
+ ALL_CACHES = 0,
+ FONT_CACHE = 1,
+ RESOURCE_CACHE = 2,
+};
+
struct MemoryPolicy {
// The initial scale factor applied to the display resolution. The default is 1, but
// lower values may be used to start with a smaller initial cache size. The cache will
diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
index 6a7411f..d04de37 100644
--- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
+++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
@@ -362,6 +362,10 @@
RenderProxy::trimMemory(level);
}
+static void android_view_ThreadedRenderer_trimCaches(JNIEnv* env, jobject clazz, jint level) {
+ RenderProxy::trimCaches(level);
+}
+
static void android_view_ThreadedRenderer_overrideProperty(JNIEnv* env, jobject clazz,
jstring name, jstring value) {
const char* nameCharArray = env->GetStringUTFChars(name, NULL);
@@ -1018,6 +1022,7 @@
(void*)android_view_ThreadedRenderer_notifyCallbackPending},
{"nNotifyExpensiveFrame", "(J)V",
(void*)android_view_ThreadedRenderer_notifyExpensiveFrame},
+ {"nTrimCaches", "(I)V", (void*)android_view_ThreadedRenderer_trimCaches},
};
static JavaVM* mJvm = nullptr;
diff --git a/libs/hwui/renderthread/CacheManager.cpp b/libs/hwui/renderthread/CacheManager.cpp
index c00a270..babce88 100644
--- a/libs/hwui/renderthread/CacheManager.cpp
+++ b/libs/hwui/renderthread/CacheManager.cpp
@@ -139,6 +139,25 @@
}
}
+void CacheManager::trimCaches(CacheTrimLevel mode) {
+ switch (mode) {
+ case CacheTrimLevel::FONT_CACHE:
+ SkGraphics::PurgeFontCache();
+ break;
+ case CacheTrimLevel::RESOURCE_CACHE:
+ SkGraphics::PurgeResourceCache();
+ break;
+ case CacheTrimLevel::ALL_CACHES:
+ SkGraphics::PurgeAllCaches();
+ if (mGrContext) {
+ mGrContext->purgeUnlockedResources(false);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
void CacheManager::trimStaleResources() {
if (!mGrContext) {
return;
diff --git a/libs/hwui/renderthread/CacheManager.h b/libs/hwui/renderthread/CacheManager.h
index d21ac9b..5e43ac2 100644
--- a/libs/hwui/renderthread/CacheManager.h
+++ b/libs/hwui/renderthread/CacheManager.h
@@ -48,6 +48,7 @@
void configureContext(GrContextOptions* context, const void* identity, ssize_t size);
#endif
void trimMemory(TrimLevel mode);
+ void trimCaches(CacheTrimLevel mode);
void trimStaleResources();
void dumpMemoryUsage(String8& log, const RenderState* renderState = nullptr);
void getMemoryUsage(size_t* cpuUsage, size_t* gpuUsage);
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index 31b4b20..224c878 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -231,6 +231,15 @@
}
}
+void RenderProxy::trimCaches(int level) {
+ // Avoid creating a RenderThread to do a trimMemory.
+ if (RenderThread::hasInstance()) {
+ RenderThread& thread = RenderThread::getInstance();
+ const auto trimLevel = static_cast<CacheTrimLevel>(level);
+ thread.queue().post([&thread, trimLevel]() { thread.trimCaches(trimLevel); });
+ }
+}
+
void RenderProxy::purgeCaches() {
if (RenderThread::hasInstance()) {
RenderThread& thread = RenderThread::getInstance();
diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h
index 82072a6..47c1b0c 100644
--- a/libs/hwui/renderthread/RenderProxy.h
+++ b/libs/hwui/renderthread/RenderProxy.h
@@ -105,6 +105,7 @@
void destroyHardwareResources();
static void trimMemory(int level);
+ static void trimCaches(int level);
static void purgeCaches();
static void overrideProperty(const char* name, const char* value);
diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp
index 9ba67a2..eb28c08 100644
--- a/libs/hwui/renderthread/RenderThread.cpp
+++ b/libs/hwui/renderthread/RenderThread.cpp
@@ -521,6 +521,11 @@
cacheManager().trimMemory(level);
}
+void RenderThread::trimCaches(CacheTrimLevel level) {
+ ATRACE_CALL();
+ cacheManager().trimCaches(level);
+}
+
} /* namespace renderthread */
} /* namespace uirenderer */
} /* namespace android */
diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h
index c77cd41..79e57de 100644
--- a/libs/hwui/renderthread/RenderThread.h
+++ b/libs/hwui/renderthread/RenderThread.h
@@ -174,6 +174,7 @@
}
void trimMemory(TrimLevel level);
+ void trimCaches(CacheTrimLevel level);
/**
* isCurrent provides a way to query, if the caller is running on
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 1fd84c7..a83bfda 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -785,12 +785,6 @@
Settings.Global.ANGLE_EGL_FEATURES,
GlobalSettingsProto.Gpu.ANGLE_EGL_FEATURES);
dumpSetting(s, p,
- Settings.Global.ANGLE_DEFERLIST,
- GlobalSettingsProto.Gpu.ANGLE_DEFERLIST);
- dumpSetting(s, p,
- Settings.Global.ANGLE_DEFERLIST_MODE,
- GlobalSettingsProto.Gpu.ANGLE_DEFERLIST_MODE);
- dumpSetting(s, p,
Settings.Global.SHOW_ANGLE_IN_USE_DIALOG_BOX,
GlobalSettingsProto.Gpu.SHOW_ANGLE_IN_USE_DIALOG);
dumpSetting(s, p,
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index ef4b814..873b434 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -518,8 +518,6 @@
Settings.Global.ANGLE_GL_DRIVER_SELECTION_PKGS,
Settings.Global.ANGLE_GL_DRIVER_SELECTION_VALUES,
Settings.Global.ANGLE_EGL_FEATURES,
- Settings.Global.ANGLE_DEFERLIST,
- Settings.Global.ANGLE_DEFERLIST_MODE,
Settings.Global.UPDATABLE_DRIVER_ALL_APPS,
Settings.Global.UPDATABLE_DRIVER_PRODUCTION_OPT_IN_APPS,
Settings.Global.UPDATABLE_DRIVER_PRERELEASE_OPT_IN_APPS,
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
index d1ee18a..25269dc 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
@@ -85,7 +85,7 @@
@Composable
private fun filledButtonColors(): ButtonColors {
- val colors = LocalAndroidColorScheme.current
+ val colors = LocalAndroidColorScheme.current.deprecated
return ButtonDefaults.buttonColors(
containerColor = colors.colorAccentPrimary,
contentColor = colors.textColorOnAccent,
@@ -94,7 +94,7 @@
@Composable
private fun outlineButtonColors(): ButtonColors {
- val colors = LocalAndroidColorScheme.current
+ val colors = LocalAndroidColorScheme.current.deprecated
return ButtonDefaults.outlinedButtonColors(
contentColor = colors.textColorPrimary,
)
@@ -102,7 +102,7 @@
@Composable
private fun outlineButtonBorder(): BorderStroke {
- val colors = LocalAndroidColorScheme.current
+ val colors = LocalAndroidColorScheme.current.deprecated
return BorderStroke(
width = 1.dp,
color = colors.colorAccentPrimaryVariant,
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
index 4f1657f..1d6f813 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
@@ -37,33 +37,83 @@
* Important: Use M3 colors from MaterialTheme.colorScheme whenever possible instead. In the future,
* most of the colors in this class will be removed in favor of their M3 counterpart.
*/
-class AndroidColorScheme internal constructor(context: Context) {
- val colorPrimary = getColor(context, R.attr.colorPrimary)
- val colorPrimaryDark = getColor(context, R.attr.colorPrimaryDark)
- val colorAccent = getColor(context, R.attr.colorAccent)
- val colorAccentPrimary = getColor(context, R.attr.colorAccentPrimary)
- val colorAccentSecondary = getColor(context, R.attr.colorAccentSecondary)
- val colorAccentTertiary = getColor(context, R.attr.colorAccentTertiary)
- val colorAccentPrimaryVariant = getColor(context, R.attr.colorAccentPrimaryVariant)
- val colorAccentSecondaryVariant = getColor(context, R.attr.colorAccentSecondaryVariant)
- val colorAccentTertiaryVariant = getColor(context, R.attr.colorAccentTertiaryVariant)
- val colorSurface = getColor(context, R.attr.colorSurface)
- val colorSurfaceHighlight = getColor(context, R.attr.colorSurfaceHighlight)
- val colorSurfaceVariant = getColor(context, R.attr.colorSurfaceVariant)
- val colorSurfaceHeader = getColor(context, R.attr.colorSurfaceHeader)
- val colorError = getColor(context, R.attr.colorError)
- val colorBackground = getColor(context, R.attr.colorBackground)
- val colorBackgroundFloating = getColor(context, R.attr.colorBackgroundFloating)
- val panelColorBackground = getColor(context, R.attr.panelColorBackground)
- val textColorPrimary = getColor(context, R.attr.textColorPrimary)
- val textColorSecondary = getColor(context, R.attr.textColorSecondary)
- val textColorTertiary = getColor(context, R.attr.textColorTertiary)
- val textColorPrimaryInverse = getColor(context, R.attr.textColorPrimaryInverse)
- val textColorSecondaryInverse = getColor(context, R.attr.textColorSecondaryInverse)
- val textColorTertiaryInverse = getColor(context, R.attr.textColorTertiaryInverse)
- val textColorOnAccent = getColor(context, R.attr.textColorOnAccent)
- val colorForeground = getColor(context, R.attr.colorForeground)
- val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse)
+class AndroidColorScheme(context: Context) {
+ val onSecondaryFixedVariant = getColor(context, R.attr.materialColorOnSecondaryFixedVariant)
+ val onTertiaryFixedVariant = getColor(context, R.attr.materialColorOnTertiaryFixedVariant)
+ val surfaceContainerLowest = getColor(context, R.attr.materialColorSurfaceContainerLowest)
+ val onPrimaryFixedVariant = getColor(context, R.attr.materialColorOnPrimaryFixedVariant)
+ val onSecondaryContainer = getColor(context, R.attr.materialColorOnSecondaryContainer)
+ val onTertiaryContainer = getColor(context, R.attr.materialColorOnTertiaryContainer)
+ val surfaceContainerLow = getColor(context, R.attr.materialColorSurfaceContainerLow)
+ val onPrimaryContainer = getColor(context, R.attr.materialColorOnPrimaryContainer)
+ val secondaryFixedDim = getColor(context, R.attr.materialColorSecondaryFixedDim)
+ val onErrorContainer = getColor(context, R.attr.materialColorOnErrorContainer)
+ val onSecondaryFixed = getColor(context, R.attr.materialColorOnSecondaryFixed)
+ val onSurfaceInverse = getColor(context, R.attr.materialColorOnSurfaceInverse)
+ val tertiaryFixedDim = getColor(context, R.attr.materialColorTertiaryFixedDim)
+ val onTertiaryFixed = getColor(context, R.attr.materialColorOnTertiaryFixed)
+ val primaryFixedDim = getColor(context, R.attr.materialColorPrimaryFixedDim)
+ val secondaryContainer = getColor(context, R.attr.materialColorSecondaryContainer)
+ val errorContainer = getColor(context, R.attr.materialColorErrorContainer)
+ val onPrimaryFixed = getColor(context, R.attr.materialColorOnPrimaryFixed)
+ val primaryInverse = getColor(context, R.attr.materialColorPrimaryInverse)
+ val secondaryFixed = getColor(context, R.attr.materialColorSecondaryFixed)
+ val surfaceInverse = getColor(context, R.attr.materialColorSurfaceInverse)
+ val surfaceVariant = getColor(context, R.attr.materialColorSurfaceVariant)
+ val tertiaryContainer = getColor(context, R.attr.materialColorTertiaryContainer)
+ val tertiaryFixed = getColor(context, R.attr.materialColorTertiaryFixed)
+ val primaryContainer = getColor(context, R.attr.materialColorPrimaryContainer)
+ val onBackground = getColor(context, R.attr.materialColorOnBackground)
+ val primaryFixed = getColor(context, R.attr.materialColorPrimaryFixed)
+ val onSecondary = getColor(context, R.attr.materialColorOnSecondary)
+ val onTertiary = getColor(context, R.attr.materialColorOnTertiary)
+ val surfaceDim = getColor(context, R.attr.materialColorSurfaceDim)
+ val surfaceBright = getColor(context, R.attr.materialColorSurfaceBright)
+ val onError = getColor(context, R.attr.materialColorOnError)
+ val surface = getColor(context, R.attr.materialColorSurface)
+ val surfaceContainerHigh = getColor(context, R.attr.materialColorSurfaceContainerHigh)
+ val surfaceContainerHighest = getColor(context, R.attr.materialColorSurfaceContainerHighest)
+ val onSurfaceVariant = getColor(context, R.attr.materialColorOnSurfaceVariant)
+ val outline = getColor(context, R.attr.materialColorOutline)
+ val outlineVariant = getColor(context, R.attr.materialColorOutlineVariant)
+ val onPrimary = getColor(context, R.attr.materialColorOnPrimary)
+ val onSurface = getColor(context, R.attr.materialColorOnSurface)
+ val surfaceContainer = getColor(context, R.attr.materialColorSurfaceContainer)
+ val primary = getColor(context, R.attr.materialColorPrimary)
+ val secondary = getColor(context, R.attr.materialColorSecondary)
+ val tertiary = getColor(context, R.attr.materialColorTertiary)
+
+ @Deprecated("Use the new android tokens: go/sysui-colors")
+ val deprecated = DeprecatedValues(context)
+
+ class DeprecatedValues(context: Context) {
+ val colorPrimary = getColor(context, R.attr.colorPrimary)
+ val colorPrimaryDark = getColor(context, R.attr.colorPrimaryDark)
+ val colorAccent = getColor(context, R.attr.colorAccent)
+ val colorAccentPrimary = getColor(context, R.attr.colorAccentPrimary)
+ val colorAccentSecondary = getColor(context, R.attr.colorAccentSecondary)
+ val colorAccentTertiary = getColor(context, R.attr.colorAccentTertiary)
+ val colorAccentPrimaryVariant = getColor(context, R.attr.colorAccentPrimaryVariant)
+ val colorAccentSecondaryVariant = getColor(context, R.attr.colorAccentSecondaryVariant)
+ val colorAccentTertiaryVariant = getColor(context, R.attr.colorAccentTertiaryVariant)
+ val colorSurface = getColor(context, R.attr.colorSurface)
+ val colorSurfaceHighlight = getColor(context, R.attr.colorSurfaceHighlight)
+ val colorSurfaceVariant = getColor(context, R.attr.colorSurfaceVariant)
+ val colorSurfaceHeader = getColor(context, R.attr.colorSurfaceHeader)
+ val colorError = getColor(context, R.attr.colorError)
+ val colorBackground = getColor(context, R.attr.colorBackground)
+ val colorBackgroundFloating = getColor(context, R.attr.colorBackgroundFloating)
+ val panelColorBackground = getColor(context, R.attr.panelColorBackground)
+ val textColorPrimary = getColor(context, R.attr.textColorPrimary)
+ val textColorSecondary = getColor(context, R.attr.textColorSecondary)
+ val textColorTertiary = getColor(context, R.attr.textColorTertiary)
+ val textColorPrimaryInverse = getColor(context, R.attr.textColorPrimaryInverse)
+ val textColorSecondaryInverse = getColor(context, R.attr.textColorSecondaryInverse)
+ val textColorTertiaryInverse = getColor(context, R.attr.textColorTertiaryInverse)
+ val textColorOnAccent = getColor(context, R.attr.textColorOnAccent)
+ val colorForeground = getColor(context, R.attr.colorForeground)
+ val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse)
+ }
companion object {
fun getColor(context: Context, attr: Int): Color {
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/theme/SystemUIThemeTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/theme/SystemUIThemeTest.kt
index 9bc6856..fe34017 100644
--- a/packages/SystemUI/compose/core/tests/src/com/android/compose/theme/SystemUIThemeTest.kt
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/theme/SystemUIThemeTest.kt
@@ -40,7 +40,9 @@
@Test
fun testAndroidColorsAreAvailableInsideTheme() {
composeRule.setContent {
- PlatformTheme { Text("foo", color = LocalAndroidColorScheme.current.colorAccent) }
+ PlatformTheme {
+ Text("foo", color = LocalAndroidColorScheme.current.deprecated.colorAccent)
+ }
}
composeRule.onNodeWithText("foo").assertIsDisplayed()
@@ -50,7 +52,7 @@
fun testAccessingAndroidColorsWithoutThemeThrows() {
assertThrows(IllegalStateException::class.java) {
composeRule.setContent {
- Text("foo", color = LocalAndroidColorScheme.current.colorAccent)
+ Text("foo", color = LocalAndroidColorScheme.current.deprecated.colorAccent)
}
}
}
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt
index 24064b1..5413f90 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt
@@ -17,10 +17,12 @@
package com.android.systemui.scene.ui.composable
import com.android.systemui.scene.shared.model.Scene
+import com.android.systemui.scene.shared.model.SceneContainerNames
import dagger.Module
import dagger.multibindings.Multibinds
+import javax.inject.Named
@Module
interface SceneModule {
- @Multibinds fun scenes(): Set<Scene>
+ @Multibinds @Named(SceneContainerNames.SYSTEM_UI_DEFAULT) fun scenes(): Set<Scene>
}
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt
index ee53ece..954bad56 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt
@@ -17,22 +17,32 @@
package com.android.systemui.scene.ui.composable
import com.android.systemui.bouncer.ui.composable.BouncerScene
+import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.ui.composable.LockscreenScene
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel
import com.android.systemui.qs.ui.composable.QuickSettingsScene
+import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
import com.android.systemui.scene.shared.model.Scene
+import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.shade.ui.composable.ShadeScene
+import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
import dagger.Module
import dagger.Provides
+import javax.inject.Named
+import kotlinx.coroutines.CoroutineScope
@Module
object SceneModule {
@Provides
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
fun scenes(
- bouncer: BouncerScene,
- gone: GoneScene,
- lockScreen: LockscreenScene,
- qs: QuickSettingsScene,
- shade: ShadeScene,
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT) bouncer: BouncerScene,
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT) gone: GoneScene,
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT) lockScreen: LockscreenScene,
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT) qs: QuickSettingsScene,
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT) shade: ShadeScene,
): Set<Scene> {
return setOf(
bouncer,
@@ -42,4 +52,71 @@
shade,
)
}
+
+ @Provides
+ @SysUISingleton
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+ fun bouncerScene(
+ viewModelFactory: BouncerViewModel.Factory,
+ ): BouncerScene {
+ return BouncerScene(
+ viewModel =
+ viewModelFactory.create(
+ containerName = SceneContainerNames.SYSTEM_UI_DEFAULT,
+ ),
+ )
+ }
+
+ @Provides
+ @SysUISingleton
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+ fun goneScene(): GoneScene {
+ return GoneScene()
+ }
+
+ @Provides
+ @SysUISingleton
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+ fun lockscreenScene(
+ @Application applicationScope: CoroutineScope,
+ viewModelFactory: LockscreenSceneViewModel.Factory,
+ ): LockscreenScene {
+ return LockscreenScene(
+ applicationScope = applicationScope,
+ viewModel =
+ viewModelFactory.create(
+ containerName = SceneContainerNames.SYSTEM_UI_DEFAULT,
+ ),
+ )
+ }
+
+ @Provides
+ @SysUISingleton
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+ fun quickSettingsScene(
+ viewModelFactory: QuickSettingsSceneViewModel.Factory,
+ ): QuickSettingsScene {
+ return QuickSettingsScene(
+ viewModel =
+ viewModelFactory.create(
+ containerName = SceneContainerNames.SYSTEM_UI_DEFAULT,
+ ),
+ )
+ }
+
+ @Provides
+ @SysUISingleton
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+ fun shadeScene(
+ @Application applicationScope: CoroutineScope,
+ viewModelFactory: ShadeSceneViewModel.Factory,
+ ): ShadeScene {
+ return ShadeScene(
+ applicationScope = applicationScope,
+ viewModel =
+ viewModelFactory.create(
+ containerName = SceneContainerNames.SYSTEM_UI_DEFAULT,
+ ),
+ )
+ }
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index f48bab9..3c74ef5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -40,22 +40,17 @@
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.shared.model.UserAction
import com.android.systemui.scene.ui.composable.ComposableScene
-import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/** The bouncer scene displays authentication challenges like PIN, password, or pattern. */
-@SysUISingleton
-class BouncerScene
-@Inject
-constructor(
- private val viewModelFactory: BouncerViewModel.Factory,
+class BouncerScene(
+ private val viewModel: BouncerViewModel,
) : ComposableScene {
override val key = SceneKey.Bouncer
@@ -73,7 +68,7 @@
override fun Content(
containerName: String,
modifier: Modifier,
- ) = BouncerScene(viewModelFactory.create(containerName), modifier)
+ ) = BouncerScene(viewModel, modifier)
}
@Composable
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
index 7c07a8b..1065267 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
@@ -31,7 +31,6 @@
import androidx.compose.ui.unit.dp
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel
import com.android.systemui.scene.shared.model.Direction
@@ -39,7 +38,6 @@
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.shared.model.UserAction
import com.android.systemui.scene.ui.composable.ComposableScene
-import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -47,29 +45,22 @@
import kotlinx.coroutines.flow.stateIn
/** The lock screen scene shows when the device is locked. */
-@SysUISingleton
-class LockscreenScene
-@Inject
-constructor(
+class LockscreenScene(
@Application private val applicationScope: CoroutineScope,
- private val viewModelFactory: LockscreenSceneViewModel.Factory,
+ private val viewModel: LockscreenSceneViewModel,
) : ComposableScene {
override val key = SceneKey.Lockscreen
- private var unsafeViewModel: LockscreenSceneViewModel? = null
-
override fun destinationScenes(
containerName: String,
): StateFlow<Map<UserAction, SceneModel>> =
- getOrCreateViewModelSingleton(containerName).let { viewModel ->
- viewModel.upDestinationSceneKey
- .map { pageKey -> destinationScenes(up = pageKey) }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.Eagerly,
- initialValue = destinationScenes(up = viewModel.upDestinationSceneKey.value)
- )
- }
+ viewModel.upDestinationSceneKey
+ .map { pageKey -> destinationScenes(up = pageKey) }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = destinationScenes(up = viewModel.upDestinationSceneKey.value)
+ )
@Composable
override fun Content(
@@ -77,7 +68,7 @@
modifier: Modifier,
) {
LockscreenScene(
- viewModel = getOrCreateViewModelSingleton(containerName),
+ viewModel = viewModel,
modifier = modifier,
)
}
@@ -90,13 +81,6 @@
UserAction.Swipe(Direction.DOWN) to SceneModel(SceneKey.Shade)
)
}
-
- private fun getOrCreateViewModelSingleton(
- containerName: String,
- ): LockscreenSceneViewModel {
- return unsafeViewModel
- ?: viewModelFactory.create(containerName).also { unsafeViewModel = it }
- }
}
@Composable
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
index a74e56b..f88fc21 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -91,7 +91,7 @@
// Make sure to use the Android colors and not the default Material3 colors to have the exact
// same colors as the View implementation.
- val androidColors = LocalAndroidColorScheme.current
+ val androidColors = LocalAndroidColorScheme.current.deprecated
Surface(
color = androidColors.colorBackground,
contentColor = androidColors.textColorPrimary,
@@ -170,7 +170,7 @@
stringResource(headerTextResource),
Modifier.padding(start = 16.dp),
style = MaterialTheme.typography.labelLarge,
- color = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
+ color = LocalAndroidColorScheme.current.deprecated.colorAccentPrimaryVariant,
)
Spacer(Modifier.height(10.dp))
@@ -180,7 +180,7 @@
if (index > 0) {
item {
Divider(
- color = LocalAndroidColorScheme.current.colorBackground,
+ color = LocalAndroidColorScheme.current.deprecated.colorBackground,
thickness = 2.dp,
)
}
@@ -204,7 +204,7 @@
withTopCornerRadius: Boolean,
withBottomCornerRadius: Boolean,
) {
- val androidColors = LocalAndroidColorScheme.current
+ val androidColors = LocalAndroidColorScheme.current.deprecated
val cornerRadius = dimensionResource(R.dimen.people_space_widget_radius)
val topCornerRadius = if (withTopCornerRadius) cornerRadius else 0.dp
val bottomCornerRadius = if (withBottomCornerRadius) cornerRadius else 0.dp
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
index 0484ff4..1e6f4a2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
@@ -76,8 +76,8 @@
Modifier.fillMaxWidth().defaultMinSize(minHeight = 56.dp),
colors =
ButtonDefaults.buttonColors(
- containerColor = androidColors.colorAccentPrimary,
- contentColor = androidColors.textColorOnAccent,
+ containerColor = androidColors.deprecated.colorAccentPrimary,
+ contentColor = androidColors.deprecated.textColorOnAccent,
)
) {
Text(stringResource(R.string.got_it))
@@ -90,8 +90,8 @@
val androidColors = LocalAndroidColorScheme.current
Surface(
shape = RoundedCornerShape(28.dp),
- color = androidColors.colorSurface,
- contentColor = androidColors.textColorPrimary,
+ color = androidColors.deprecated.colorSurface,
+ contentColor = androidColors.deprecated.textColorPrimary,
) {
Row(
Modifier.padding(vertical = 20.dp, horizontal = 16.dp),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
index 349f5c3..75bf281 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
@@ -122,7 +122,7 @@
}
val backgroundColor = colorAttr(R.attr.underSurfaceColor)
- val contentColor = LocalAndroidColorScheme.current.textColorPrimary
+ val contentColor = LocalAndroidColorScheme.current.deprecated.textColorPrimary
val backgroundTopRadius = dimensionResource(R.dimen.qs_corner_radius)
val backgroundModifier =
remember(
@@ -287,7 +287,7 @@
number.toString(),
modifier = Modifier.align(Alignment.Center),
style = MaterialTheme.typography.bodyLarge,
- color = LocalAndroidColorScheme.current.textColorPrimary,
+ color = LocalAndroidColorScheme.current.deprecated.textColorPrimary,
// TODO(b/242040009): This should only use a standard text style instead and
// should not override the text size.
fontSize = 18.sp,
@@ -305,7 +305,7 @@
@Composable
private fun NewChangesDot(modifier: Modifier = Modifier) {
val contentDescription = stringResource(R.string.fgs_dot_content_description)
- val color = LocalAndroidColorScheme.current.colorAccentTertiary
+ val color = LocalAndroidColorScheme.current.deprecated.colorAccentTertiary
Canvas(modifier.size(12.dp).semantics { this.contentDescription = contentDescription }) {
drawCircle(color)
@@ -324,8 +324,9 @@
Expandable(
shape = CircleShape,
color = colorAttr(R.attr.underSurfaceColor),
- contentColor = LocalAndroidColorScheme.current.textColorSecondary,
- borderStroke = BorderStroke(1.dp, LocalAndroidColorScheme.current.colorBackground),
+ contentColor = LocalAndroidColorScheme.current.deprecated.textColorSecondary,
+ borderStroke =
+ BorderStroke(1.dp, LocalAndroidColorScheme.current.deprecated.colorBackground),
modifier = modifier.padding(horizontal = 4.dp),
onClick = onClick,
) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index 58db37e..30b80ca 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -27,24 +27,19 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.shared.model.UserAction
import com.android.systemui.scene.ui.composable.ComposableScene
-import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/** The Quick Settings (AKA "QS") scene shows the quick setting tiles. */
-@SysUISingleton
-class QuickSettingsScene
-@Inject
-constructor(
- private val viewModelFactory: QuickSettingsSceneViewModel.Factory,
+class QuickSettingsScene(
+ private val viewModel: QuickSettingsSceneViewModel,
) : ComposableScene {
override val key = SceneKey.QuickSettings
@@ -64,7 +59,7 @@
modifier: Modifier,
) {
QuickSettingsScene(
- viewModel = viewModelFactory.create(containerName),
+ viewModel = viewModel,
modifier = modifier,
)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
index b387463..0a4da1d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
@@ -23,12 +23,10 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.shared.model.UserAction
-import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -37,8 +35,7 @@
* "Gone" is not a real scene but rather the absence of scenes when we want to skip showing any
* content from the scene framework.
*/
-@SysUISingleton
-class GoneScene @Inject constructor() : ComposableScene {
+class GoneScene : ComposableScene {
override val key = SceneKey.Gone
override fun destinationScenes(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index e4513d0..20e1751 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -27,7 +27,6 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.SceneKey
@@ -35,7 +34,6 @@
import com.android.systemui.scene.shared.model.UserAction
import com.android.systemui.scene.ui.composable.ComposableScene
import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
-import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -43,29 +41,22 @@
import kotlinx.coroutines.flow.stateIn
/** The shade scene shows scrolling list of notifications and some of the quick setting tiles. */
-@SysUISingleton
-class ShadeScene
-@Inject
-constructor(
+class ShadeScene(
@Application private val applicationScope: CoroutineScope,
- private val viewModelFactory: ShadeSceneViewModel.Factory,
+ private val viewModel: ShadeSceneViewModel,
) : ComposableScene {
override val key = SceneKey.Shade
- private var unsafeViewModel: ShadeSceneViewModel? = null
-
override fun destinationScenes(
containerName: String,
): StateFlow<Map<UserAction, SceneModel>> =
- getOrCreateViewModelSingleton(containerName).let { viewModel ->
- viewModel.upDestinationSceneKey
- .map { sceneKey -> destinationScenes(up = sceneKey) }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.Eagerly,
- initialValue = destinationScenes(up = viewModel.upDestinationSceneKey.value),
- )
- }
+ viewModel.upDestinationSceneKey
+ .map { sceneKey -> destinationScenes(up = sceneKey) }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = destinationScenes(up = viewModel.upDestinationSceneKey.value),
+ )
@Composable
override fun Content(
@@ -73,7 +64,7 @@
modifier: Modifier,
) {
ShadeScene(
- viewModel = getOrCreateViewModelSingleton(containerName),
+ viewModel = viewModel,
modifier = modifier,
)
}
@@ -86,13 +77,6 @@
UserAction.Swipe(Direction.DOWN) to SceneModel(SceneKey.QuickSettings),
)
}
-
- private fun getOrCreateViewModelSingleton(
- containerName: String,
- ): ShadeSceneViewModel {
- return unsafeViewModel
- ?: viewModelFactory.create(containerName).also { unsafeViewModel = it }
- }
}
@Composable
diff --git a/packages/SystemUI/res/layout/auth_biometric_contents.xml b/packages/SystemUI/res/layout/auth_biometric_contents.xml
index b3b40f3..8169189 100644
--- a/packages/SystemUI/res/layout/auth_biometric_contents.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_contents.xml
@@ -13,7 +13,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
+<!-- TODO(b/251476085): inline in biometric_prompt_layout after Biometric*Views are un-flagged -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
diff --git a/packages/SystemUI/res/layout/auth_biometric_face_view.xml b/packages/SystemUI/res/layout/auth_biometric_face_view.xml
index be30f21..e3d0732 100644
--- a/packages/SystemUI/res/layout/auth_biometric_face_view.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_face_view.xml
@@ -13,7 +13,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
+<!-- TODO(b/251476085): remove after BiometricFaceView is un-flagged -->
<com.android.systemui.biometrics.AuthBiometricFaceView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contents"
diff --git a/packages/SystemUI/res/layout/auth_biometric_fingerprint_and_face_view.xml b/packages/SystemUI/res/layout/auth_biometric_fingerprint_and_face_view.xml
index 05ca2a7..896d836 100644
--- a/packages/SystemUI/res/layout/auth_biometric_fingerprint_and_face_view.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_fingerprint_and_face_view.xml
@@ -13,7 +13,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
+<!-- TODO(b/251476085): remove after BiometricFingerprintAndFaceView is un-flagged -->
<com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contents"
diff --git a/packages/SystemUI/res/layout/auth_biometric_fingerprint_view.xml b/packages/SystemUI/res/layout/auth_biometric_fingerprint_view.xml
index 01ea31f..e36f9796 100644
--- a/packages/SystemUI/res/layout/auth_biometric_fingerprint_view.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_fingerprint_view.xml
@@ -13,7 +13,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
+<!-- TODO(b/251476085): remove after BiometricFingerprintView is un-flagged -->
<com.android.systemui.biometrics.AuthBiometricFingerprintView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contents"
diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
new file mode 100644
index 0000000..05ff1b1
--- /dev/null
+++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
@@ -0,0 +1,176 @@
+<!--
+ ~ 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.
+ -->
+<com.android.systemui.biometrics.ui.BiometricPromptLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/contents"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="@integer/biometric_dialog_text_gravity"
+ android:singleLine="true"
+ android:marqueeRepeatLimit="1"
+ android:ellipsize="marquee"
+ android:importantForAccessibility="no"
+ style="@style/TextAppearance.AuthCredential.Title"/>
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="@integer/biometric_dialog_text_gravity"
+ android:singleLine="true"
+ android:marqueeRepeatLimit="1"
+ android:ellipsize="marquee"
+ style="@style/TextAppearance.AuthCredential.Subtitle"/>
+
+ <TextView
+ android:id="@+id/description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:scrollbars ="vertical"
+ android:importantForAccessibility="no"
+ style="@style/TextAppearance.AuthCredential.Description"/>
+
+ <Space android:id="@+id/space_above_icon"
+ android:layout_width="match_parent"
+ android:layout_height="48dp" />
+
+ <FrameLayout
+ android:id="@+id/biometric_icon_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center">
+
+ <com.airbnb.lottie.LottieAnimationView
+ android:id="@+id/biometric_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:contentDescription="@null"
+ android:scaleType="fitXY" />
+
+ <com.airbnb.lottie.LottieAnimationView
+ android:id="@+id/biometric_icon_overlay"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:contentDescription="@null"
+ android:scaleType="fitXY" />
+ </FrameLayout>
+
+ <!-- For sensors such as UDFPS, this view is used during custom measurement/layout to add extra
+ padding so that the biometric icon is always in the right physical position. -->
+ <Space android:id="@+id/space_below_icon"
+ android:layout_width="match_parent"
+ android:layout_height="12dp" />
+
+ <TextView
+ android:id="@+id/indicator"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="24dp"
+ android:textSize="12sp"
+ android:gravity="center_horizontal"
+ android:accessibilityLiveRegion="polite"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:scrollHorizontally="true"
+ android:fadingEdge="horizontal"
+ android:textColor="@color/biometric_dialog_gray"/>
+
+ <LinearLayout
+ android:id="@+id/button_bar"
+ android:layout_width="match_parent"
+ android:layout_height="88dp"
+ style="?android:attr/buttonBarStyle"
+ android:orientation="horizontal"
+ android:paddingTop="24dp">
+
+ <Space android:id="@+id/leftSpacer"
+ android:layout_width="8dp"
+ android:layout_height="match_parent"
+ android:visibility="visible" />
+
+ <!-- Negative Button, reserved for app -->
+ <Button android:id="@+id/button_negative"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_gravity="center_vertical"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:maxWidth="@dimen/biometric_dialog_button_negative_max_width"
+ android:visibility="gone"/>
+ <!-- Cancel Button, replaces negative button when biometric is accepted -->
+ <Button android:id="@+id/button_cancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_gravity="center_vertical"
+ android:maxWidth="@dimen/biometric_dialog_button_negative_max_width"
+ android:text="@string/cancel"
+ android:visibility="gone"/>
+ <!-- "Use Credential" Button, replaces if device credential is allowed -->
+ <Button android:id="@+id/button_use_credential"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_gravity="center_vertical"
+ android:maxWidth="@dimen/biometric_dialog_button_negative_max_width"
+ android:visibility="gone"/>
+
+ <Space android:id="@+id/middleSpacer"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:visibility="visible"/>
+
+ <!-- Positive Button -->
+ <Button android:id="@+id/button_confirm"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@*android:style/Widget.DeviceDefault.Button.Colored"
+ android:layout_gravity="center_vertical"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:maxWidth="@dimen/biometric_dialog_button_positive_max_width"
+ android:text="@string/biometric_dialog_confirm"
+ android:visibility="gone"/>
+ <!-- Try Again Button -->
+ <Button android:id="@+id/button_try_again"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@*android:style/Widget.DeviceDefault.Button.Colored"
+ android:layout_gravity="center_vertical"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:maxWidth="@dimen/biometric_dialog_button_positive_max_width"
+ android:text="@string/biometric_dialog_try_again"
+ android:visibility="gone"/>
+
+ <Space android:id="@+id/rightSpacer"
+ android:layout_width="8dp"
+ android:layout_height="match_parent"
+ android:visibility="visible" />
+ </LinearLayout>
+
+</com.android.systemui.biometrics.ui.BiometricPromptLayout>
diff --git a/packages/SystemUI/res/layout/screen_record_dialog.xml b/packages/SystemUI/res/layout/screen_record_dialog.xml
index ae052502..bbf3adf 100644
--- a/packages/SystemUI/res/layout/screen_record_dialog.xml
+++ b/packages/SystemUI/res/layout/screen_record_dialog.xml
@@ -73,7 +73,7 @@
android:tint="?android:attr/textColorSecondary"
android:layout_gravity="center"
android:layout_weight="0"
- android:layout_marginRight="@dimen/screenrecord_option_padding"/>
+ android:layout_marginEnd="@dimen/screenrecord_option_padding"/>
<Spinner
android:id="@+id/screen_recording_options"
android:layout_width="0dp"
@@ -106,7 +106,7 @@
android:src="@drawable/ic_touch"
android:tint="?android:attr/textColorSecondary"
android:layout_gravity="center"
- android:layout_marginRight="@dimen/screenrecord_option_padding"/>
+ android:layout_marginEnd="@dimen/screenrecord_option_padding"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 271fab1..a62dead 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1730,15 +1730,15 @@
<!-- Keyboard backlight indicator-->
<dimen name="backlight_indicator_root_corner_radius">48dp</dimen>
<dimen name="backlight_indicator_root_vertical_padding">8dp</dimen>
- <dimen name="backlight_indicator_root_horizontal_padding">4dp</dimen>
+ <dimen name="backlight_indicator_root_horizontal_padding">6dp</dimen>
<dimen name="backlight_indicator_icon_width">22dp</dimen>
<dimen name="backlight_indicator_icon_height">11dp</dimen>
- <dimen name="backlight_indicator_icon_left_margin">2dp</dimen>
+ <dimen name="backlight_indicator_icon_padding">10dp</dimen>
<dimen name="backlight_indicator_step_width">52dp</dimen>
<dimen name="backlight_indicator_step_height">40dp</dimen>
- <dimen name="backlight_indicator_step_horizontal_margin">4dp</dimen>
+ <dimen name="backlight_indicator_step_horizontal_margin">2dp</dimen>
<dimen name="backlight_indicator_step_small_radius">4dp</dimen>
- <dimen name="backlight_indicator_step_large_radius">48dp</dimen>
+ <dimen name="backlight_indicator_step_large_radius">28dp</dimen>
<!-- Broadcast dialog -->
<dimen name="broadcast_dialog_title_img_margin_top">18dp</dimen>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index eaeaabe..e5c9461 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -203,5 +203,8 @@
<item type="id" name="log_access_dialog_allow_button" />
<item type="id" name="log_access_dialog_deny_button" />
+
+ <!-- keyboard backlight indicator-->
+ <item type="id" name="backlight_icon" />
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 003f9b0..67fdb4c 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -353,7 +353,7 @@
<!-- Message shown when a biometric is authenticated, waiting for the user to confirm authentication [CHAR LIMIT=40]-->
<string name="biometric_dialog_tap_confirm">Tap Confirm to complete</string>
<!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]-->
- <string name="biometric_dialog_tap_confirm_with_face">Unlocked by face. Press the unlock icon to continue.</string>
+ <string name="biometric_dialog_tap_confirm_with_face">Unlocked by face.</string>
<!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]-->
<string name="biometric_dialog_tap_confirm_with_face_alt_1">Unlocked by face. Press to continue.</string>
<!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]-->
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt
index 1404053..682888f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt
@@ -21,42 +21,43 @@
import com.airbnb.lottie.LottieAnimationView
import com.android.systemui.R
import com.android.systemui.biometrics.AuthBiometricView.BiometricState
-import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATED
import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR
import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP
import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION
/** Face/Fingerprint combined icon animator for BiometricPrompt. */
-class AuthBiometricFingerprintAndFaceIconController(
- context: Context,
- iconView: LottieAnimationView,
- iconViewOverlay: LottieAnimationView
+open class AuthBiometricFingerprintAndFaceIconController(
+ context: Context,
+ iconView: LottieAnimationView,
+ iconViewOverlay: LottieAnimationView,
) : AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay) {
override val actsAsConfirmButton: Boolean = true
override fun shouldAnimateIconViewForTransition(
- @BiometricState oldState: Int,
- @BiometricState newState: Int
+ @BiometricState oldState: Int,
+ @BiometricState newState: Int
): Boolean = when (newState) {
STATE_PENDING_CONFIRMATION -> true
- STATE_AUTHENTICATED -> false
else -> super.shouldAnimateIconViewForTransition(oldState, newState)
}
@RawRes
override fun getAnimationForTransition(
- @BiometricState oldState: Int,
- @BiometricState newState: Int
+ @BiometricState oldState: Int,
+ @BiometricState newState: Int
): Int? = when (newState) {
STATE_PENDING_CONFIRMATION -> {
if (oldState == STATE_ERROR || oldState == STATE_HELP) {
R.raw.fingerprint_dialogue_error_to_unlock_lottie
+ } else if (oldState == STATE_PENDING_CONFIRMATION) {
+ // TODO(jbolinger): missing asset for this transition
+ // (unlocked icon to success checkmark)
+ R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie
} else {
R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie
}
}
- STATE_AUTHENTICATED -> null
else -> super.getAnimationForTransition(oldState, newState)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceView.kt
index 57ffd24..7ce74db 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceView.kt
@@ -40,11 +40,11 @@
override fun ignoreUnsuccessfulEventsFrom(@Modality modality: Int, unsuccessfulReason: String) =
modality == TYPE_FACE && !(isFaceClass3 && isLockoutErrorString(unsuccessfulReason))
- override fun onPointerDown(failedModalities: Set<Int>) = failedModalities.contains(TYPE_FACE)
-
override fun createIconController(): AuthIconController =
AuthBiometricFingerprintAndFaceIconController(mContext, mIconView, mIconViewOverlay)
+ override fun isCoex() = true
+
private fun isLockoutErrorString(unsuccessfulReason: String) =
unsuccessfulReason == FaceManager.getErrorString(
mContext,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
index 4db371b..fb160f2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
@@ -56,43 +56,42 @@
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
-import java.util.Set;
/**
* Contains the Biometric views (title, subtitle, icon, buttons, etc.) and its controllers.
*/
-public abstract class AuthBiometricView extends LinearLayout {
+public abstract class AuthBiometricView extends LinearLayout implements AuthBiometricViewAdapter {
private static final String TAG = "AuthBiometricView";
/**
* Authentication hardware idle.
*/
- protected static final int STATE_IDLE = 0;
+ public static final int STATE_IDLE = 0;
/**
* UI animating in, authentication hardware active.
*/
- protected static final int STATE_AUTHENTICATING_ANIMATING_IN = 1;
+ public static final int STATE_AUTHENTICATING_ANIMATING_IN = 1;
/**
* UI animated in, authentication hardware active.
*/
- protected static final int STATE_AUTHENTICATING = 2;
+ public static final int STATE_AUTHENTICATING = 2;
/**
* UI animated in, authentication hardware active.
*/
- protected static final int STATE_HELP = 3;
+ public static final int STATE_HELP = 3;
/**
* Hard error, e.g. ERROR_TIMEOUT. Authentication hardware idle.
*/
- protected static final int STATE_ERROR = 4;
+ public static final int STATE_ERROR = 4;
/**
* Authenticated, waiting for user confirmation. Authentication hardware idle.
*/
- protected static final int STATE_PENDING_CONFIRMATION = 5;
+ public static final int STATE_PENDING_CONFIRMATION = 5;
/**
* Authenticated, dialog animating away soon.
*/
- protected static final int STATE_AUTHENTICATED = 6;
+ public static final int STATE_AUTHENTICATED = 6;
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_IDLE, STATE_AUTHENTICATING_ANIMATING_IN, STATE_AUTHENTICATING, STATE_HELP,
@@ -102,13 +101,14 @@
/**
* Callback to the parent when a user action has occurred.
*/
- interface Callback {
+ public interface Callback {
int ACTION_AUTHENTICATED = 1;
int ACTION_USER_CANCELED = 2;
int ACTION_BUTTON_NEGATIVE = 3;
int ACTION_BUTTON_TRY_AGAIN = 4;
int ACTION_ERROR = 5;
int ACTION_USE_DEVICE_CREDENTIAL = 6;
+ int ACTION_START_DELAYED_FINGERPRINT_SENSOR = 7;
/**
* When an action has occurred. The caller will only invoke this when the callback should
@@ -268,6 +268,27 @@
/** Create the controller for managing the icons transitions during the prompt.*/
@NonNull
protected abstract AuthIconController createIconController();
+
+ @Override
+ public AuthIconController getLegacyIconController() {
+ return mIconController;
+ }
+
+ @Override
+ public void cancelAnimation() {
+ animate().cancel();
+ }
+
+ @Override
+ public View asView() {
+ return this;
+ }
+
+ @Override
+ public boolean isCoex() {
+ return false;
+ }
+
void setPanelController(AuthPanelController panelController) {
mPanelController = panelController;
}
@@ -544,12 +565,12 @@
mState = newState;
}
- void onOrientationChanged() {
+ public void onOrientationChanged() {
// Update padding and AuthPanel outline by calling updateSize when the orientation changed.
updateSize(mSize);
}
- public void onDialogAnimatedIn() {
+ public void onDialogAnimatedIn(boolean fingerprintWasStarted) {
updateState(STATE_AUTHENTICATING);
}
@@ -597,18 +618,6 @@
}
/**
- * Fingerprint pointer down event. This does nothing by default and will not be called if the
- * device does not have an appropriate sensor (UDFPS), but it may be used as an alternative
- * to the "retry" button when fingerprint is used with other modalities.
- *
- * @param failedModalities the set of modalities that have failed
- * @return true if a retry was initiated as a result of this event
- */
- public boolean onPointerDown(Set<Integer> failedModalities) {
- return false;
- }
-
- /**
* Show a help message to the user.
*
* @param modality sensor modality
@@ -752,7 +761,8 @@
/**
* Kicks off the animation process and invokes the callback.
*/
- void startTransitionToCredentialUI() {
+ @Override
+ public void startTransitionToCredentialUI() {
updateSize(AuthDialog.SIZE_LARGE);
mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL);
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricViewAdapter.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricViewAdapter.kt
new file mode 100644
index 0000000..631511c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricViewAdapter.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2022 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.systemui.biometrics
+
+import android.hardware.biometrics.BiometricAuthenticator
+import android.os.Bundle
+import android.view.View
+
+/** TODO(b/251476085): Temporary interface while legacy biometric prompt is around. */
+@Deprecated("temporary adapter while migrating biometric prompt - do not expand")
+interface AuthBiometricViewAdapter {
+ val legacyIconController: AuthIconController?
+
+ fun onDialogAnimatedIn(fingerprintWasStarted: Boolean)
+
+ fun onAuthenticationSucceeded(@BiometricAuthenticator.Modality modality: Int)
+
+ fun onAuthenticationFailed(
+ @BiometricAuthenticator.Modality modality: Int,
+ failureReason: String
+ )
+
+ fun onError(@BiometricAuthenticator.Modality modality: Int, error: String)
+
+ fun onHelp(@BiometricAuthenticator.Modality modality: Int, help: String)
+
+ fun startTransitionToCredentialUI()
+
+ fun requestLayout()
+
+ fun onSaveState(bundle: Bundle?)
+
+ fun restoreState(bundle: Bundle?)
+
+ fun onOrientationChanged()
+
+ fun cancelAnimation()
+
+ fun isCoex(): Boolean
+
+ fun asView(): View
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index e775c2e..49ac264 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -16,15 +16,13 @@
package com.android.systemui.biometrics;
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
-import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_BIOMETRIC_PROMPT_TRANSITION;
import android.animation.Animator;
-import android.annotation.DurationMillisLong;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -66,12 +64,19 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.R;
import com.android.systemui.biometrics.AuthController.ScaleFactorProvider;
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
+import com.android.systemui.biometrics.domain.model.BiometricModalities;
+import com.android.systemui.biometrics.ui.BiometricPromptLayout;
import com.android.systemui.biometrics.ui.CredentialView;
import com.android.systemui.biometrics.ui.binder.AuthBiometricFingerprintViewBinder;
+import com.android.systemui.biometrics.ui.binder.BiometricViewBinder;
import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel;
import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -84,6 +89,8 @@
import javax.inject.Provider;
+import kotlinx.coroutines.CoroutineScope;
+
/**
* Top level container/controller for the BiometricPrompt UI.
*/
@@ -126,16 +133,20 @@
private final WakefulnessLifecycle mWakefulnessLifecycle;
private final AuthDialogPanelInteractionDetector mPanelInteractionDetector;
private final InteractionJankMonitor mInteractionJankMonitor;
+ private final CoroutineScope mApplicationCoroutineScope;
// TODO: these should be migrated out once ready
- private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor;
+ private final Provider<PromptCredentialInteractor> mPromptCredentialInteractor;
private final Provider<AuthBiometricFingerprintViewModel>
mAuthBiometricFingerprintViewModelProvider;
+ private final @NonNull Provider<PromptSelectorInteractor> mPromptSelectorInteractorProvider;
+ // TODO(b/251476085): these should be migrated out of the view
private final Provider<CredentialViewModel> mCredentialViewModelProvider;
+ private final PromptViewModel mPromptViewModel;
@VisibleForTesting final BiometricCallback mBiometricCallback;
- @Nullable private AuthBiometricView mBiometricView;
+ @Nullable private AuthBiometricViewAdapter mBiometricView;
@Nullable private View mCredentialView;
private final AuthPanelController mPanelController;
private final FrameLayout mFrameLayout;
@@ -154,7 +165,8 @@
// HAT received from LockSettingsService when credential is verified.
@Nullable private byte[] mCredentialAttestation;
- @VisibleForTesting
+ // TODO(b/251476085): remove when legacy prompt is replaced
+ @Deprecated
static class Config {
Context mContext;
AuthDialogCallback mCallback;
@@ -167,96 +179,9 @@
long mOperationId;
long mRequestId = -1;
boolean mSkipAnimation = false;
- @BiometricMultiSensorMode int mMultiSensorConfig = BIOMETRIC_MULTI_SENSOR_DEFAULT;
ScaleFactorProvider mScaleProvider;
}
- public static class Builder {
- Config mConfig;
-
- public Builder(Context context) {
- mConfig = new Config();
- mConfig.mContext = context;
- }
-
- public Builder setCallback(AuthDialogCallback callback) {
- mConfig.mCallback = callback;
- return this;
- }
-
- public Builder setPromptInfo(PromptInfo promptInfo) {
- mConfig.mPromptInfo = promptInfo;
- return this;
- }
-
- public Builder setRequireConfirmation(boolean requireConfirmation) {
- mConfig.mRequireConfirmation = requireConfirmation;
- return this;
- }
-
- public Builder setUserId(int userId) {
- mConfig.mUserId = userId;
- return this;
- }
-
- public Builder setOpPackageName(String opPackageName) {
- mConfig.mOpPackageName = opPackageName;
- return this;
- }
-
- public Builder setSkipIntro(boolean skip) {
- mConfig.mSkipIntro = skip;
- return this;
- }
-
- public Builder setOperationId(@DurationMillisLong long operationId) {
- mConfig.mOperationId = operationId;
- return this;
- }
-
- /** Unique id for this request. */
- public Builder setRequestId(long requestId) {
- mConfig.mRequestId = requestId;
- return this;
- }
-
- @VisibleForTesting
- public Builder setSkipAnimationDuration(boolean skip) {
- mConfig.mSkipAnimation = skip;
- return this;
- }
-
- /** The multi-sensor mode. */
- public Builder setMultiSensorConfig(@BiometricMultiSensorMode int multiSensorConfig) {
- mConfig.mMultiSensorConfig = multiSensorConfig;
- return this;
- }
-
- public Builder setScaleFactorProvider(ScaleFactorProvider scaleProvider) {
- mConfig.mScaleProvider = scaleProvider;
- return this;
- }
-
- public AuthContainerView build(@Background DelayableExecutor bgExecutor, int[] sensorIds,
- @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
- @Nullable List<FaceSensorPropertiesInternal> faceProps,
- @NonNull WakefulnessLifecycle wakefulnessLifecycle,
- @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
- @NonNull UserManager userManager,
- @NonNull LockPatternUtils lockPatternUtils,
- @NonNull InteractionJankMonitor jankMonitor,
- @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor,
- @NonNull Provider<AuthBiometricFingerprintViewModel>
- authBiometricFingerprintViewModelProvider,
- @NonNull Provider<CredentialViewModel> credentialViewModelProvider) {
- mConfig.mSensorIds = sensorIds;
- return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle,
- panelInteractionDetector, userManager, lockPatternUtils, jankMonitor,
- biometricPromptInteractor, authBiometricFingerprintViewModelProvider,
- credentialViewModelProvider, new Handler(Looper.getMainLooper()), bgExecutor);
- }
- }
-
@VisibleForTesting
final class BiometricCallback implements AuthBiometricView.Callback {
@Override
@@ -285,6 +210,9 @@
addCredentialView(false /* animatePanel */, true /* animateContents */);
}, mConfig.mSkipAnimation ? 0 : AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS);
break;
+ case AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR:
+ mConfig.mCallback.onStartFingerprintNow(getRequestId());
+ break;
default:
Log.e(TAG, "Unhandled action: " + action);
}
@@ -336,8 +264,10 @@
alertDialog.show();
}
- @VisibleForTesting
- AuthContainerView(Config config,
+ // TODO(b/251476085): remove Config and further decompose these properties out of view classes
+ AuthContainerView(@NonNull Config config,
+ @NonNull FeatureFlags featureFlags,
+ @NonNull CoroutineScope applicationCoroutineScope,
@Nullable List<FingerprintSensorPropertiesInternal> fpProps,
@Nullable List<FaceSensorPropertiesInternal> faceProps,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@@ -345,9 +275,36 @@
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull InteractionJankMonitor jankMonitor,
- @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor,
@NonNull Provider<AuthBiometricFingerprintViewModel>
authBiometricFingerprintViewModelProvider,
+ @NonNull Provider<PromptCredentialInteractor> promptCredentialInteractor,
+ @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractor,
+ @NonNull PromptViewModel promptViewModel,
+ @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
+ @NonNull @Background DelayableExecutor bgExecutor) {
+ this(config, featureFlags, applicationCoroutineScope, fpProps, faceProps,
+ wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils,
+ jankMonitor, authBiometricFingerprintViewModelProvider, promptSelectorInteractor,
+ promptCredentialInteractor, promptViewModel, credentialViewModelProvider,
+ new Handler(Looper.getMainLooper()), bgExecutor);
+ }
+
+ @VisibleForTesting
+ AuthContainerView(@NonNull Config config,
+ @NonNull FeatureFlags featureFlags,
+ @NonNull CoroutineScope applicationCoroutineScope,
+ @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
+ @Nullable List<FaceSensorPropertiesInternal> faceProps,
+ @NonNull WakefulnessLifecycle wakefulnessLifecycle,
+ @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
+ @NonNull UserManager userManager,
+ @NonNull LockPatternUtils lockPatternUtils,
+ @NonNull InteractionJankMonitor jankMonitor,
+ @NonNull Provider<AuthBiometricFingerprintViewModel>
+ authBiometricFingerprintViewModelProvider,
+ @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider,
+ @NonNull Provider<PromptCredentialInteractor> credentialInteractor,
+ @NonNull PromptViewModel promptViewModel,
@NonNull Provider<CredentialViewModel> credentialViewModelProvider,
@NonNull Handler mainHandler,
@NonNull @Background DelayableExecutor bgExecutor) {
@@ -360,6 +317,7 @@
mWindowManager = mContext.getSystemService(WindowManager.class);
mWakefulnessLifecycle = wakefulnessLifecycle;
mPanelInteractionDetector = panelInteractionDetector;
+ mApplicationCoroutineScope = applicationCoroutineScope;
mTranslationY = getResources()
.getDimension(R.dimen.biometric_dialog_animation_translation_offset);
@@ -376,10 +334,70 @@
mPanelController = new AuthPanelController(mContext, mPanelView);
mBackgroundExecutor = bgExecutor;
mInteractionJankMonitor = jankMonitor;
- mBiometricPromptInteractor = biometricPromptInteractor;
+ mPromptCredentialInteractor = credentialInteractor;
mAuthBiometricFingerprintViewModelProvider = authBiometricFingerprintViewModelProvider;
+ mPromptSelectorInteractorProvider = promptSelectorInteractorProvider;
mCredentialViewModelProvider = credentialViewModelProvider;
+ mPromptViewModel = promptViewModel;
+ if (featureFlags.isEnabled(Flags.BIOMETRIC_BP_STRONG)) {
+ showPrompt(config, layoutInflater, promptViewModel,
+ Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds),
+ Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds));
+ } else {
+ showLegacyPrompt(config, layoutInflater, fpProps, faceProps);
+ }
+
+ // TODO: De-dupe the logic with AuthCredentialPasswordView
+ setOnKeyListener((v, keyCode, event) -> {
+ if (keyCode != KeyEvent.KEYCODE_BACK) {
+ return false;
+ }
+ if (event.getAction() == KeyEvent.ACTION_UP) {
+ onBackInvoked();
+ }
+ return true;
+ });
+
+ setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+ setFocusableInTouchMode(true);
+ requestFocus();
+ }
+
+ private void showPrompt(@NonNull Config config, @NonNull LayoutInflater layoutInflater,
+ @NonNull PromptViewModel viewModel,
+ @Nullable FingerprintSensorPropertiesInternal fpProps,
+ @Nullable FaceSensorPropertiesInternal faceProps) {
+ if (Utils.isBiometricAllowed(config.mPromptInfo)) {
+ mPromptSelectorInteractorProvider.get().useBiometricsForAuthentication(
+ config.mPromptInfo,
+ config.mRequireConfirmation,
+ config.mUserId,
+ config.mOperationId,
+ new BiometricModalities(fpProps, faceProps));
+
+ final BiometricPromptLayout view = (BiometricPromptLayout) layoutInflater.inflate(
+ R.layout.biometric_prompt_layout, null, false);
+ mBiometricView = BiometricViewBinder.bind(view, viewModel, mPanelController,
+ // TODO(b/201510778): This uses the wrong timeout in some cases
+ getJankListener(view, TRANSIT, AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
+ mBackgroundView, mBiometricCallback, mApplicationCoroutineScope);
+
+ // TODO(b/251476085): migrate these dependencies
+ if (fpProps != null && fpProps.isAnyUdfpsType()) {
+ view.setUdfpsAdapter(new UdfpsDialogMeasureAdapter(view, fpProps),
+ config.mScaleProvider);
+ }
+ } else {
+ mPromptSelectorInteractorProvider.get().resetPrompt();
+ }
+ }
+
+ // TODO(b/251476085): remove entirely
+ private void showLegacyPrompt(@NonNull Config config, @NonNull LayoutInflater layoutInflater,
+ @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
+ @Nullable List<FaceSensorPropertiesInternal> faceProps
+ ) {
// Inflate biometric view only if necessary.
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
final FingerprintSensorPropertiesInternal fpProperties =
@@ -421,31 +439,18 @@
// init view before showing
if (mBiometricView != null) {
- mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation);
- mBiometricView.setPanelController(mPanelController);
- mBiometricView.setPromptInfo(mConfig.mPromptInfo);
- mBiometricView.setCallback(mBiometricCallback);
- mBiometricView.setBackgroundView(mBackgroundView);
- mBiometricView.setUserId(mConfig.mUserId);
- mBiometricView.setEffectiveUserId(mEffectiveUserId);
- mBiometricView.setJankListener(getJankListener(mBiometricView, TRANSIT,
+ final AuthBiometricView view = (AuthBiometricView) mBiometricView;
+ view.setRequireConfirmation(mConfig.mRequireConfirmation);
+ view.setPanelController(mPanelController);
+ view.setPromptInfo(mConfig.mPromptInfo);
+ view.setCallback(mBiometricCallback);
+ view.setBackgroundView(mBackgroundView);
+ view.setUserId(mConfig.mUserId);
+ view.setEffectiveUserId(mEffectiveUserId);
+ // TODO(b/201510778): This uses the wrong timeout in some cases (remove w/ above)
+ view.setJankListener(getJankListener(view, TRANSIT,
AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS));
}
-
- // TODO: De-dupe the logic with AuthCredentialPasswordView
- setOnKeyListener((v, keyCode, event) -> {
- if (keyCode != KeyEvent.KEYCODE_BACK) {
- return false;
- }
- if (event.getAction() == KeyEvent.ACTION_UP) {
- onBackInvoked();
- }
- return true;
- });
-
- setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
- setFocusableInTouchMode(true);
- requestFocus();
}
private void onBackInvoked() {
@@ -495,7 +500,7 @@
mBackgroundView.setOnClickListener(null);
mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
- mBiometricPromptInteractor.get().useCredentialsForAuthentication(
+ mPromptSelectorInteractorProvider.get().useCredentialsForAuthentication(
mConfig.mPromptInfo, credentialType, mConfig.mUserId, mConfig.mOperationId);
final CredentialViewModel vm = mCredentialViewModelProvider.get();
vm.setAnimateContents(animateContents);
@@ -527,7 +532,7 @@
() -> animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED));
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
- mBiometricScrollView.addView(mBiometricView);
+ mBiometricScrollView.addView(mBiometricView.asView());
} else if (Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) {
addCredentialView(true /* animatePanel */, false /* animateContents */);
} else {
@@ -601,9 +606,13 @@
}
private static boolean shouldUpdatePositionForUdfps(@NonNull View view) {
+ // TODO(b/251476085): legacy view (delete when removed)
if (view instanceof AuthBiometricFingerprintView) {
return ((AuthBiometricFingerprintView) view).isUdfps();
}
+ if (view instanceof BiometricPromptLayout) {
+ return ((BiometricPromptLayout) view).isUdfps();
+ }
return false;
}
@@ -613,7 +622,7 @@
if (display == null) {
return false;
}
- if (!shouldUpdatePositionForUdfps(mBiometricView)) {
+ if (mBiometricView == null || !shouldUpdatePositionForUdfps(mBiometricView.asView())) {
return false;
}
@@ -626,12 +635,12 @@
case Surface.ROTATION_90:
mPanelController.setPosition(AuthPanelController.POSITION_RIGHT);
- setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
+ setScrollViewGravity(Gravity.BOTTOM | Gravity.RIGHT);
break;
case Surface.ROTATION_270:
mPanelController.setPosition(AuthPanelController.POSITION_LEFT);
- setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT);
+ setScrollViewGravity(Gravity.BOTTOM | Gravity.LEFT);
break;
case Surface.ROTATION_180:
@@ -689,7 +698,7 @@
mCredentialView.animate().cancel();
}
mPanelView.animate().cancel();
- mBiometricView.animate().cancel();
+ mBiometricView.cancelAnimation();
animate().cancel();
onDialogAnimatedIn();
}
@@ -750,8 +759,9 @@
@Override
public void onPointerDown() {
if (mBiometricView != null) {
- if (mBiometricView.onPointerDown(mFailedModalities)) {
+ if (mFailedModalities.contains(TYPE_FACE)) {
Log.d(TAG, "retrying failed modalities (pointer down)");
+ mFailedModalities.remove(TYPE_FACE);
mBiometricCallback.onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN);
}
} else {
@@ -885,11 +895,17 @@
}
mContainerState = STATE_SHOWING;
if (mBiometricView != null) {
- mConfig.mCallback.onDialogAnimatedIn(getRequestId());
- mBiometricView.onDialogAnimatedIn();
+ final boolean delayFingerprint = mBiometricView.isCoex() && !mConfig.mRequireConfirmation;
+ mConfig.mCallback.onDialogAnimatedIn(getRequestId(), !delayFingerprint);
+ mBiometricView.onDialogAnimatedIn(!delayFingerprint);
}
}
+ @Override
+ public PromptViewModel getViewModel() {
+ return mPromptViewModel;
+ }
+
@VisibleForTesting
static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, CharSequence title) {
final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
@@ -922,26 +938,5 @@
if (mConfig != null) {
pw.println(" config.sensorIds exist=" + (mConfig.mSensorIds != null));
}
- final AuthBiometricView biometricView = mBiometricView;
- pw.println(" scrollView=" + findViewById(R.id.biometric_scrollview));
- pw.println(" biometricView=" + biometricView);
- if (biometricView != null) {
- int[] ids = {
- R.id.title,
- R.id.subtitle,
- R.id.description,
- R.id.biometric_icon_frame,
- R.id.biometric_icon,
- R.id.indicator,
- R.id.button_bar,
- R.id.button_negative,
- R.id.button_use_credential,
- R.id.button_confirm,
- R.id.button_try_again
- };
- for (final int id: ids) {
- pw.println(" " + biometricView.findViewById(id));
- }
- }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index fd9cee0..57f1928 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -37,7 +37,6 @@
import android.hardware.biometrics.BiometricAuthenticator.Modality;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricManager.Authenticators;
-import android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.BiometricStateListener;
import android.hardware.biometrics.IBiometricContextListener;
@@ -71,14 +70,18 @@
import com.android.settingslib.udfps.UdfpsOverlayParams;
import com.android.settingslib.udfps.UdfpsUtils;
import com.android.systemui.CoreStartable;
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
import com.android.systemui.biometrics.domain.interactor.LogContextInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel;
import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.doze.DozeReceiver;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.keyguard.data.repository.BiometricType;
import com.android.systemui.statusbar.CommandQueue;
@@ -86,8 +89,6 @@
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.concurrency.Execution;
-import kotlin.Unit;
-
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -101,6 +102,9 @@
import javax.inject.Inject;
import javax.inject.Provider;
+import kotlin.Unit;
+import kotlinx.coroutines.CoroutineScope;
+
/**
* Receives messages sent from {@link com.android.server.biometrics.BiometricService} and shows the
* appropriate biometric UI (e.g. BiometricDialogView).
@@ -109,7 +113,7 @@
* {@link com.android.keyguard.KeyguardUpdateMonitor}
*/
@SysUISingleton
-public class AuthController implements CoreStartable, CommandQueue.Callbacks,
+public class AuthController implements CoreStartable, CommandQueue.Callbacks,
AuthDialogCallback, DozeReceiver {
private static final String TAG = "AuthController";
@@ -118,6 +122,7 @@
private final Handler mHandler;
private final Context mContext;
+ private final FeatureFlags mFeatureFlags;
private final Execution mExecution;
private final CommandQueue mCommandQueue;
private final ActivityTaskManager mActivityTaskManager;
@@ -125,13 +130,15 @@
@Nullable private final FaceManager mFaceManager;
private final Provider<UdfpsController> mUdfpsControllerFactory;
private final Provider<SideFpsController> mSidefpsControllerFactory;
+ private final CoroutineScope mApplicationCoroutineScope;
// TODO: these should be migrated out once ready
- @NonNull private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor;
-
@NonNull private final Provider<AuthBiometricFingerprintViewModel>
mAuthBiometricFingerprintViewModelProvider;
+ @NonNull private final Provider<PromptCredentialInteractor> mPromptCredentialInteractor;
+ @NonNull private final Provider<PromptSelectorInteractor> mPromptSelectorInteractor;
@NonNull private final Provider<CredentialViewModel> mCredentialViewModelProvider;
+ @NonNull private final Provider<PromptViewModel> mPromptViewModelProvider;
@NonNull private final LogContextInteractor mLogContextInteractor;
private final Display mDisplay;
@@ -461,7 +468,7 @@
}
@Override
- public void onDialogAnimatedIn(long requestId) {
+ public void onDialogAnimatedIn(long requestId, boolean startFingerprintNow) {
final IBiometricSysuiReceiver receiver = getCurrentReceiver(requestId);
if (receiver == null) {
Log.w(TAG, "Skip onDialogAnimatedIn");
@@ -469,7 +476,22 @@
}
try {
- receiver.onDialogAnimatedIn();
+ receiver.onDialogAnimatedIn(startFingerprintNow);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException when sending onDialogAnimatedIn", e);
+ }
+ }
+
+ @Override
+ public void onStartFingerprintNow(long requestId) {
+ final IBiometricSysuiReceiver receiver = getCurrentReceiver(requestId);
+ if (receiver == null) {
+ Log.e(TAG, "onStartUdfpsNow: Receiver is null");
+ return;
+ }
+
+ try {
+ receiver.onStartFingerprintNow();
} catch (RemoteException e) {
Log.e(TAG, "RemoteException when sending onDialogAnimatedIn", e);
}
@@ -728,6 +750,8 @@
}
@Inject
public AuthController(Context context,
+ @NonNull FeatureFlags featureFlags,
+ @Application CoroutineScope applicationCoroutineScope,
Execution execution,
CommandQueue commandQueue,
ActivityTaskManager activityTaskManager,
@@ -743,16 +767,19 @@
@NonNull LockPatternUtils lockPatternUtils,
@NonNull UdfpsLogger udfpsLogger,
@NonNull LogContextInteractor logContextInteractor,
- @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor,
@NonNull Provider<AuthBiometricFingerprintViewModel>
authBiometricFingerprintViewModelProvider,
+ @NonNull Provider<PromptCredentialInteractor> promptCredentialInteractorProvider,
+ @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider,
@NonNull Provider<CredentialViewModel> credentialViewModelProvider,
+ @NonNull Provider<PromptViewModel> promptViewModelProvider,
@NonNull InteractionJankMonitor jankMonitor,
@Main Handler handler,
@Background DelayableExecutor bgExecutor,
@NonNull VibratorHelper vibrator,
@NonNull UdfpsUtils udfpsUtils) {
mContext = context;
+ mFeatureFlags = featureFlags;
mExecution = execution;
mUserManager = userManager;
mLockPatternUtils = lockPatternUtils;
@@ -773,10 +800,13 @@
mFaceEnrolledForUser = new SparseBooleanArray();
mVibratorHelper = vibrator;
mUdfpsUtils = udfpsUtils;
+ mApplicationCoroutineScope = applicationCoroutineScope;
mLogContextInteractor = logContextInteractor;
- mBiometricPromptInteractor = biometricPromptInteractor;
mAuthBiometricFingerprintViewModelProvider = authBiometricFingerprintViewModelProvider;
+ mPromptSelectorInteractor = promptSelectorInteractorProvider;
+ mPromptCredentialInteractor = promptCredentialInteractorProvider;
+ mPromptViewModelProvider = promptViewModelProvider;
mCredentialViewModelProvider = credentialViewModelProvider;
mOrientationListener = new BiometricDisplayListener(
@@ -913,8 +943,7 @@
@Override
public void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver,
int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation,
- int userId, long operationId, String opPackageName, long requestId,
- @BiometricMultiSensorMode int multiSensorConfig) {
+ int userId, long operationId, String opPackageName, long requestId) {
@Authenticators.Types final int authenticators = promptInfo.getAuthenticators();
if (DEBUG) {
@@ -927,8 +956,7 @@
+ ", credentialAllowed: " + credentialAllowed
+ ", requireConfirmation: " + requireConfirmation
+ ", operationId: " + operationId
- + ", requestId: " + requestId
- + ", multiSensorConfig: " + multiSensorConfig);
+ + ", requestId: " + requestId);
}
SomeArgs args = SomeArgs.obtain();
args.arg1 = promptInfo;
@@ -940,7 +968,6 @@
args.arg6 = opPackageName;
args.argl1 = operationId;
args.argl2 = requestId;
- args.argi2 = multiSensorConfig;
boolean skipAnimation = false;
if (mCurrentDialog != null) {
@@ -948,7 +975,7 @@
skipAnimation = true;
}
- showDialog(args, skipAnimation, null /* savedState */);
+ showDialog(args, skipAnimation, null /* savedState */, mPromptViewModelProvider.get());
}
/**
@@ -1171,7 +1198,8 @@
return mFpEnrolledForUser.getOrDefault(userId, false);
}
- private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
+ private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState,
+ @Nullable PromptViewModel viewModel) {
mCurrentDialogArgs = args;
final PromptInfo promptInfo = (PromptInfo) args.arg1;
@@ -1182,7 +1210,6 @@
final String opPackageName = (String) args.arg6;
final long operationId = args.argl1;
final long requestId = args.argl2;
- @BiometricMultiSensorMode final int multiSensorConfig = args.argi2;
// Create a new dialog but do not replace the current one yet.
final AuthDialog newDialog = buildDialog(
@@ -1195,11 +1222,11 @@
skipAnimation,
operationId,
requestId,
- multiSensorConfig,
mWakefulnessLifecycle,
mPanelInteractionDetector,
mUserManager,
- mLockPatternUtils);
+ mLockPatternUtils,
+ viewModel);
if (newDialog == null) {
Log.e(TAG, "Unsupported type configuration");
@@ -1253,6 +1280,7 @@
// Save the state of the current dialog (buttons showing, etc)
if (mCurrentDialog != null) {
+ final PromptViewModel viewModel = mCurrentDialog.getViewModel();
final Bundle savedState = new Bundle();
mCurrentDialog.onSaveState(savedState);
mCurrentDialog.dismissWithoutCallback(false /* animate */);
@@ -1271,7 +1299,7 @@
promptInfo.setAuthenticators(Authenticators.DEVICE_CREDENTIAL);
}
- showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState);
+ showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState, viewModel);
}
}
}
@@ -1286,26 +1314,28 @@
protected AuthDialog buildDialog(@Background DelayableExecutor bgExecutor,
PromptInfo promptInfo, boolean requireConfirmation, int userId, int[] sensorIds,
String opPackageName, boolean skipIntro, long operationId, long requestId,
- @BiometricMultiSensorMode int multiSensorConfig,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
@NonNull UserManager userManager,
- @NonNull LockPatternUtils lockPatternUtils) {
- return new AuthContainerView.Builder(mContext)
- .setCallback(this)
- .setPromptInfo(promptInfo)
- .setRequireConfirmation(requireConfirmation)
- .setUserId(userId)
- .setOpPackageName(opPackageName)
- .setSkipIntro(skipIntro)
- .setOperationId(operationId)
- .setRequestId(requestId)
- .setMultiSensorConfig(multiSensorConfig)
- .setScaleFactorProvider(() -> getScaleFactor())
- .build(bgExecutor, sensorIds, mFpProps, mFaceProps, wakefulnessLifecycle,
- panelInteractionDetector, userManager, lockPatternUtils,
- mInteractionJankMonitor, mBiometricPromptInteractor,
- mAuthBiometricFingerprintViewModelProvider, mCredentialViewModelProvider);
+ @NonNull LockPatternUtils lockPatternUtils,
+ @NonNull PromptViewModel viewModel) {
+ final AuthContainerView.Config config = new AuthContainerView.Config();
+ config.mContext = mContext;
+ config.mCallback = this;
+ config.mPromptInfo = promptInfo;
+ config.mRequireConfirmation = requireConfirmation;
+ config.mUserId = userId;
+ config.mOpPackageName = opPackageName;
+ config.mSkipIntro = skipIntro;
+ config.mOperationId = operationId;
+ config.mRequestId = requestId;
+ config.mSensorIds = sensorIds;
+ config.mScaleProvider = this::getScaleFactor;
+ return new AuthContainerView(config, mFeatureFlags, mApplicationCoroutineScope, mFpProps, mFaceProps,
+ wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils,
+ mInteractionJankMonitor, mAuthBiometricFingerprintViewModelProvider,
+ mPromptCredentialInteractor, mPromptSelectorInteractor, viewModel,
+ mCredentialViewModelProvider, bgExecutor);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
index 51f39b3..b6eabfa 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
@@ -24,13 +24,17 @@
import android.view.WindowManager;
import com.android.systemui.Dumpable;
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Interface for the biometric dialog UI.
+ *
+ * TODO(b/251476085): remove along with legacy controller once flag is removed
*/
+@Deprecated
public interface AuthDialog extends Dumpable {
String KEY_CONTAINER_GOING_AWAY = "container_going_away";
@@ -70,10 +74,10 @@
* {@link AuthPanelController}.
*/
class LayoutParams {
- final int mMediumHeight;
- final int mMediumWidth;
+ public final int mMediumHeight;
+ public final int mMediumWidth;
- LayoutParams(int mediumWidth, int mediumHeight) {
+ public LayoutParams(int mediumWidth, int mediumHeight) {
mMediumWidth = mediumWidth;
mMediumHeight = mediumHeight;
}
@@ -172,4 +176,6 @@
* must remain fixed on the physical sensor location.
*/
void onOrientationChanged();
+
+ PromptViewModel getViewModel();
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
index bbe461a..9a21940 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
@@ -70,5 +70,10 @@
/**
* Notifies when the dialog has finished animating.
*/
- void onDialogAnimatedIn(long requestId);
+ void onDialogAnimatedIn(long requestId, boolean startFingerprintNow);
+
+ /**
+ * Notifies that the fingerprint sensor should be started now.
+ */
+ void onStartFingerprintNow(long requestId);
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt
index f5f4640..f56bb88 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt
@@ -84,9 +84,6 @@
}
}
- /** If the icon should act as a "retry" button in the [currentState]. */
- fun iconTapSendsRetryWhen(@BiometricState currentState: Int): Boolean = false
-
/** Call during [updateState] if the controller is not [deactivated]. */
abstract fun updateIcon(@BiometricState lastState: Int, @BiometricState newState: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
index ad10071..acdde34 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
@@ -114,16 +114,7 @@
}
private int getTopBound(@Position int position) {
- switch (position) {
- case POSITION_BOTTOM:
- return Math.max(mContainerHeight - mContentHeight - mMargin, mMargin);
- case POSITION_LEFT:
- case POSITION_RIGHT:
- return Math.max((mContainerHeight - mContentHeight) / 2, mMargin);
- default:
- Log.e(TAG, "Unrecognized position: " + position);
- return getTopBound(POSITION_BOTTOM);
- }
+ return Math.max(mContainerHeight - mContentHeight - mMargin, mMargin);
}
public void setContainerDimensions(int containerWidth, int containerHeight) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
index 43745bf..16dc42a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
@@ -63,7 +63,7 @@
}
@NonNull
- AuthDialog.LayoutParams onMeasureInternal(
+ public AuthDialog.LayoutParams onMeasureInternal(
int width, int height, @NonNull AuthDialog.LayoutParams layoutParams,
float scaleFactor) {
@@ -86,7 +86,7 @@
* too cleanly support this case. So, let's have the onLayout code translate the sensor location
* instead.
*/
- int getBottomSpacerHeight() {
+ public int getBottomSpacerHeight() {
return mBottomSpacerHeight;
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
index 096d941..ddf1457 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
@@ -31,6 +31,8 @@
import com.android.systemui.biometrics.domain.interactor.LogContextInteractorImpl
import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor
import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.util.concurrency.ThreadFactory
import dagger.Binds
@@ -57,6 +59,11 @@
@Binds
@SysUISingleton
+ fun providesPromptSelectorInteractor(impl: PromptSelectorInteractorImpl):
+ PromptSelectorInteractor
+
+ @Binds
+ @SysUISingleton
fun providesCredentialInteractor(impl: CredentialInteractorImpl): CredentialInteractor
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
index 92a13cf..b4dc272 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
@@ -2,7 +2,7 @@
import android.hardware.biometrics.PromptInfo
import com.android.systemui.biometrics.AuthController
-import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.biometrics.shared.model.PromptKind
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
@@ -35,12 +35,20 @@
/** The kind of credential to use (biometric, pin, pattern, etc.). */
val kind: StateFlow<PromptKind>
+ /**
+ * If explicit confirmation is required.
+ *
+ * Note: overlaps/conflicts with [PromptInfo.isConfirmationRequested], which needs clean up.
+ */
+ val isConfirmationRequired: StateFlow<Boolean>
+
/** Update the prompt configuration, which should be set before [isShowing]. */
fun setPrompt(
promptInfo: PromptInfo,
userId: Int,
gatekeeperChallenge: Long?,
- kind: PromptKind = PromptKind.ANY_BIOMETRIC,
+ kind: PromptKind,
+ requireConfirmation: Boolean = false,
)
/** Unset the prompt info. */
@@ -74,29 +82,35 @@
private val _userId: MutableStateFlow<Int?> = MutableStateFlow(null)
override val userId = _userId.asStateFlow()
- private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.ANY_BIOMETRIC)
+ private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.Biometric())
override val kind = _kind.asStateFlow()
+ private val _isConfirmationRequired: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ override val isConfirmationRequired = _isConfirmationRequired.asStateFlow()
+
override fun setPrompt(
promptInfo: PromptInfo,
userId: Int,
gatekeeperChallenge: Long?,
kind: PromptKind,
+ requireConfirmation: Boolean,
) {
_kind.value = kind
_userId.value = userId
_challenge.value = gatekeeperChallenge
_promptInfo.value = promptInfo
+ _isConfirmationRequired.value = requireConfirmation
}
override fun unsetPrompt() {
_promptInfo.value = null
_userId.value = null
_challenge.value = null
- _kind.value = PromptKind.ANY_BIOMETRIC
+ _kind.value = PromptKind.Biometric()
+ _isConfirmationRequired.value = false
}
companion object {
- private const val TAG = "BiometricPromptRepository"
+ private const val TAG = "PromptRepositoryImpl"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
index 6362c2f..d92c217 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
@@ -1,14 +1,30 @@
+/*
+ * 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.systemui.biometrics.domain.interactor
import android.hardware.biometrics.PromptInfo
import com.android.internal.widget.LockPatternView
import com.android.internal.widget.LockscreenCredential
import com.android.systemui.biometrics.Utils
-import com.android.systemui.biometrics.data.model.PromptKind
import com.android.systemui.biometrics.data.repository.PromptRepository
import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
import com.android.systemui.biometrics.domain.model.BiometricUserInfo
+import com.android.systemui.biometrics.shared.model.PromptKind
import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -24,8 +40,16 @@
/**
* Business logic for BiometricPrompt's CredentialViews, which primarily includes checking a users
* PIN, pattern, or password credential instead of a biometric.
+ *
+ * This is used to cache the calling app's options that were given to the underlying authenticate
+ * APIs and should be set before any UI is shown to the user.
+ *
+ * There can be at most one request active at a given time. Use [resetPrompt] when no request is
+ * active to clear the cache.
+ *
+ * Views that use any biometric should use [PromptSelectorInteractor] instead.
*/
-class BiometricPromptCredentialInteractor
+class PromptCredentialInteractor
@Inject
constructor(
@Background private val bgDispatcher: CoroutineDispatcher,
@@ -36,7 +60,7 @@
val isShowing: Flow<Boolean> = biometricPromptRepository.isShowing
/** Metadata about the current credential prompt, including app-supplied preferences. */
- val prompt: Flow<BiometricPromptRequest?> =
+ val prompt: Flow<BiometricPromptRequest.Credential?> =
combine(
biometricPromptRepository.promptInfo,
biometricPromptRepository.challenge,
@@ -48,20 +72,20 @@
}
when (kind) {
- PromptKind.PIN ->
+ PromptKind.Pin ->
BiometricPromptRequest.Credential.Pin(
info = promptInfo,
userInfo = userInfo(userId),
operationInfo = operationInfo(challenge)
)
- PromptKind.PATTERN ->
+ PromptKind.Pattern ->
BiometricPromptRequest.Credential.Pattern(
info = promptInfo,
userInfo = userInfo(userId),
operationInfo = operationInfo(challenge),
stealthMode = credentialInteractor.isStealthModeActive(userId)
)
- PromptKind.PASSWORD ->
+ PromptKind.Password ->
BiometricPromptRequest.Credential.Password(
info = promptInfo,
userInfo = userInfo(userId),
@@ -182,8 +206,8 @@
/** Convert a [Utils.CredentialType] to the corresponding [PromptKind]. */
private fun @receiver:Utils.CredentialType Int.asBiometricPromptCredential(): PromptKind =
when (this) {
- Utils.CREDENTIAL_PIN -> PromptKind.PIN
- Utils.CREDENTIAL_PASSWORD -> PromptKind.PASSWORD
- Utils.CREDENTIAL_PATTERN -> PromptKind.PATTERN
- else -> PromptKind.ANY_BIOMETRIC
+ Utils.CREDENTIAL_PIN -> PromptKind.Pin
+ Utils.CREDENTIAL_PASSWORD -> PromptKind.Password
+ Utils.CREDENTIAL_PATTERN -> PromptKind.Pattern
+ else -> PromptKind.Biometric()
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
new file mode 100644
index 0000000..e6e07f9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
@@ -0,0 +1,186 @@
+/*
+ * 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.systemui.biometrics.domain.interactor
+
+import android.hardware.biometrics.PromptInfo
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.Utils.getCredentialType
+import com.android.systemui.biometrics.Utils.isDeviceCredentialAllowed
+import com.android.systemui.biometrics.data.repository.PromptRepository
+import com.android.systemui.biometrics.domain.model.BiometricModalities
+import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
+import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
+import com.android.systemui.biometrics.domain.model.BiometricUserInfo
+import com.android.systemui.biometrics.shared.model.PromptKind
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+/**
+ * Business logic for BiometricPrompt's biometric view variants (face, fingerprint, coex, etc.).
+ *
+ * This is used to cache the calling app's options that were given to the underlying authenticate
+ * APIs and should be set before any UI is shown to the user.
+ *
+ * There can be at most one request active at a given time. Use [resetPrompt] when no request is
+ * active to clear the cache.
+ *
+ * Views that use credential fallback should use [PromptCredentialInteractor] instead.
+ */
+interface PromptSelectorInteractor {
+
+ /** Static metadata about the current prompt. */
+ val prompt: Flow<BiometricPromptRequest.Biometric?>
+
+ /** If using a credential is allowed. */
+ val isCredentialAllowed: Flow<Boolean>
+
+ /**
+ * The kind of credential the user may use as a fallback or [PromptKind.Biometric] if unknown or
+ * not [isCredentialAllowed].
+ */
+ val credentialKind: Flow<PromptKind>
+
+ /** If the API caller requested explicit confirmation after successful authentication. */
+ val isConfirmationRequested: Flow<Boolean>
+
+ /** Use biometrics for authentication. */
+ fun useBiometricsForAuthentication(
+ promptInfo: PromptInfo,
+ requireConfirmation: Boolean,
+ userId: Int,
+ challenge: Long,
+ modalities: BiometricModalities,
+ )
+
+ /** Use credential-based authentication instead of biometrics. */
+ fun useCredentialsForAuthentication(
+ promptInfo: PromptInfo,
+ @Utils.CredentialType kind: Int,
+ userId: Int,
+ challenge: Long,
+ )
+
+ /** Unset the current authentication request. */
+ fun resetPrompt()
+}
+
+@SysUISingleton
+class PromptSelectorInteractorImpl
+@Inject
+constructor(
+ private val promptRepository: PromptRepository,
+ lockPatternUtils: LockPatternUtils,
+) : PromptSelectorInteractor {
+
+ override val prompt: Flow<BiometricPromptRequest.Biometric?> =
+ combine(
+ promptRepository.promptInfo,
+ promptRepository.challenge,
+ promptRepository.userId,
+ promptRepository.kind
+ ) { promptInfo, challenge, userId, kind ->
+ if (promptInfo == null || userId == null || challenge == null) {
+ return@combine null
+ }
+
+ when (kind) {
+ is PromptKind.Biometric ->
+ BiometricPromptRequest.Biometric(
+ info = promptInfo,
+ userInfo = BiometricUserInfo(userId = userId),
+ operationInfo = BiometricOperationInfo(gatekeeperChallenge = challenge),
+ modalities = kind.activeModalities,
+ )
+ else -> null
+ }
+ }
+
+ override val isConfirmationRequested: Flow<Boolean> =
+ promptRepository.promptInfo
+ .map { info -> info?.isConfirmationRequested ?: false }
+ .distinctUntilChanged()
+
+ override val isCredentialAllowed: Flow<Boolean> =
+ promptRepository.promptInfo
+ .map { info -> if (info != null) isDeviceCredentialAllowed(info) else false }
+ .distinctUntilChanged()
+
+ override val credentialKind: Flow<PromptKind> =
+ combine(prompt, isCredentialAllowed) { prompt, isAllowed ->
+ if (prompt != null && isAllowed) {
+ when (
+ getCredentialType(lockPatternUtils, prompt.userInfo.deviceCredentialOwnerId)
+ ) {
+ Utils.CREDENTIAL_PIN -> PromptKind.Pin
+ Utils.CREDENTIAL_PASSWORD -> PromptKind.Password
+ Utils.CREDENTIAL_PATTERN -> PromptKind.Pattern
+ else -> PromptKind.Biometric()
+ }
+ } else {
+ PromptKind.Biometric()
+ }
+ }
+
+ override fun useBiometricsForAuthentication(
+ promptInfo: PromptInfo,
+ requireConfirmation: Boolean,
+ userId: Int,
+ challenge: Long,
+ modalities: BiometricModalities
+ ) {
+ promptRepository.setPrompt(
+ promptInfo = promptInfo,
+ userId = userId,
+ gatekeeperChallenge = challenge,
+ kind = PromptKind.Biometric(modalities),
+ requireConfirmation = requireConfirmation,
+ )
+ }
+
+ override fun useCredentialsForAuthentication(
+ promptInfo: PromptInfo,
+ @Utils.CredentialType kind: Int,
+ userId: Int,
+ challenge: Long,
+ ) {
+ promptRepository.setPrompt(
+ promptInfo = promptInfo,
+ userId = userId,
+ gatekeeperChallenge = challenge,
+ kind = kind.asBiometricPromptCredential(),
+ )
+ }
+
+ override fun resetPrompt() {
+ promptRepository.unsetPrompt()
+ }
+}
+
+// TODO(b/251476085): remove along with Utils.CredentialType
+/** Convert a [Utils.CredentialType] to the corresponding [PromptKind]. */
+private fun @receiver:Utils.CredentialType Int.asBiometricPromptCredential(): PromptKind =
+ when (this) {
+ Utils.CREDENTIAL_PIN -> PromptKind.Pin
+ Utils.CREDENTIAL_PASSWORD -> PromptKind.Password
+ Utils.CREDENTIAL_PATTERN -> PromptKind.Pattern
+ else -> PromptKind.Biometric()
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModalities.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModalities.kt
new file mode 100644
index 0000000..274f58a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModalities.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.systemui.biometrics.domain.model
+
+import android.hardware.biometrics.SensorProperties
+import android.hardware.face.FaceSensorPropertiesInternal
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+
+/** The available modalities for an operation. */
+data class BiometricModalities(
+ val fingerprintProperties: FingerprintSensorPropertiesInternal? = null,
+ val faceProperties: FaceSensorPropertiesInternal? = null,
+) {
+ /** If there are no available modalities. */
+ val isEmpty: Boolean
+ get() = !hasFingerprint && !hasFace
+
+ /** If fingerprint authentication is available (and [fingerprintProperties] is non-null). */
+ val hasFingerprint: Boolean
+ get() = fingerprintProperties != null
+
+ /** If fingerprint authentication is available (and [faceProperties] is non-null). */
+ val hasFace: Boolean
+ get() = faceProperties != null
+
+ /** If only face authentication is enabled. */
+ val hasFaceOnly: Boolean
+ get() = hasFace && !hasFingerprint
+
+ /** If only fingerprint authentication is enabled. */
+ val hasFingerprintOnly: Boolean
+ get() = hasFingerprint && !hasFace
+
+ /** If face & fingerprint authentication is enabled (coex). */
+ val hasFaceAndFingerprint: Boolean
+ get() = hasFingerprint && hasFace
+
+ /** If [hasFace] and it is configured as a STRONG class 3 biometric. */
+ val isFaceStrong: Boolean
+ get() = faceProperties?.sensorStrength == SensorProperties.STRENGTH_STRONG
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModality.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModality.kt
new file mode 100644
index 0000000..3197c09
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModality.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.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.systemui.biometrics.domain.model
+
+import android.hardware.biometrics.BiometricAuthenticator
+
+/** Shadows [BiometricAuthenticator.Modality] for Kotlin use within SysUI. */
+enum class BiometricModality {
+ None,
+ Fingerprint,
+ Face,
+}
+
+/** Convert a framework [BiometricAuthenticator.Modality] to a SysUI [BiometricModality]. */
+@BiometricAuthenticator.Modality
+fun Int.asBiometricModality(): BiometricModality =
+ when (this) {
+ BiometricAuthenticator.TYPE_FINGERPRINT -> BiometricModality.Fingerprint
+ BiometricAuthenticator.TYPE_FACE -> BiometricModality.Face
+ else -> BiometricModality.None
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
index 5ee0381..75de47d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
@@ -21,6 +21,7 @@
info: PromptInfo,
userInfo: BiometricUserInfo,
operationInfo: BiometricOperationInfo,
+ val modalities: BiometricModalities,
) :
BiometricPromptRequest(
title = info.title?.toString() ?: "",
@@ -28,7 +29,9 @@
description = info.description?.toString() ?: "",
userInfo = userInfo,
operationInfo = operationInfo
- )
+ ) {
+ val negativeButtonText: String = info.negativeButtonText?.toString() ?: ""
+ }
/** Prompt using a credential (pin, pattern, password). */
sealed class Credential(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/PromptKind.kt
similarity index 64%
rename from packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt
rename to packages/SystemUI/src/com/android/systemui/biometrics/shared/model/PromptKind.kt
index e82646f..416fc64 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/PromptKind.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -14,15 +14,19 @@
* limitations under the License.
*/
-package com.android.systemui.biometrics.data.model
+package com.android.systemui.biometrics.shared.model
import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.domain.model.BiometricModalities
// TODO(b/251476085): this should eventually replace Utils.CredentialType
/** Credential options for biometric prompt. Shadows [Utils.CredentialType]. */
-enum class PromptKind {
- ANY_BIOMETRIC,
- PIN,
- PATTERN,
- PASSWORD,
+sealed interface PromptKind {
+ data class Biometric(
+ val activeModalities: BiometricModalities = BiometricModalities(),
+ ) : PromptKind
+
+ object Pin : PromptKind
+ object Pattern : PromptKind
+ object Password : PromptKind
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
new file mode 100644
index 0000000..3753d10
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
@@ -0,0 +1,170 @@
+/*
+ * 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.systemui.biometrics.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+import com.android.systemui.biometrics.AuthBiometricFingerprintIconController;
+import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.biometrics.AuthDialog;
+import com.android.systemui.biometrics.UdfpsDialogMeasureAdapter;
+
+import kotlin.Pair;
+
+/**
+ * Contains the Biometric views (title, subtitle, icon, buttons, etc.).
+ *
+ * TODO(b/251476085): get the udfps junk out of here, at a minimum. Likely can be replaced with a
+ * normal LinearLayout.
+ */
+public class BiometricPromptLayout extends LinearLayout {
+
+ private static final String TAG = "BiometricPromptLayout";
+
+ @Nullable
+ private AuthController.ScaleFactorProvider mScaleFactorProvider;
+ @Nullable
+ private UdfpsDialogMeasureAdapter mUdfpsAdapter;
+
+ private final boolean mUseCustomBpSize;
+ private final int mCustomBpWidth;
+ private final int mCustomBpHeight;
+
+ public BiometricPromptLayout(Context context) {
+ this(context, null);
+ }
+
+ public BiometricPromptLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mUseCustomBpSize = getResources().getBoolean(R.bool.use_custom_bp_size);
+ mCustomBpWidth = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_width);
+ mCustomBpHeight = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_height);
+ }
+
+ @Deprecated
+ public void setUdfpsAdapter(@NonNull UdfpsDialogMeasureAdapter adapter,
+ @NonNull AuthController.ScaleFactorProvider scaleProvider) {
+ mUdfpsAdapter = adapter;
+ mScaleFactorProvider = scaleProvider != null ? scaleProvider : () -> 1.0f;
+ }
+
+ @Deprecated
+ public boolean isUdfps() {
+ return mUdfpsAdapter != null;
+ }
+
+ @Deprecated
+ public void updateFingerprintAffordanceSize(
+ @NonNull AuthBiometricFingerprintIconController iconController) {
+ if (mUdfpsAdapter != null) {
+ final int sensorDiameter = mUdfpsAdapter.getSensorDiameter(
+ mScaleFactorProvider.provide());
+ iconController.setIconLayoutParamSize(new Pair(sensorDiameter, sensorDiameter));
+ }
+ }
+
+ @NonNull
+ private AuthDialog.LayoutParams onMeasureInternal(int width, int height) {
+ int totalHeight = 0;
+ final int numChildren = getChildCount();
+ for (int i = 0; i < numChildren; i++) {
+ final View child = getChildAt(i);
+
+ if (child.getId() == R.id.space_above_icon
+ || child.getId() == R.id.space_below_icon
+ || child.getId() == R.id.button_bar) {
+ child.measure(
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(child.getLayoutParams().height,
+ MeasureSpec.EXACTLY));
+ } else if (child.getId() == R.id.biometric_icon_frame) {
+ final View iconView = findViewById(R.id.biometric_icon);
+ child.measure(
+ MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().width,
+ MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().height,
+ MeasureSpec.EXACTLY));
+ } else if (child.getId() == R.id.biometric_icon) {
+ child.measure(
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
+ } else {
+ child.measure(
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
+ }
+
+ if (child.getVisibility() != View.GONE) {
+ totalHeight += child.getMeasuredHeight();
+ }
+ }
+
+ final AuthDialog.LayoutParams params = new AuthDialog.LayoutParams(width, totalHeight);
+ if (mUdfpsAdapter != null) {
+ return mUdfpsAdapter.onMeasureInternal(width, height, params,
+ (mScaleFactorProvider != null) ? mScaleFactorProvider.provide() : 1.0f);
+ } else {
+ return params;
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (mUseCustomBpSize) {
+ width = mCustomBpWidth;
+ height = mCustomBpHeight;
+ } else {
+ width = Math.min(width, height);
+ }
+
+ final AuthDialog.LayoutParams params = onMeasureInternal(width, height);
+ setMeasuredDimension(params.mMediumWidth, params.mMediumHeight);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (mUdfpsAdapter != null) {
+ // Move the UDFPS icon and indicator text if necessary. This probably only needs to
+ // happen for devices where the UDFPS sensor is too low.
+ // TODO(b/201510778): Update this logic to support cases where the sensor or text
+ // overlap the button bar area.
+ final float bottomSpacerHeight = mUdfpsAdapter.getBottomSpacerHeight();
+ Log.w(TAG, "bottomSpacerHeight: " + bottomSpacerHeight);
+ if (bottomSpacerHeight < 0) {
+ final FrameLayout iconFrame = findViewById(R.id.biometric_icon_frame);
+ iconFrame.setTranslationY(-bottomSpacerHeight);
+ final TextView indicator = findViewById(R.id.indicator);
+ indicator.setTranslationY(-bottomSpacerHeight);
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
new file mode 100644
index 0000000..8486c3f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -0,0 +1,622 @@
+/*
+ * 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.systemui.biometrics.ui.binder
+
+import android.animation.Animator
+import android.content.Context
+import android.hardware.biometrics.BiometricAuthenticator
+import android.hardware.biometrics.BiometricConstants
+import android.hardware.biometrics.BiometricPrompt
+import android.hardware.face.FaceManager
+import android.os.Bundle
+import android.text.method.ScrollingMovementMethod
+import android.util.Log
+import android.view.View
+import android.view.accessibility.AccessibilityManager
+import android.widget.Button
+import android.widget.TextView
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.airbnb.lottie.LottieAnimationView
+import com.android.systemui.R
+import com.android.systemui.biometrics.AuthBiometricFaceIconController
+import com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceIconController
+import com.android.systemui.biometrics.AuthBiometricFingerprintIconController
+import com.android.systemui.biometrics.AuthBiometricView
+import com.android.systemui.biometrics.AuthBiometricView.Callback
+import com.android.systemui.biometrics.AuthBiometricViewAdapter
+import com.android.systemui.biometrics.AuthIconController
+import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.domain.model.BiometricModalities
+import com.android.systemui.biometrics.domain.model.BiometricModality
+import com.android.systemui.biometrics.domain.model.asBiometricModality
+import com.android.systemui.biometrics.shared.model.PromptKind
+import com.android.systemui.biometrics.ui.BiometricPromptLayout
+import com.android.systemui.biometrics.ui.viewmodel.FingerprintStartMode
+import com.android.systemui.biometrics.ui.viewmodel.PromptMessage
+import com.android.systemui.biometrics.ui.viewmodel.PromptSize
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+private const val TAG = "BiometricViewBinder"
+
+/** Top-most view binder for BiometricPrompt views. */
+object BiometricViewBinder {
+
+ /** Binds a [BiometricPromptLayout] to a [PromptViewModel]. */
+ @JvmStatic
+ fun bind(
+ view: BiometricPromptLayout,
+ viewModel: PromptViewModel,
+ panelViewController: AuthPanelController,
+ jankListener: BiometricJankListener,
+ backgroundView: View,
+ legacyCallback: Callback,
+ applicationScope: CoroutineScope,
+ ): AuthBiometricViewAdapter {
+ val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!!
+ fun notifyAccessibilityChanged() {
+ Utils.notifyAccessibilityContentChanged(accessibilityManager, view)
+ }
+
+ val textColorError =
+ view.resources.getColor(R.color.biometric_dialog_error, view.context.theme)
+ val textColorHint =
+ view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme)
+
+ val titleView = view.findViewById<TextView>(R.id.title)
+ val subtitleView = view.findViewById<TextView>(R.id.subtitle)
+ val descriptionView = view.findViewById<TextView>(R.id.description)
+
+ // set selected for marquee
+ titleView.isSelected = true
+ subtitleView.isSelected = true
+ descriptionView.movementMethod = ScrollingMovementMethod()
+
+ val iconViewOverlay = view.findViewById<LottieAnimationView>(R.id.biometric_icon_overlay)
+ val iconView = view.findViewById<LottieAnimationView>(R.id.biometric_icon)
+ val indicatorMessageView = view.findViewById<TextView>(R.id.indicator)
+
+ // Negative-side (left) buttons
+ val negativeButton = view.findViewById<Button>(R.id.button_negative)
+ val cancelButton = view.findViewById<Button>(R.id.button_cancel)
+ val credentialFallbackButton = view.findViewById<Button>(R.id.button_use_credential)
+
+ // Positive-side (right) buttons
+ val confirmationButton = view.findViewById<Button>(R.id.button_confirm)
+ val retryButton = view.findViewById<Button>(R.id.button_try_again)
+
+ // TODO(b/251476085): temporary workaround for the unsafe callbacks & legacy controllers
+ val adapter =
+ Spaghetti(
+ view = view,
+ viewModel = viewModel,
+ applicationContext = view.context.applicationContext,
+ applicationScope = applicationScope,
+ )
+
+ // bind to prompt
+ var boundSize = false
+ view.repeatWhenAttached {
+ // these do not change and need to be set before any size transitions
+ val modalities = viewModel.modalities.first()
+ titleView.text = viewModel.title.first()
+ descriptionView.text = viewModel.description.first()
+ subtitleView.text = viewModel.subtitle.first()
+
+ // set button listeners
+ negativeButton.setOnClickListener {
+ legacyCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE)
+ }
+ cancelButton.setOnClickListener {
+ legacyCallback.onAction(Callback.ACTION_USER_CANCELED)
+ }
+ credentialFallbackButton.setOnClickListener {
+ viewModel.onSwitchToCredential()
+ legacyCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL)
+ }
+ confirmationButton.setOnClickListener { viewModel.confirmAuthenticated() }
+ retryButton.setOnClickListener {
+ viewModel.showAuthenticating(isRetry = true)
+ legacyCallback.onAction(Callback.ACTION_BUTTON_TRY_AGAIN)
+ }
+
+ // TODO(b/251476085): migrate legacy icon controllers and remove
+ var legacyState: Int = viewModel.legacyState.value
+ val iconController =
+ modalities.asIconController(
+ view.context,
+ iconView,
+ iconViewOverlay,
+ )
+ adapter.attach(this, iconController, modalities, legacyCallback)
+ if (iconController is AuthBiometricFingerprintIconController) {
+ view.updateFingerprintAffordanceSize(iconController)
+ }
+ if (iconController is HackyCoexIconController) {
+ iconController.faceMode = !viewModel.isConfirmationRequested.first()
+ }
+
+ // the icon controller must be created before this happens for the legacy
+ // sizing code in BiometricPromptLayout to work correctly. Simplify this
+ // when those are also migrated. (otherwise the icon size may not be set to
+ // a pixel value before the view is measured and WRAP_CONTENT will be incorrectly
+ // used as part of the measure spec)
+ if (!boundSize) {
+ boundSize = true
+ BiometricViewSizeBinder.bind(
+ view = view,
+ viewModel = viewModel,
+ viewsToHideWhenSmall =
+ listOf(
+ titleView,
+ subtitleView,
+ descriptionView,
+ ),
+ viewsToFadeInOnSizeChange =
+ listOf(
+ titleView,
+ subtitleView,
+ descriptionView,
+ indicatorMessageView,
+ negativeButton,
+ cancelButton,
+ retryButton,
+ confirmationButton,
+ credentialFallbackButton,
+ ),
+ panelViewController = panelViewController,
+ jankListener = jankListener,
+ )
+ }
+
+ // TODO(b/251476085): migrate legacy icon controllers and remove
+ // The fingerprint sensor is started by the legacy
+ // AuthContainerView#onDialogAnimatedIn in all cases but the implicit coex flow
+ // (delayed mode). In that case, start it on the first transition to delayed
+ // which will be triggered by any auth failure.
+ lifecycleScope.launch {
+ val oldMode = viewModel.fingerprintStartMode.first()
+ viewModel.fingerprintStartMode.collect { newMode ->
+ // trigger sensor to start
+ if (
+ oldMode == FingerprintStartMode.Pending &&
+ newMode == FingerprintStartMode.Delayed
+ ) {
+ legacyCallback.onAction(Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR)
+ }
+
+ if (newMode.isStarted) {
+ // do wonky switch from implicit to explicit flow
+ (iconController as? HackyCoexIconController)?.faceMode = false
+ viewModel.showAuthenticating(
+ modalities.asDefaultHelpMessage(view.context),
+ )
+ }
+ }
+ }
+
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ // handle background clicks
+ launch {
+ combine(viewModel.isAuthenticated, viewModel.size) { (authenticated, _), size ->
+ when {
+ authenticated -> false
+ size == PromptSize.SMALL -> false
+ size == PromptSize.LARGE -> false
+ else -> true
+ }
+ }
+ .collect { dismissOnClick ->
+ backgroundView.setOnClickListener {
+ if (dismissOnClick) {
+ legacyCallback.onAction(Callback.ACTION_USER_CANCELED)
+ } else {
+ Log.w(TAG, "Ignoring background click")
+ }
+ }
+ }
+ }
+
+ // set messages
+ launch {
+ viewModel.isIndicatorMessageVisible.collect { show ->
+ indicatorMessageView.visibility = show.asVisibleOrHidden()
+ }
+ }
+
+ // configure & hide/disable buttons
+ launch {
+ viewModel.credentialKind
+ .map { kind ->
+ when (kind) {
+ PromptKind.Pin ->
+ view.resources.getString(R.string.biometric_dialog_use_pin)
+ PromptKind.Password ->
+ view.resources.getString(R.string.biometric_dialog_use_password)
+ PromptKind.Pattern ->
+ view.resources.getString(R.string.biometric_dialog_use_pattern)
+ else -> ""
+ }
+ }
+ .collect { credentialFallbackButton.text = it }
+ }
+ launch { viewModel.negativeButtonText.collect { negativeButton.text = it } }
+ launch {
+ viewModel.isConfirmButtonVisible.collect { show ->
+ confirmationButton.visibility = show.asVisibleOrGone()
+ }
+ }
+ launch {
+ viewModel.isCancelButtonVisible.collect { show ->
+ cancelButton.visibility = show.asVisibleOrGone()
+ }
+ }
+ launch {
+ viewModel.isNegativeButtonVisible.collect { show ->
+ negativeButton.visibility = show.asVisibleOrGone()
+ }
+ }
+ launch {
+ viewModel.isTryAgainButtonVisible.collect { show ->
+ retryButton.visibility = show.asVisibleOrGone()
+ }
+ }
+ launch {
+ viewModel.isCredentialButtonVisible.collect { show ->
+ credentialFallbackButton.visibility = show.asVisibleOrGone()
+ }
+ }
+
+ // reuse the icon as a confirm button
+ launch {
+ viewModel.isConfirmButtonVisible
+ .map { isPending ->
+ when {
+ isPending && iconController.actsAsConfirmButton ->
+ View.OnClickListener { viewModel.confirmAuthenticated() }
+ else -> null
+ }
+ }
+ .collect { onClick ->
+ iconViewOverlay.setOnClickListener(onClick)
+ iconView.setOnClickListener(onClick)
+ }
+ }
+
+ // TODO(b/251476085): remove w/ legacy icon controllers
+ // set icon affordance using legacy states
+ // like the old code, this causes animations to repeat on config changes :(
+ // but keep behavior for now as no one has complained...
+ launch {
+ viewModel.legacyState.collect { newState ->
+ iconController.updateState(legacyState, newState)
+ legacyState = newState
+ }
+ }
+
+ // not sure why this is here, but the legacy code did it probably needed?
+ launch {
+ viewModel.isAuthenticating.collect { isAuthenticating ->
+ if (isAuthenticating) {
+ notifyAccessibilityChanged()
+ }
+ }
+ }
+
+ // dismiss prompt when authenticated and confirmed
+ launch {
+ viewModel.isAuthenticated.collect { authState ->
+ if (authState.isAuthenticatedAndConfirmed) {
+ view.announceForAccessibility(
+ view.resources.getString(R.string.biometric_dialog_authenticated)
+ )
+ notifyAccessibilityChanged()
+
+ launch {
+ delay(authState.delay)
+ legacyCallback.onAction(Callback.ACTION_AUTHENTICATED)
+ }
+ }
+ }
+ }
+
+ // show error & help messages
+ launch {
+ viewModel.message.collect { promptMessage ->
+ val isError = promptMessage is PromptMessage.Error
+
+ indicatorMessageView.text = promptMessage.message
+ indicatorMessageView.setTextColor(
+ if (isError) textColorError else textColorHint
+ )
+
+ // select to enable marquee unless a screen reader is enabled
+ // TODO(wenhuiy): this may have recently changed per UX - verify and remove
+ indicatorMessageView.isSelected =
+ !accessibilityManager.isEnabled ||
+ !accessibilityManager.isTouchExplorationEnabled
+
+ notifyAccessibilityChanged()
+ }
+ }
+ }
+ }
+
+ return adapter
+ }
+}
+
+/**
+ * Adapter for legacy events. Remove once legacy controller can be replaced by flagged code.
+ *
+ * These events can be dispatched when the view is being recreated so they need to be delivered to
+ * the view model (which will be retained) via the application scope.
+ *
+ * Do not reference the [view] for anything other than [asView].
+ *
+ * TODO(b/251476085): remove after replacing AuthContainerView
+ */
+private class Spaghetti(
+ private val view: View,
+ private val viewModel: PromptViewModel,
+ private val applicationContext: Context,
+ private val applicationScope: CoroutineScope,
+) : AuthBiometricViewAdapter {
+
+ private var lifecycleScope: CoroutineScope? = null
+ private var modalities: BiometricModalities = BiometricModalities()
+ private var faceFailedAtLeastOnce = false
+ private var legacyCallback: Callback? = null
+
+ override var legacyIconController: AuthIconController? = null
+ private set
+
+ // hacky way to suppress lockout errors
+ private val lockoutErrorStrings =
+ listOf(
+ BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
+ BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT,
+ )
+ .map { FaceManager.getErrorString(applicationContext, it, 0 /* vendorCode */) }
+
+ fun attach(
+ lifecycleOwner: LifecycleOwner,
+ iconController: AuthIconController,
+ activeModalities: BiometricModalities,
+ callback: Callback,
+ ) {
+ modalities = activeModalities
+ legacyIconController = iconController
+ legacyCallback = callback
+
+ lifecycleOwner.lifecycle.addObserver(
+ object : DefaultLifecycleObserver {
+ override fun onCreate(owner: LifecycleOwner) {
+ lifecycleScope = owner.lifecycleScope
+ iconController.deactivated = false
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ lifecycleScope = null
+ iconController.deactivated = true
+ }
+ }
+ )
+ }
+
+ override fun onDialogAnimatedIn(fingerprintWasStarted: Boolean) {
+ if (fingerprintWasStarted) {
+ viewModel.ensureFingerprintHasStarted(isDelayed = false)
+ viewModel.showAuthenticating(modalities.asDefaultHelpMessage(applicationContext))
+ } else {
+ viewModel.showAuthenticating()
+ }
+ }
+
+ override fun onAuthenticationSucceeded(@BiometricAuthenticator.Modality modality: Int) {
+ applicationScope.launch {
+ val authenticatedModality = modality.asBiometricModality()
+ val msgId = getHelpForSuccessfulAuthentication(authenticatedModality)
+ viewModel.showAuthenticated(
+ modality = authenticatedModality,
+ dismissAfterDelay = 500,
+ helpMessage = if (msgId != null) applicationContext.getString(msgId) else ""
+ )
+ }
+ }
+
+ private suspend fun getHelpForSuccessfulAuthentication(
+ authenticatedModality: BiometricModality,
+ ): Int? =
+ when {
+ // for coex, show a message when face succeeds after fingerprint has also started
+ modalities.hasFaceAndFingerprint &&
+ (viewModel.fingerprintStartMode.first() != FingerprintStartMode.Pending) &&
+ (authenticatedModality == BiometricModality.Face) ->
+ R.string.biometric_dialog_tap_confirm_with_face
+ else -> null
+ }
+
+ override fun onAuthenticationFailed(
+ @BiometricAuthenticator.Modality modality: Int,
+ failureReason: String,
+ ) {
+ val failedModality = modality.asBiometricModality()
+ viewModel.ensureFingerprintHasStarted(isDelayed = true)
+
+ applicationScope.launch {
+ val suppress =
+ modalities.hasFaceAndFingerprint &&
+ (failedModality == BiometricModality.Face) &&
+ faceFailedAtLeastOnce
+ if (failedModality == BiometricModality.Face) {
+ faceFailedAtLeastOnce = true
+ }
+
+ viewModel.showTemporaryError(
+ failureReason,
+ messageAfterError = modalities.asDefaultHelpMessage(applicationContext),
+ authenticateAfterError = modalities.hasFingerprint,
+ suppressIfErrorShowing = suppress,
+ failedModality = failedModality,
+ )
+ }
+ }
+
+ override fun onError(modality: Int, error: String) {
+ val errorModality = modality.asBiometricModality()
+ if (ignoreUnsuccessfulEventsFrom(errorModality, error)) {
+ return
+ }
+
+ applicationScope.launch {
+ val suppress =
+ modalities.hasFaceAndFingerprint && (errorModality == BiometricModality.Face)
+ viewModel.showTemporaryError(
+ error,
+ suppressIfErrorShowing = suppress,
+ )
+ delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong())
+ legacyCallback?.onAction(Callback.ACTION_ERROR)
+ }
+ }
+
+ override fun onHelp(modality: Int, help: String) {
+ if (ignoreUnsuccessfulEventsFrom(modality.asBiometricModality(), "")) {
+ return
+ }
+
+ applicationScope.launch {
+ viewModel.showTemporaryHelp(
+ help,
+ messageAfterHelp = modalities.asDefaultHelpMessage(applicationContext),
+ )
+ }
+ }
+
+ private fun ignoreUnsuccessfulEventsFrom(modality: BiometricModality, message: String) =
+ when {
+ modalities.hasFaceAndFingerprint ->
+ (modality == BiometricModality.Face) &&
+ !(modalities.isFaceStrong && lockoutErrorStrings.contains(message))
+ else -> false
+ }
+
+ override fun startTransitionToCredentialUI() {
+ applicationScope.launch {
+ viewModel.onSwitchToCredential()
+ legacyCallback?.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL)
+ }
+ }
+
+ override fun requestLayout() {
+ // nothing, for legacy view...
+ }
+
+ override fun restoreState(bundle: Bundle?) {
+ // nothing, for legacy view...
+ }
+
+ override fun onSaveState(bundle: Bundle?) {
+ // nothing, for legacy view...
+ }
+
+ override fun onOrientationChanged() {
+ // nothing, for legacy view...
+ }
+
+ override fun cancelAnimation() {
+ view.animate()?.cancel()
+ }
+
+ override fun isCoex() = modalities.hasFaceAndFingerprint
+
+ override fun asView() = view
+}
+
+private fun BiometricModalities.asDefaultHelpMessage(context: Context): String =
+ when {
+ hasFingerprint -> context.getString(R.string.fingerprint_dialog_touch_sensor)
+ else -> ""
+ }
+
+private fun BiometricModalities.asIconController(
+ context: Context,
+ iconView: LottieAnimationView,
+ iconViewOverlay: LottieAnimationView,
+): AuthIconController =
+ when {
+ hasFaceAndFingerprint -> HackyCoexIconController(context, iconView, iconViewOverlay)
+ hasFingerprint -> AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay)
+ hasFace -> AuthBiometricFaceIconController(context, iconView)
+ else -> throw IllegalStateException("unexpected view type :$this")
+ }
+
+private fun Boolean.asVisibleOrGone(): Int = if (this) View.VISIBLE else View.GONE
+
+private fun Boolean.asVisibleOrHidden(): Int = if (this) View.VISIBLE else View.INVISIBLE
+
+// TODO(b/251476085): proper type?
+typealias BiometricJankListener = Animator.AnimatorListener
+
+// TODO(b/251476085): delete - temporary until the legacy icon controllers are replaced
+private class HackyCoexIconController(
+ context: Context,
+ iconView: LottieAnimationView,
+ iconViewOverlay: LottieAnimationView,
+) : AuthBiometricFingerprintAndFaceIconController(context, iconView, iconViewOverlay) {
+
+ private var state: Int? = null
+ private val faceController = AuthBiometricFaceIconController(context, iconView)
+
+ var faceMode: Boolean = true
+ set(value) {
+ if (field != value) {
+ field = value
+
+ faceController.deactivated = !value
+ iconView.setImageIcon(null)
+ iconViewOverlay.setImageIcon(null)
+ state?.let { updateIcon(AuthBiometricView.STATE_IDLE, it) }
+ }
+ }
+
+ override fun updateIcon(lastState: Int, newState: Int) {
+ if (deactivated) {
+ return
+ }
+
+ if (faceMode) {
+ faceController.updateIcon(lastState, newState)
+ } else {
+ super.updateIcon(lastState, newState)
+ }
+
+ state = newState
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
new file mode 100644
index 0000000..e4c4e9a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
@@ -0,0 +1,209 @@
+/*
+ * 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.systemui.biometrics.ui.binder
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.view.View
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityManager
+import android.widget.TextView
+import androidx.core.animation.addListener
+import androidx.core.view.doOnLayout
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.R
+import com.android.systemui.biometrics.AuthDialog
+import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.ui.BiometricPromptLayout
+import com.android.systemui.biometrics.ui.viewmodel.PromptSize
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
+import com.android.systemui.biometrics.ui.viewmodel.isLarge
+import com.android.systemui.biometrics.ui.viewmodel.isMedium
+import com.android.systemui.biometrics.ui.viewmodel.isNullOrNotSmall
+import com.android.systemui.biometrics.ui.viewmodel.isSmall
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.launch
+
+/** Helper for [BiometricViewBinder] to handle resize transitions. */
+object BiometricViewSizeBinder {
+
+ /** Resizes [BiometricPromptLayout] and the [panelViewController] via the [PromptViewModel]. */
+ fun bind(
+ view: BiometricPromptLayout,
+ viewModel: PromptViewModel,
+ viewsToHideWhenSmall: List<TextView>,
+ viewsToFadeInOnSizeChange: List<View>,
+ panelViewController: AuthPanelController,
+ jankListener: BiometricJankListener,
+ ) {
+ val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!!
+ fun notifyAccessibilityChanged() {
+ Utils.notifyAccessibilityContentChanged(accessibilityManager, view)
+ }
+
+ fun startMonitoredAnimation(animators: List<Animator>) {
+ with(AnimatorSet()) {
+ addListener(jankListener)
+ addListener(onEnd = { notifyAccessibilityChanged() })
+ play(animators.first()).apply { animators.drop(1).forEach { next -> with(next) } }
+ start()
+ }
+ }
+
+ val iconHolderView = view.findViewById<View>(R.id.biometric_icon_frame)
+ val iconPadding = view.resources.getDimension(R.dimen.biometric_dialog_icon_padding)
+ val fullSizeYOffset =
+ view.resources.getDimension(R.dimen.biometric_dialog_medium_to_large_translation_offset)
+
+ // cache the original position of the icon view (as done in legacy view)
+ // this must happen before any size changes can be made
+ var iconHolderOriginalY = 0f
+ view.doOnLayout {
+ iconHolderOriginalY = iconHolderView.y
+
+ // bind to prompt
+ // TODO(b/251476085): migrate the legacy panel controller and simplify this
+ view.repeatWhenAttached {
+ var currentSize: PromptSize? = null
+ lifecycleScope.launch {
+ viewModel.size.collect { size ->
+ // prepare for animated size transitions
+ for (v in viewsToHideWhenSmall) {
+ v.showTextOrHide(forceHide = size.isSmall)
+ }
+ if (currentSize == null && size.isSmall) {
+ iconHolderView.alpha = 0f
+ }
+ if ((currentSize.isSmall && size.isMedium) || size.isSmall) {
+ viewsToFadeInOnSizeChange.forEach { it.alpha = 0f }
+ }
+
+ // propagate size changes to legacy panel controller and animate transitions
+ view.doOnLayout {
+ val width = view.measuredWidth
+ val height = view.measuredHeight
+
+ when {
+ size.isSmall -> {
+ iconHolderView.alpha = 1f
+ iconHolderView.y =
+ view.height - iconHolderView.height - iconPadding
+ val newHeight =
+ iconHolderView.height + 2 * iconPadding.toInt() -
+ iconHolderView.paddingTop -
+ iconHolderView.paddingBottom
+ panelViewController.updateForContentDimensions(
+ width,
+ newHeight,
+ 0, /* animateDurationMs */
+ )
+ }
+ size.isMedium && currentSize.isSmall -> {
+ val duration = AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS
+ panelViewController.updateForContentDimensions(
+ width,
+ height,
+ duration,
+ )
+ startMonitoredAnimation(
+ listOf(
+ iconHolderView.asVerticalAnimator(
+ duration = duration.toLong(),
+ toY = iconHolderOriginalY,
+ ),
+ viewsToFadeInOnSizeChange.asFadeInAnimator(
+ duration = duration.toLong(),
+ delay = duration.toLong(),
+ ),
+ )
+ )
+ }
+ size.isMedium && currentSize.isNullOrNotSmall -> {
+ panelViewController.updateForContentDimensions(
+ width,
+ height,
+ 0, /* animateDurationMs */
+ )
+ }
+ size.isLarge -> {
+ val duration = AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS
+ panelViewController.setUseFullScreen(true)
+ panelViewController.updateForContentDimensions(
+ panelViewController.containerWidth,
+ panelViewController.containerHeight,
+ duration,
+ )
+
+ startMonitoredAnimation(
+ listOf(
+ view.asVerticalAnimator(
+ duration.toLong() * 2 / 3,
+ toY = view.y - fullSizeYOffset
+ ),
+ listOf(view)
+ .asFadeInAnimator(
+ duration = duration.toLong() / 2,
+ delay = duration.toLong(),
+ ),
+ )
+ )
+ // TODO(b/251476085): clean up (copied from legacy)
+ if (view.isAttachedToWindow) {
+ val parent = view.parent as? ViewGroup
+ parent?.removeView(view)
+ }
+ }
+ }
+
+ currentSize = size
+ notifyAccessibilityChanged()
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun TextView.showTextOrHide(forceHide: Boolean = false) {
+ visibility = if (forceHide || text.isBlank()) View.GONE else View.VISIBLE
+}
+
+private fun View.asVerticalAnimator(
+ duration: Long,
+ toY: Float,
+ fromY: Float = this.y
+): ValueAnimator {
+ val animator = ValueAnimator.ofFloat(fromY, toY)
+ animator.duration = duration
+ animator.addUpdateListener { y = it.animatedValue as Float }
+ return animator
+}
+
+private fun List<View>.asFadeInAnimator(duration: Long, delay: Long): ValueAnimator {
+ forEach { it.alpha = 0f }
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ animator.duration = duration
+ animator.startDelay = delay
+ animator.addUpdateListener {
+ val alpha = it.animatedValue as Float
+ forEach { view -> view.alpha = alpha }
+ }
+ return animator
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt
similarity index 89%
rename from packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt
index ba23f1c..a64798c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt
@@ -4,7 +4,7 @@
import android.os.UserHandle
/** View model for the top-level header / info area of BiometricPrompt. */
-interface HeaderViewModel {
+interface CredentialHeaderViewModel {
val user: UserHandle
val title: String
val subtitle: String
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
index 84bbceb..9d7b940 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
@@ -7,8 +7,8 @@
import com.android.internal.widget.LockPatternView
import com.android.systemui.R
import com.android.systemui.biometrics.Utils
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor
import com.android.systemui.biometrics.domain.interactor.CredentialStatus
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor
import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
import com.android.systemui.dagger.qualifiers.Application
import javax.inject.Inject
@@ -27,11 +27,11 @@
@Inject
constructor(
@Application private val applicationContext: Context,
- private val credentialInteractor: BiometricPromptCredentialInteractor,
+ private val credentialInteractor: PromptCredentialInteractor,
) {
/** Top level information about the prompt. */
- val header: Flow<HeaderViewModel> =
+ val header: Flow<CredentialHeaderViewModel> =
credentialInteractor.prompt.filterIsInstance<BiometricPromptRequest.Credential>().map {
request ->
BiometricPromptHeaderViewModelImpl(
@@ -109,12 +109,14 @@
}
/** Check a PIN or password and update [validatedAttestation] or [remainingAttempts]. */
- suspend fun checkCredential(text: CharSequence, header: HeaderViewModel) =
+ suspend fun checkCredential(text: CharSequence, header: CredentialHeaderViewModel) =
checkCredential(credentialInteractor.checkCredential(header.asRequest(), text = text))
/** Check a pattern and update [validatedAttestation] or [remainingAttempts]. */
- suspend fun checkCredential(pattern: List<LockPatternView.Cell>, header: HeaderViewModel) =
- checkCredential(credentialInteractor.checkCredential(header.asRequest(), pattern = pattern))
+ suspend fun checkCredential(
+ pattern: List<LockPatternView.Cell>,
+ header: CredentialHeaderViewModel
+ ) = checkCredential(credentialInteractor.checkCredential(header.asRequest(), pattern = pattern))
private suspend fun checkCredential(result: CredentialStatus) {
when (result) {
@@ -172,7 +174,7 @@
override val subtitle: String,
override val description: String,
override val icon: Drawable,
-) : HeaderViewModel
+) : CredentialHeaderViewModel
-private fun HeaderViewModel.asRequest(): BiometricPromptRequest.Credential =
+private fun CredentialHeaderViewModel.asRequest(): BiometricPromptRequest.Credential =
(this as BiometricPromptHeaderViewModelImpl).request
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthState.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthState.kt
new file mode 100644
index 0000000..9cb91b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthState.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.systemui.biometrics.ui.viewmodel
+
+import com.android.systemui.biometrics.domain.model.BiometricModality
+
+/**
+ * The authenticated state with the [authenticatedModality] (when [isAuthenticated]) with an
+ * optional [delay] to keep the UI showing before dismissing when [needsUserConfirmation] is not
+ * required.
+ */
+data class PromptAuthState(
+ val isAuthenticated: Boolean,
+ val authenticatedModality: BiometricModality = BiometricModality.None,
+ val needsUserConfirmation: Boolean = false,
+ val delay: Long = 0,
+) {
+ /** If authentication was successful and the user has confirmed (or does not need to). */
+ val isAuthenticatedAndConfirmed: Boolean
+ get() = isAuthenticated && !needsUserConfirmation
+
+ /** If a successful authentication has not occurred. */
+ val isNotAuthenticated: Boolean
+ get() = !isAuthenticated
+
+ /** If a authentication has succeeded and it was done by face (may need confirmation). */
+ val isAuthenticatedByFace: Boolean
+ get() = isAuthenticated && authenticatedModality == BiometricModality.Face
+
+ /** If a authentication has succeeded and it was done by fingerprint (may need confirmation). */
+ val isAuthenticatedByFingerprint: Boolean
+ get() = isAuthenticated && authenticatedModality == BiometricModality.Fingerprint
+
+ /** Copies this state, but toggles [needsUserConfirmation] to false. */
+ fun asConfirmed(): PromptAuthState =
+ PromptAuthState(
+ isAuthenticated = isAuthenticated,
+ authenticatedModality = authenticatedModality,
+ needsUserConfirmation = false,
+ delay = delay,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptMessage.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptMessage.kt
new file mode 100644
index 0000000..219da71
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptMessage.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.systemui.biometrics.ui.viewmodel
+
+/**
+ * A help, hint, or error message to show.
+ *
+ * These typically correspond to the same category of help/error callbacks from the underlying HAL
+ * that runs the biometric operation, but may be customized by the framework.
+ */
+sealed interface PromptMessage {
+
+ /** The message to show the user or the empty string. */
+ val message: String
+ get() =
+ when (this) {
+ is Error -> errorMessage
+ is Help -> helpMessage
+ else -> ""
+ }
+
+ /** If this is an [Error] or [Help] message. */
+ val isErrorOrHelp: Boolean
+ get() = this is Error || this is Help
+
+ /** An error message. */
+ data class Error(val errorMessage: String) : PromptMessage
+
+ /** A help message. */
+ data class Help(val helpMessage: String) : PromptMessage
+
+ /** No message. */
+ object Empty : PromptMessage
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptSize.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptSize.kt
new file mode 100644
index 0000000..d779062
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptSize.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.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.systemui.biometrics.ui.viewmodel
+
+/** The size of a biometric prompt. */
+enum class PromptSize {
+ /** Minimal UI, showing only biometric icon. */
+ SMALL,
+ /** Normal-sized biometric UI, showing title, icon, buttons, etc. */
+ MEDIUM,
+ /** Full-screen credential UI. */
+ LARGE,
+}
+
+val PromptSize?.isSmall: Boolean
+ get() = this != null && this == PromptSize.SMALL
+
+val PromptSize?.isNotSmall: Boolean
+ get() = this != null && this != PromptSize.SMALL
+
+val PromptSize?.isNullOrNotSmall: Boolean
+ get() = this == null || this != PromptSize.SMALL
+
+val PromptSize?.isMedium: Boolean
+ get() = this != null && this == PromptSize.MEDIUM
+
+val PromptSize?.isLarge: Boolean
+ get() = this != null && this == PromptSize.LARGE
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
new file mode 100644
index 0000000..2f8ed09
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -0,0 +1,453 @@
+/*
+ * 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.systemui.biometrics.ui.viewmodel
+
+import android.hardware.biometrics.BiometricPrompt
+import android.util.Log
+import com.android.systemui.biometrics.AuthBiometricView
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
+import com.android.systemui.biometrics.domain.model.BiometricModalities
+import com.android.systemui.biometrics.domain.model.BiometricModality
+import com.android.systemui.biometrics.shared.model.PromptKind
+import javax.inject.Inject
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/** ViewModel for BiometricPrompt. */
+class PromptViewModel
+@Inject
+constructor(
+ private val interactor: PromptSelectorInteractor,
+) {
+ /** The set of modalities available for this prompt */
+ val modalities: Flow<BiometricModalities> =
+ interactor.prompt.map { it?.modalities ?: BiometricModalities() }.distinctUntilChanged()
+
+ // TODO(b/251476085): remove after icon controllers are migrated - do not keep this state
+ private var _legacyState = MutableStateFlow(AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN)
+ val legacyState: StateFlow<Int> = _legacyState.asStateFlow()
+
+ private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
+ /** If the user is currently authenticating (i.e. at least one biometric is scanning). */
+ val isAuthenticating: Flow<Boolean> = _isAuthenticating.asStateFlow()
+
+ private val _isAuthenticated: MutableStateFlow<PromptAuthState> =
+ MutableStateFlow(PromptAuthState(false))
+
+ /** If the user has successfully authenticated and confirmed (when explicitly required). */
+ val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow()
+
+ /** If the API caller requested explicit confirmation after successful authentication. */
+ val isConfirmationRequested: Flow<Boolean> = interactor.isConfirmationRequested
+
+ /** The kind of credential the user has. */
+ val credentialKind: Flow<PromptKind> = interactor.credentialKind
+
+ /** The label to use for the cancel button. */
+ val negativeButtonText: Flow<String> = interactor.prompt.map { it?.negativeButtonText ?: "" }
+
+ private val _message: MutableStateFlow<PromptMessage> = MutableStateFlow(PromptMessage.Empty)
+
+ /** A message to show the user, if there is an error, hint, or help to show. */
+ val message: Flow<PromptMessage> = _message.asStateFlow()
+
+ private val isRetrySupported: Flow<Boolean> = modalities.map { it.hasFace }
+
+ private val _fingerprintStartMode = MutableStateFlow(FingerprintStartMode.Pending)
+
+ /** Fingerprint sensor state. */
+ val fingerprintStartMode: Flow<FingerprintStartMode> = _fingerprintStartMode.asStateFlow()
+
+ private val _forceLargeSize = MutableStateFlow(false)
+ private val _forceMediumSize = MutableStateFlow(false)
+
+ /** The size of the prompt. */
+ val size: Flow<PromptSize> =
+ combine(
+ _forceLargeSize,
+ _forceMediumSize,
+ modalities,
+ interactor.isConfirmationRequested,
+ fingerprintStartMode,
+ ) { forceLarge, forceMedium, modalities, confirmationRequired, fpStartMode ->
+ when {
+ forceLarge -> PromptSize.LARGE
+ forceMedium -> PromptSize.MEDIUM
+ modalities.hasFaceOnly && !confirmationRequired -> PromptSize.SMALL
+ modalities.hasFaceAndFingerprint &&
+ !confirmationRequired &&
+ fpStartMode == FingerprintStartMode.Pending -> PromptSize.SMALL
+ else -> PromptSize.MEDIUM
+ }
+ }
+ .distinctUntilChanged()
+
+ /** Title for the prompt. */
+ val title: Flow<String> = interactor.prompt.map { it?.title ?: "" }.distinctUntilChanged()
+
+ /** Subtitle for the prompt. */
+ val subtitle: Flow<String> = interactor.prompt.map { it?.subtitle ?: "" }.distinctUntilChanged()
+
+ /** Description for the prompt. */
+ val description: Flow<String> =
+ interactor.prompt.map { it?.description ?: "" }.distinctUntilChanged()
+
+ /** If the indicator (help, error) message should be shown. */
+ val isIndicatorMessageVisible: Flow<Boolean> =
+ combine(
+ size,
+ message,
+ ) { size, message ->
+ size.isNotSmall && message.message.isNotBlank()
+ }
+ .distinctUntilChanged()
+
+ /** If the auth is pending confirmation and the confirm button should be shown. */
+ val isConfirmButtonVisible: Flow<Boolean> =
+ combine(
+ size,
+ isAuthenticated,
+ ) { size, authState ->
+ size.isNotSmall && authState.isAuthenticated && authState.needsUserConfirmation
+ }
+ .distinctUntilChanged()
+
+ /** If the negative button should be shown. */
+ val isNegativeButtonVisible: Flow<Boolean> =
+ combine(
+ size,
+ isAuthenticated,
+ interactor.isCredentialAllowed,
+ ) { size, authState, credentialAllowed ->
+ size.isNotSmall && authState.isNotAuthenticated && !credentialAllowed
+ }
+ .distinctUntilChanged()
+
+ /** If the cancel button should be shown (. */
+ val isCancelButtonVisible: Flow<Boolean> =
+ combine(
+ size,
+ isAuthenticated,
+ isNegativeButtonVisible,
+ isConfirmButtonVisible,
+ ) { size, authState, showNegativeButton, showConfirmButton ->
+ size.isNotSmall &&
+ authState.isAuthenticated &&
+ !showNegativeButton &&
+ showConfirmButton
+ }
+ .distinctUntilChanged()
+
+ private val _canTryAgainNow = MutableStateFlow(false)
+ /**
+ * If authentication can be manually restarted via the try again button or touching a
+ * fingerprint sensor.
+ */
+ val canTryAgainNow: Flow<Boolean> =
+ combine(
+ _canTryAgainNow,
+ size,
+ isAuthenticated,
+ isRetrySupported,
+ ) { readyToTryAgain, size, authState, supportsRetry ->
+ readyToTryAgain && size.isNotSmall && supportsRetry && authState.isNotAuthenticated
+ }
+ .distinctUntilChanged()
+
+ /** If the try again button show be shown (only the button, see [canTryAgainNow]). */
+ val isTryAgainButtonVisible: Flow<Boolean> =
+ combine(
+ canTryAgainNow,
+ modalities,
+ ) { tryAgainIsPossible, modalities ->
+ tryAgainIsPossible && modalities.hasFaceOnly
+ }
+ .distinctUntilChanged()
+
+ /** If the credential fallback button show be shown. */
+ val isCredentialButtonVisible: Flow<Boolean> =
+ combine(
+ size,
+ isAuthenticated,
+ interactor.isCredentialAllowed,
+ ) { size, authState, credentialAllowed ->
+ size.isNotSmall && authState.isNotAuthenticated && credentialAllowed
+ }
+ .distinctUntilChanged()
+
+ private var messageJob: Job? = null
+
+ /**
+ * Show a temporary error [message] associated with an optional [failedModality].
+ *
+ * An optional [messageAfterError] will be shown via [showAuthenticating] when
+ * [authenticateAfterError] is set (or via [showHelp] when not set) after the error is
+ * dismissed.
+ *
+ * The error is ignored if the user has already authenticated and it is treated as
+ * [onSilentError] if [suppressIfErrorShowing] is set and an error message is already showing.
+ */
+ suspend fun showTemporaryError(
+ message: String,
+ messageAfterError: String = "",
+ authenticateAfterError: Boolean = false,
+ suppressIfErrorShowing: Boolean = false,
+ failedModality: BiometricModality = BiometricModality.None,
+ ) = coroutineScope {
+ if (_isAuthenticated.value.isAuthenticated) {
+ return@coroutineScope
+ }
+ if (_message.value.isErrorOrHelp && suppressIfErrorShowing) {
+ onSilentError(failedModality)
+ return@coroutineScope
+ }
+
+ _isAuthenticating.value = false
+ _isAuthenticated.value = PromptAuthState(false)
+ _forceMediumSize.value = true
+ _canTryAgainNow.value = supportsRetry(failedModality)
+ _message.value = PromptMessage.Error(message)
+ _legacyState.value = AuthBiometricView.STATE_ERROR
+
+ messageJob?.cancel()
+ messageJob = launch {
+ delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong())
+ if (authenticateAfterError) {
+ showAuthenticating(messageAfterError)
+ } else {
+ showHelp(messageAfterError)
+ }
+ }
+ }
+
+ /**
+ * Call instead of [showTemporaryError] if an error from the HAL should be silently ignored to
+ * enable retry (if the [failedModality] supports retrying).
+ *
+ * Ignored if the user has already authenticated.
+ */
+ private fun onSilentError(failedModality: BiometricModality = BiometricModality.None) {
+ if (_isAuthenticated.value.isNotAuthenticated) {
+ _canTryAgainNow.value = supportsRetry(failedModality)
+ }
+ }
+
+ /**
+ * Call to ensure the fingerprint sensor has started. Either when the dialog is first shown
+ * (most cases) or when it should be enabled after a first error (coex implicit flow).
+ */
+ fun ensureFingerprintHasStarted(isDelayed: Boolean) {
+ if (_fingerprintStartMode.value == FingerprintStartMode.Pending) {
+ _fingerprintStartMode.value =
+ if (isDelayed) FingerprintStartMode.Delayed else FingerprintStartMode.Normal
+ }
+ }
+
+ // enable retry only when face fails (fingerprint runs constantly)
+ private fun supportsRetry(failedModality: BiometricModality) =
+ failedModality == BiometricModality.Face
+
+ /**
+ * Show a persistent help message.
+ *
+ * Will be show even if the user has already authenticated.
+ */
+ suspend fun showHelp(message: String) {
+ val alreadyAuthenticated = _isAuthenticated.value.isAuthenticated
+ if (!alreadyAuthenticated) {
+ _isAuthenticating.value = false
+ _isAuthenticated.value = PromptAuthState(false)
+ }
+
+ _message.value =
+ if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty
+ _forceMediumSize.value = true
+ _legacyState.value =
+ if (alreadyAuthenticated) {
+ AuthBiometricView.STATE_PENDING_CONFIRMATION
+ } else {
+ AuthBiometricView.STATE_HELP
+ }
+
+ messageJob?.cancel()
+ messageJob = null
+ }
+
+ /**
+ * Show a temporary help message and transition back to a fixed message.
+ *
+ * Ignored if the user has already authenticated.
+ */
+ suspend fun showTemporaryHelp(
+ message: String,
+ messageAfterHelp: String = "",
+ ) = coroutineScope {
+ if (_isAuthenticated.value.isAuthenticated) {
+ return@coroutineScope
+ }
+
+ _isAuthenticating.value = false
+ _isAuthenticated.value = PromptAuthState(false)
+ _message.value =
+ if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty
+ _forceMediumSize.value = true
+ _legacyState.value = AuthBiometricView.STATE_HELP
+
+ messageJob?.cancel()
+ messageJob = launch {
+ delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong())
+ showAuthenticating(messageAfterHelp)
+ }
+ }
+
+ /** Show the user that biometrics are actively running and set [isAuthenticating]. */
+ fun showAuthenticating(message: String = "", isRetry: Boolean = false) {
+ if (_isAuthenticated.value.isAuthenticated) {
+ // TODO(jbolinger): convert to go/tex-apc?
+ Log.w(TAG, "Cannot show authenticating after authenticated")
+ return
+ }
+
+ _isAuthenticating.value = true
+ _isAuthenticated.value = PromptAuthState(false)
+ _message.value = if (message.isBlank()) PromptMessage.Empty else PromptMessage.Help(message)
+ _legacyState.value = AuthBiometricView.STATE_AUTHENTICATING
+
+ // reset the try again button(s) after the user attempts a retry
+ if (isRetry) {
+ _canTryAgainNow.value = false
+ }
+
+ messageJob?.cancel()
+ messageJob = null
+ }
+
+ /**
+ * Show successfully authentication, set [isAuthenticated], and dismiss the prompt after a
+ * [dismissAfterDelay] or prompt for explicit confirmation (if required).
+ */
+ suspend fun showAuthenticated(
+ modality: BiometricModality,
+ dismissAfterDelay: Long,
+ helpMessage: String = "",
+ ) {
+ if (_isAuthenticated.value.isAuthenticated) {
+ // TODO(jbolinger): convert to go/tex-apc?
+ Log.w(TAG, "Cannot show authenticated after authenticated")
+ return
+ }
+
+ _isAuthenticating.value = false
+ val needsUserConfirmation = needsExplicitConfirmation(modality)
+ _isAuthenticated.value =
+ PromptAuthState(true, modality, needsUserConfirmation, dismissAfterDelay)
+ _message.value = PromptMessage.Empty
+ _legacyState.value =
+ if (needsUserConfirmation) {
+ AuthBiometricView.STATE_PENDING_CONFIRMATION
+ } else {
+ AuthBiometricView.STATE_AUTHENTICATED
+ }
+
+ messageJob?.cancel()
+ messageJob = null
+
+ if (helpMessage.isNotBlank()) {
+ showHelp(helpMessage)
+ }
+ }
+
+ private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean {
+ val availableModalities = modalities.first()
+ val confirmationRequested = interactor.isConfirmationRequested.first()
+
+ if (availableModalities.hasFaceAndFingerprint) {
+ // coex only needs confirmation when face is successful, unless it happens on the
+ // first attempt (i.e. without failure) before fingerprint scanning starts
+ if (modality == BiometricModality.Face) {
+ return (fingerprintStartMode.first() != FingerprintStartMode.Pending) ||
+ confirmationRequested
+ }
+ }
+ if (availableModalities.hasFaceOnly) {
+ return confirmationRequested
+ }
+ // fingerprint only never requires confirmation
+ return false
+ }
+
+ /**
+ * Set the prompt's auth state to authenticated and confirmed.
+ *
+ * This should only be used after [showAuthenticated] when the operation requires explicit user
+ * confirmation.
+ */
+ fun confirmAuthenticated() {
+ val authState = _isAuthenticated.value
+ if (authState.isNotAuthenticated) {
+ "Cannot show authenticated after authenticated"
+ Log.w(TAG, "Cannot confirm authenticated when not authenticated")
+ return
+ }
+
+ _isAuthenticated.value = authState.asConfirmed()
+ _message.value = PromptMessage.Empty
+ _legacyState.value = AuthBiometricView.STATE_AUTHENTICATED
+
+ messageJob?.cancel()
+ messageJob = null
+ }
+
+ /**
+ * Switch to the credential view.
+ *
+ * TODO(b/251476085): this should be decoupled from the shared panel controller
+ */
+ fun onSwitchToCredential() {
+ _forceLargeSize.value = true
+ }
+
+ companion object {
+ private const val TAG = "PromptViewModel"
+ }
+}
+
+/** How the fingerprint sensor was started for the prompt. */
+enum class FingerprintStartMode {
+ /** Fingerprint sensor has not started. */
+ Pending,
+
+ /** Fingerprint sensor started immediately when prompt was displayed. */
+ Normal,
+
+ /** Fingerprint sensor started after the first failure of another passive modality. */
+ Delayed;
+
+ /** If this is [Normal] or [Delayed]. */
+ val isStarted: Boolean
+ get() = this == Normal || this == Delayed
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index c8485dc..e118fdf 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -217,6 +217,7 @@
)
/** Whether to use a new data source for intents to run on keyguard dismissal. */
+ // TODO(b/275069969): Tracking bug.
@JvmField
val REFACTOR_KEYGUARD_DISMISS_INTENT = unreleasedFlag(231, "refactor_keyguard_dismiss_intent")
@@ -247,6 +248,11 @@
@JvmField
val MIGRATE_INDICATION_AREA = unreleasedFlag(236, "migrate_indication_area")
+ /** Whether to listen for fingerprint authentication over keyguard occluding activities. */
+ // TODO(b/283260512): Tracking bug.
+ @JvmField
+ val FP_LISTEN_OCCLUDING_APPS = unreleasedFlag(237, "fp_listen_occluding_apps")
+
// 300 - power menu
// TODO(b/254512600): Tracking Bug
@JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
@@ -673,6 +679,10 @@
val TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK =
unreleasedFlag(2401, "trim_resources_with_background_trim_on_lock")
+ // TODO:(b/283203305): Tracking bug
+ @JvmField
+ val TRIM_FONT_CACHES_AT_UNLOCK = releasedFlag(2402, "trim_font_caches_on_unlock")
+
// 2700 - unfold transitions
// TODO(b/265764985): Tracking Bug
@Keep
@@ -717,4 +727,12 @@
@JvmField
val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION =
unreleasedFlag(2805, "split_shade_subpixel_optimization", teamfood = true)
+
+ // TODO(b/278761837): Tracking Bug
+ @JvmField
+ val USE_NEW_ACTIVITY_STARTER = releasedFlag(2801, name = "use_new_activity_starter")
+
+ // TODO(b/283084712): Tracking Bug
+ @JvmField
+ val IMPROVED_HUN_ANIMATIONS = unreleasedFlag(283084712, "improved_hun_animations")
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
index d3678b5..7078341 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
@@ -22,9 +22,12 @@
import android.app.Dialog
import android.content.Context
import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.OvalShape
import android.graphics.drawable.shapes.RoundRectShape
import android.os.Bundle
import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup.MarginLayoutParams
import android.view.Window
import android.view.WindowManager
import android.widget.FrameLayout
@@ -32,9 +35,10 @@
import android.widget.LinearLayout
import android.widget.LinearLayout.LayoutParams
import android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
+import androidx.annotation.IdRes
+import androidx.core.view.setPadding
import com.android.settingslib.Utils
import com.android.systemui.R
-import com.android.systemui.util.children
class KeyboardBacklightDialog(
context: Context,
@@ -51,7 +55,7 @@
private data class BacklightIconProperties(
val width: Int,
val height: Int,
- val leftMargin: Int,
+ val padding: Int,
)
private data class StepViewProperties(
@@ -71,6 +75,7 @@
private lateinit var rootProperties: RootProperties
private lateinit var iconProperties: BacklightIconProperties
private lateinit var stepProperties: StepViewProperties
+
@ColorInt
var filledRectangleColor = getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
@ColorInt
@@ -78,7 +83,16 @@
getColorFromStyle(com.android.internal.R.attr.materialColorOutlineVariant)
@ColorInt
var backgroundColor = getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceBright)
- @ColorInt var iconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnPrimary)
+ @ColorInt
+ var defaultIconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnPrimary)
+ @ColorInt
+ var defaultIconBackgroundColor =
+ getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
+ @ColorInt
+ var dimmedIconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnSurface)
+ @ColorInt
+ var dimmedIconBackgroundColor =
+ getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceDim)
init {
currentLevel = initialCurrentLevel
@@ -111,8 +125,7 @@
BacklightIconProperties(
width = getDimensionPixelSize(R.dimen.backlight_indicator_icon_width),
height = getDimensionPixelSize(R.dimen.backlight_indicator_icon_height),
- leftMargin =
- getDimensionPixelSize(R.dimen.backlight_indicator_icon_left_margin),
+ padding = getDimensionPixelSize(R.dimen.backlight_indicator_icon_padding),
)
stepProperties =
StepViewProperties(
@@ -139,23 +152,34 @@
if (maxLevel != max || forceRefresh) {
maxLevel = max
rootView.removeAllViews()
+ rootView.addView(buildIconTile())
buildStepViews().forEach { rootView.addView(it) }
}
currentLevel = current
- updateLevel()
+ updateIconTile()
+ updateStepColors()
}
- private fun updateLevel() {
- rootView.children.forEachIndexed(
- action = { index, v ->
- val drawable = v.background as ShapeDrawable
- if (index <= currentLevel) {
- updateColor(drawable, filledRectangleColor)
- } else {
- updateColor(drawable, emptyRectangleColor)
- }
- }
- )
+ private fun updateIconTile() {
+ val iconTile = rootView.findViewById(BACKLIGHT_ICON_ID) as ImageView
+ val backgroundDrawable = iconTile.background as ShapeDrawable
+ if (currentLevel == 0) {
+ iconTile.setColorFilter(dimmedIconColor)
+ updateColor(backgroundDrawable, dimmedIconBackgroundColor)
+ } else {
+ iconTile.setColorFilter(defaultIconColor)
+ updateColor(backgroundDrawable, defaultIconBackgroundColor)
+ }
+ }
+
+ private fun updateStepColors() {
+ (1 until rootView.childCount).forEach { index ->
+ val drawable = rootView.getChildAt(index).background as ShapeDrawable
+ updateColor(
+ drawable,
+ if (index <= currentLevel) filledRectangleColor else emptyRectangleColor,
+ )
+ }
}
private fun updateColor(drawable: ShapeDrawable, @ColorInt color: Int) {
@@ -192,9 +216,33 @@
}
private fun buildStepViews(): List<FrameLayout> {
- val stepViews = (0..maxLevel).map { i -> createStepViewAt(i) }
- stepViews[0].addView(createBacklightIconView())
- return stepViews
+ return (1..maxLevel).map { i -> createStepViewAt(i) }
+ }
+
+ private fun buildIconTile(): View {
+ val diameter = stepProperties.height
+ val circleDrawable =
+ ShapeDrawable(OvalShape()).apply {
+ intrinsicHeight = diameter
+ intrinsicWidth = diameter
+ }
+
+ return ImageView(context).apply {
+ setImageResource(R.drawable.ic_keyboard_backlight)
+ id = BACKLIGHT_ICON_ID
+ setColorFilter(defaultIconColor)
+ setPadding(iconProperties.padding)
+ layoutParams =
+ MarginLayoutParams(diameter, diameter).apply {
+ setMargins(
+ /* left= */ stepProperties.horizontalMargin,
+ /* top= */ 0,
+ /* right= */ stepProperties.horizontalMargin,
+ /* bottom= */ 0
+ )
+ }
+ background = circleDrawable
+ }
}
private fun createStepViewAt(i: Int): FrameLayout {
@@ -221,18 +269,6 @@
}
}
- private fun createBacklightIconView(): ImageView {
- return ImageView(context).apply {
- setImageResource(R.drawable.ic_keyboard_backlight)
- setColorFilter(iconColor)
- layoutParams =
- FrameLayout.LayoutParams(iconProperties.width, iconProperties.height).apply {
- gravity = Gravity.CENTER
- leftMargin = iconProperties.leftMargin
- }
- }
- }
-
private fun setWindowPosition() {
window?.apply {
setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)
@@ -262,30 +298,29 @@
private fun radiiForIndex(i: Int, last: Int): FloatArray {
val smallRadius = stepProperties.smallRadius
val largeRadius = stepProperties.largeRadius
- return when (i) {
- 0 -> // left radii bigger
- floatArrayOf(
- largeRadius,
- largeRadius,
- smallRadius,
- smallRadius,
- smallRadius,
- smallRadius,
- largeRadius,
- largeRadius
- )
- last -> // right radii bigger
- floatArrayOf(
- smallRadius,
- smallRadius,
- largeRadius,
- largeRadius,
- largeRadius,
- largeRadius,
- smallRadius,
- smallRadius
- )
- else -> FloatArray(8) { smallRadius } // all radii equal
+ val radii = FloatArray(8) { smallRadius }
+ if (i == 1) {
+ radii.setLeftCorners(largeRadius)
}
+ // note "first" and "last" might be the same tile
+ if (i == last) {
+ radii.setRightCorners(largeRadius)
+ }
+ return radii
+ }
+
+ private fun FloatArray.setLeftCorners(radius: Float) {
+ LEFT_CORNERS_INDICES.forEach { this[it] = radius }
+ }
+ private fun FloatArray.setRightCorners(radius: Float) {
+ RIGHT_CORNERS_INDICES.forEach { this[it] = radius }
+ }
+
+ private companion object {
+ @IdRes val BACKLIGHT_ICON_ID = R.id.backlight_icon
+
+ // indices used to define corners radii in ShapeDrawable
+ val LEFT_CORNERS_INDICES: IntArray = intArrayOf(0, 1, 6, 7)
+ val RIGHT_CORNERS_INDICES: IntArray = intArrayOf(2, 3, 4, 5)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
index 8386a05b..d8affa4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
@@ -18,6 +18,7 @@
import android.annotation.WorkerThread
import android.content.ComponentCallbacks2
+import android.graphics.HardwareRenderer
import android.os.Trace
import android.util.Log
import com.android.systemui.CoreStartable
@@ -27,12 +28,13 @@
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.utils.GlobalWindowManager
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
@@ -50,6 +52,7 @@
@Inject
constructor(
private val keyguardInteractor: KeyguardInteractor,
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val globalWindowManager: GlobalWindowManager,
@Application private val applicationScope: CoroutineScope,
@Background private val bgDispatcher: CoroutineDispatcher,
@@ -58,7 +61,10 @@
override fun start() {
Log.d(LOG_TAG, "Resource trimmer registered.")
- if (!featureFlags.isEnabled(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)) {
+ if (
+ !(featureFlags.isEnabled(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK) ||
+ featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK))
+ ) {
return
}
@@ -78,6 +84,30 @@
.distinctUntilChanged()
.collect { onWakefulnessUpdated(it.first, it.second, it.third) }
}
+
+ applicationScope.launch(bgDispatcher) {
+ // We drop 1 to avoid triggering on initial collect().
+ keyguardTransitionInteractor.anyStateToGoneTransition.collect { transition ->
+ if (transition.transitionState == TransitionState.FINISHED) {
+ onKeyguardGone()
+ }
+ }
+ }
+ }
+
+ @WorkerThread
+ private fun onKeyguardGone() {
+ if (!featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK)) {
+ return
+ }
+
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Trimming font caches since keyguard went away.")
+ }
+ // We want to clear temporary caches we've created while rendering and animating
+ // lockscreen elements, especially clocks.
+ globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
+ globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_FONT)
}
@WorkerThread
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index 6d7455f..752471d 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -16,12 +16,16 @@
package com.android.systemui.scene
+import com.android.systemui.scene.data.model.SceneContainerConfigModule
import com.android.systemui.scene.ui.composable.SceneModule
+import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModelModule
import dagger.Module
@Module(
includes =
[
+ SceneContainerConfigModule::class,
+ SceneContainerViewModelModule::class,
SceneModule::class,
],
)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfigModule.kt b/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfigModule.kt
new file mode 100644
index 0000000..0af8094
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfigModule.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.systemui.scene.data.model
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.scene.shared.model.SceneContainerNames
+import com.android.systemui.scene.shared.model.SceneKey
+import dagger.Module
+import dagger.Provides
+import javax.inject.Named
+
+@Module
+object SceneContainerConfigModule {
+
+ @Provides
+ fun containerConfigs(): Map<String, SceneContainerConfig> {
+ return mapOf(
+ SceneContainerNames.SYSTEM_UI_DEFAULT to
+ SceneContainerConfig(
+ name = SceneContainerNames.SYSTEM_UI_DEFAULT,
+ // Note that this list is in z-order. The first one is the bottom-most and the
+ // last
+ // one is top-most.
+ sceneKeys =
+ listOf(
+ SceneKey.Gone,
+ SceneKey.Lockscreen,
+ SceneKey.Bouncer,
+ SceneKey.Shade,
+ SceneKey.QuickSettings,
+ ),
+ initialSceneKey = SceneKey.Lockscreen,
+ ),
+ )
+ }
+
+ @Provides
+ @SysUISingleton
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+ fun provideDefaultSceneContainerConfig(
+ configs: Map<String, SceneContainerConfig>,
+ ): SceneContainerConfig {
+ return checkNotNull(configs[SceneContainerNames.SYSTEM_UI_DEFAULT]) {
+ "No SceneContainerConfig named \"${SceneContainerNames.SYSTEM_UI_DEFAULT}\"."
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerNames.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerNames.kt
new file mode 100644
index 0000000..64f5087
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerNames.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.systemui.scene.shared.model
+
+object SceneContainerNames {
+ const val SYSTEM_UI_DEFAULT = "system_ui"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index a4daafc..8c1ad9b 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -19,17 +19,12 @@
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.StateFlow
/** Models UI state for a single scene container. */
-class SceneContainerViewModel
-@AssistedInject
-constructor(
+class SceneContainerViewModel(
private val interactor: SceneInteractor,
- @Assisted val containerName: String,
+ val containerName: String,
) {
/**
* Keys of all scenes in the container.
@@ -54,11 +49,4 @@
fun setSceneTransitionProgress(progress: Float) {
interactor.setSceneTransitionProgress(containerName, progress)
}
-
- @AssistedFactory
- interface Factory {
- fun create(
- containerName: String,
- ): SceneContainerViewModel
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelModule.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelModule.kt
new file mode 100644
index 0000000..100f427
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelModule.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.systemui.scene.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneContainerNames
+import dagger.Module
+import dagger.Provides
+import javax.inject.Named
+
+@Module
+object SceneContainerViewModelModule {
+
+ @Provides
+ @SysUISingleton
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+ fun defaultSceneContainerViewModel(
+ interactor: SceneInteractor,
+ ): SceneContainerViewModel {
+ return SceneContainerViewModel(
+ interactor = interactor,
+ containerName = SceneContainerNames.SYSTEM_UI_DEFAULT,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index fb4feb8..a532195 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -33,7 +33,6 @@
import android.content.Context;
import android.graphics.drawable.Icon;
import android.hardware.biometrics.BiometricAuthenticator.Modality;
-import android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
import android.hardware.biometrics.IBiometricContextListener;
import android.hardware.biometrics.IBiometricSysuiReceiver;
import android.hardware.biometrics.PromptInfo;
@@ -317,7 +316,7 @@
IBiometricSysuiReceiver receiver,
int[] sensorIds, boolean credentialAllowed,
boolean requireConfirmation, int userId, long operationId, String opPackageName,
- long requestId, @BiometricMultiSensorMode int multiSensorConfig) {
+ long requestId) {
}
/** @see IStatusBar#onBiometricAuthenticated(int) */
@@ -956,8 +955,7 @@
@Override
public void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver,
int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation,
- int userId, long operationId, String opPackageName, long requestId,
- @BiometricMultiSensorMode int multiSensorConfig) {
+ int userId, long operationId, String opPackageName, long requestId) {
synchronized (mLock) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = promptInfo;
@@ -969,7 +967,6 @@
args.arg6 = opPackageName;
args.argl1 = operationId;
args.argl2 = requestId;
- args.argi2 = multiSensorConfig;
mHandler.obtainMessage(MSG_BIOMETRIC_SHOW, args)
.sendToTarget();
}
@@ -1573,8 +1570,7 @@
someArgs.argi1 /* userId */,
someArgs.argl1 /* operationId */,
(String) someArgs.arg6 /* opPackageName */,
- someArgs.argl2 /* requestId */,
- someArgs.argi2 /* multiSensorConfig */);
+ someArgs.argl2 /* requestId */);
}
someArgs.recycle();
break;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index df68e7e..0414a14 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -23,7 +23,6 @@
import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING;
-import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.res.ColorStateList;
import android.hardware.biometrics.BiometricSourceType;
@@ -36,7 +35,6 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewRootImpl;
-import android.view.WindowManagerGlobal;
import android.window.BackEvent;
import android.window.OnBackAnimationCallback;
import android.window.OnBackInvokedDispatcher;
@@ -985,8 +983,6 @@
mShadeViewController.resetViewGroupFade();
mCentralSurfaces.finishKeyguardFadingAway();
mBiometricUnlockController.finishKeyguardFadingAway();
- WindowManagerGlobal.getInstance().trimMemory(
- ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
}
private void wakeAndUnlockDejank() {
diff --git a/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt b/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt
index 038fddc..4111850 100644
--- a/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt
@@ -1,5 +1,6 @@
package com.android.systemui.utils
+import android.graphics.HardwareRenderer.CacheTrimLevel
import android.view.WindowManagerGlobal
import javax.inject.Inject
@@ -13,4 +14,9 @@
fun trimMemory(level: Int) {
WindowManagerGlobal.getInstance().trimMemory(level)
}
+
+ /** Sends a trim caches command to [WindowManagerGlobal]. */
+ fun trimCaches(@CacheTrimLevel level: Int) {
+ WindowManagerGlobal.getInstance().trimCaches(level)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt
index 213dc87..2d1e8a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt
@@ -73,7 +73,7 @@
@Test
fun fingerprintSuccessDoesNotRequireExplicitConfirmation() {
- biometricView.onDialogAnimatedIn()
+ biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
biometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT)
TestableLooper.get(this).moveTimeForward(1000)
waitForIdleSync()
@@ -84,7 +84,7 @@
@Test
fun faceSuccessRequiresExplicitConfirmation() {
- biometricView.onDialogAnimatedIn()
+ biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
biometricView.onAuthenticationSucceeded(TYPE_FACE)
waitForIdleSync()
@@ -104,7 +104,7 @@
@Test
fun ignoresFaceErrors_faceIsNotClass3_notLockoutError() {
- biometricView.onDialogAnimatedIn()
+ biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
biometricView.onError(TYPE_FACE, "not a face")
waitForIdleSync()
@@ -121,7 +121,7 @@
@Test
fun doNotIgnoresFaceErrors_faceIsClass3_notLockoutError() {
biometricView.isFaceClass3 = true
- biometricView.onDialogAnimatedIn()
+ biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
biometricView.onError(TYPE_FACE, "not a face")
waitForIdleSync()
@@ -138,7 +138,7 @@
@Test
fun doNotIgnoresFaceErrors_faceIsClass3_lockoutError() {
biometricView.isFaceClass3 = true
- biometricView.onDialogAnimatedIn()
+ biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
biometricView.onError(
TYPE_FACE,
FaceManager.getErrorString(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt
index 22ebc7e..8e5d96b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt
@@ -120,7 +120,7 @@
@Test
fun testNegativeButton_beforeAuthentication_sendsActionButtonNegative() {
- biometricView.onDialogAnimatedIn()
+ biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
biometricView.mNegativeButton.performClick()
TestableLooper.get(this).moveTimeForward(1000)
waitForIdleSync()
@@ -212,7 +212,7 @@
@Test
fun testIgnoresUselessHelp() {
biometricView.mAnimationDurationHideDialog = 10_000
- biometricView.onDialogAnimatedIn()
+ biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
waitForIdleSync()
assertThat(biometricView.isAuthenticating).isTrue()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index 9d68cf3..d31a86a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -41,11 +41,15 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FakePromptRepository
import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository
-import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
+import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel
import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
@@ -53,29 +57,34 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
import org.junit.After
+import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
+import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.eq
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
+import org.mockito.Mockito.`when` as whenever
@RunWith(AndroidJUnit4::class)
@RunWithLooper(setAsMainLooper = true)
@SmallTest
-class AuthContainerViewTest : SysuiTestCase() {
+open class AuthContainerViewTest : SysuiTestCase() {
@JvmField @Rule
var mockitoRule = MockitoJUnit.rule()
+ private val featureFlags = FakeFeatureFlags()
+
@Mock
lateinit var callback: AuthDialogCallback
@Mock
@@ -91,16 +100,25 @@
@Mock
lateinit var interactionJankMonitor: InteractionJankMonitor
+ // TODO(b/278622168): remove with flag
+ open val useNewBiometricPrompt = false
+
private val testScope = TestScope(StandardTestDispatcher())
private val fakeExecutor = FakeExecutor(FakeSystemClock())
private val biometricPromptRepository = FakePromptRepository()
private val rearDisplayStateRepository = FakeRearDisplayStateRepository()
private val credentialInteractor = FakeCredentialInteractor()
- private val bpCredentialInteractor = BiometricPromptCredentialInteractor(
+ private val bpCredentialInteractor = PromptCredentialInteractor(
Dispatchers.Main.immediate,
biometricPromptRepository,
- credentialInteractor
+ credentialInteractor,
)
+ private val promptSelectorInteractor by lazy {
+ PromptSelectorInteractorImpl(
+ biometricPromptRepository,
+ lockPatternUtils,
+ )
+ }
private val displayStateInteractor = DisplayStateInteractorImpl(
testScope.backgroundScope,
mContext,
@@ -115,6 +133,11 @@
private var authContainer: TestAuthContainerView? = null
+ @Before
+ fun setup() {
+ featureFlags.set(Flags.BIOMETRIC_BP_STRONG, useNewBiometricPrompt)
+ }
+
@After
fun tearDown() {
if (authContainer?.isAttachedToWindow == true) {
@@ -125,7 +148,7 @@
@Test
fun testNotifiesAnimatedIn() {
initializeFingerprintContainer()
- verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L)
+ verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */)
}
@Test
@@ -164,13 +187,13 @@
container.dismissFromSystemServer()
waitForIdleSync()
- verify(callback, never()).onDialogAnimatedIn(anyLong())
+ verify(callback, never()).onDialogAnimatedIn(anyLong(), anyBoolean())
container.addToView()
waitForIdleSync()
// attaching the view resets the state and allows this to happen again
- verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L)
+ verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */)
}
@Test
@@ -185,7 +208,7 @@
// the first time is triggered by initializeFingerprintContainer()
// the second time was triggered by dismissWithoutCallback()
- verify(callback, times(2)).onDialogAnimatedIn(authContainer?.requestId ?: 0L)
+ verify(callback, times(2)).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */)
}
@Test
@@ -479,6 +502,8 @@
this.authenticators = authenticators
}
},
+ featureFlags,
+ testScope.backgroundScope,
fingerprintProps,
faceProps,
wakefulnessLifecycle,
@@ -486,8 +511,10 @@
userManager,
lockPatternUtils,
interactionJankMonitor,
- { bpCredentialInteractor },
{ authBiometricFingerprintViewModel },
+ { promptSelectorInteractor },
+ { bpCredentialInteractor },
+ PromptViewModel(promptSelectorInteractor),
{ credentialViewModel },
Handler(TestableLooper.get(this).looper),
fakeExecutor
@@ -497,7 +524,10 @@
}
}
- override fun waitForIdleSync() = TestableLooper.get(this).processAllMessages()
+ override fun waitForIdleSync() {
+ testScope.runCurrent()
+ TestableLooper.get(this).processAllMessages()
+ }
private fun AuthContainerView.addToView() {
ViewUtils.attachView(this)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest2.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest2.kt
new file mode 100644
index 0000000..b56d055
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest2.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.systemui.biometrics
+
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.runner.RunWith
+
+// TODO(b/278622168): remove with flag
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+class AuthContainerViewTest2 : AuthContainerViewTest() {
+ override val useNewBiometricPrompt = true
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index a326cc7..b9f92a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -18,7 +18,6 @@
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.Authenticators;
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
import static com.google.common.truth.Truth.assertThat;
@@ -54,7 +53,6 @@
import android.graphics.Point;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricConstants;
-import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.BiometricStateListener;
import android.hardware.biometrics.ComponentInfoInternal;
@@ -91,10 +89,14 @@
import com.android.settingslib.udfps.UdfpsUtils;
import com.android.systemui.RoboPilotTest;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
import com.android.systemui.biometrics.domain.interactor.LogContextInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel;
import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.VibratorHelper;
@@ -171,12 +173,16 @@
@Mock
private InteractionJankMonitor mInteractionJankMonitor;
@Mock
- private BiometricPromptCredentialInteractor mBiometricPromptCredentialInteractor;
+ private PromptCredentialInteractor mBiometricPromptCredentialInteractor;
+ @Mock
+ private PromptSelectorInteractor mPromptSelectionInteractor;
@Mock
private AuthBiometricFingerprintViewModel mAuthBiometricFingerprintViewModel;
@Mock
private CredentialViewModel mCredentialViewModel;
@Mock
+ private PromptViewModel mPromptViewModel;
+ @Mock
private UdfpsUtils mUdfpsUtils;
@Captor
@@ -194,12 +200,17 @@
private Handler mHandler;
private DelayableExecutor mBackgroundExecutor;
private TestableAuthController mAuthController;
+ private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Mock
private VibratorHelper mVibratorHelper;
@Before
public void setup() throws RemoteException {
+ // TODO(b/278622168): remove with flag
+ // AuthController simply passes this through to AuthContainerView (does not impact test)
+ mFeatureFlags.set(Flags.BIOMETRIC_BP_STRONG, false);
+
mContextSpy = spy(mContext);
mExecution = new FakeExecution();
mTestableLooper = TestableLooper.get(this);
@@ -952,8 +963,7 @@
0 /* userId */,
0 /* operationId */,
"testPackage",
- REQUEST_ID,
- BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE);
+ REQUEST_ID);
}
private void switchTask(String packageName) {
@@ -993,25 +1003,26 @@
private PromptInfo mLastBiometricPromptInfo;
TestableAuthController(Context context) {
- super(context, mExecution, mCommandQueue, mActivityTaskManager, mWindowManager,
+ super(context, mFeatureFlags, null /* applicationCoroutineScope */,
+ mExecution, mCommandQueue, mActivityTaskManager, mWindowManager,
mFingerprintManager, mFaceManager, () -> mUdfpsController,
() -> mSideFpsController, mDisplayManager, mWakefulnessLifecycle,
mPanelInteractionDetector, mUserManager, mLockPatternUtils, mUdfpsLogger,
- mLogContextInteractor, () -> mBiometricPromptCredentialInteractor,
- () -> mAuthBiometricFingerprintViewModel, () -> mCredentialViewModel,
- mInteractionJankMonitor, mHandler, mBackgroundExecutor, mVibratorHelper,
- mUdfpsUtils);
+ mLogContextInteractor, () -> mAuthBiometricFingerprintViewModel,
+ () -> mBiometricPromptCredentialInteractor, () -> mPromptSelectionInteractor,
+ () -> mCredentialViewModel, () -> mPromptViewModel,
+ mInteractionJankMonitor, mHandler,
+ mBackgroundExecutor, mVibratorHelper, mUdfpsUtils);
}
@Override
protected AuthDialog buildDialog(DelayableExecutor bgExecutor, PromptInfo promptInfo,
boolean requireConfirmation, int userId, int[] sensorIds,
String opPackageName, boolean skipIntro, long operationId, long requestId,
- @BiometricManager.BiometricMultiSensorMode int multiSensorConfig,
WakefulnessLifecycle wakefulnessLifecycle,
AuthDialogPanelInteractionDetector panelInteractionDetector,
UserManager userManager,
- LockPatternUtils lockPatternUtils) {
+ LockPatternUtils lockPatternUtils, PromptViewModel viewModel) {
mLastBiometricPromptInfo = promptInfo;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
index 1379a0e..94244cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
@@ -18,10 +18,11 @@
import android.annotation.IdRes
import android.content.Context
-import android.hardware.biometrics.BiometricManager
+import android.hardware.biometrics.BiometricManager.Authenticators
import android.hardware.biometrics.ComponentInfoInternal
import android.hardware.biometrics.PromptInfo
import android.hardware.biometrics.SensorProperties
+import android.hardware.biometrics.SensorPropertiesInternal
import android.hardware.face.FaceSensorProperties
import android.hardware.face.FaceSensorPropertiesInternal
import android.hardware.fingerprint.FingerprintSensorProperties
@@ -61,9 +62,9 @@
private fun buildPromptInfo(allowDeviceCredential: Boolean): PromptInfo {
val promptInfo = PromptInfo()
promptInfo.title = "Title"
- var authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK
+ var authenticators = Authenticators.BIOMETRIC_WEAK
if (allowDeviceCredential) {
- authenticators = authenticators or BiometricManager.Authenticators.DEVICE_CREDENTIAL
+ authenticators = authenticators or Authenticators.DEVICE_CREDENTIAL
} else {
promptInfo.negativeButtonText = "Negative"
}
@@ -80,7 +81,8 @@
/** Create [FingerprintSensorPropertiesInternal] for a test. */
internal fun fingerprintSensorPropertiesInternal(
- ids: List<Int> = listOf(0)
+ ids: List<Int> = listOf(0),
+ strong: Boolean = true,
): List<FingerprintSensorPropertiesInternal> {
val componentInfo =
listOf(
@@ -102,7 +104,7 @@
return ids.map { id ->
FingerprintSensorPropertiesInternal(
id,
- SensorProperties.STRENGTH_STRONG,
+ if (strong) SensorProperties.STRENGTH_STRONG else SensorProperties.STRENGTH_WEAK,
5 /* maxEnrollmentsPerUser */,
componentInfo,
FingerprintSensorProperties.TYPE_REAR,
@@ -113,7 +115,8 @@
/** Create [FaceSensorPropertiesInternal] for a test. */
internal fun faceSensorPropertiesInternal(
- ids: List<Int> = listOf(1)
+ ids: List<Int> = listOf(1),
+ strong: Boolean = true,
): List<FaceSensorPropertiesInternal> {
val componentInfo =
listOf(
@@ -135,7 +138,7 @@
return ids.map { id ->
FaceSensorPropertiesInternal(
id,
- SensorProperties.STRENGTH_STRONG,
+ if (strong) SensorProperties.STRENGTH_STRONG else SensorProperties.STRENGTH_WEAK,
2 /* maxEnrollmentsPerUser */,
componentInfo,
FaceSensorProperties.TYPE_RGB,
@@ -146,6 +149,24 @@
}
}
+@Authenticators.Types
+internal fun Collection<SensorPropertiesInternal?>.extractAuthenticatorTypes(): Int {
+ var authenticators = Authenticators.EMPTY_SET
+ mapNotNull { it?.sensorStrength }
+ .forEach { strength ->
+ authenticators =
+ authenticators or
+ when (strength) {
+ SensorProperties.STRENGTH_CONVENIENCE ->
+ Authenticators.BIOMETRIC_CONVENIENCE
+ SensorProperties.STRENGTH_WEAK -> Authenticators.BIOMETRIC_WEAK
+ SensorProperties.STRENGTH_STRONG -> Authenticators.BIOMETRIC_STRONG
+ else -> Authenticators.EMPTY_SET
+ }
+ }
+ return authenticators
+}
+
internal fun promptInfo(
title: String = "title",
subtitle: String = "sub",
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt
index 2d5614c..4836af6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt
@@ -4,7 +4,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.AuthController
-import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.biometrics.shared.model.PromptKind
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
@@ -60,7 +60,7 @@
@Test
fun setsAndUnsetsPrompt() = runBlockingTest {
- val kind = PromptKind.PIN
+ val kind = PromptKind.Pin
val uid = 8
val challenge = 90L
val promptInfo = PromptInfo()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
index dbcbf41..720a35c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
@@ -9,15 +9,17 @@
import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
import com.android.systemui.biometrics.domain.model.BiometricUserInfo
import com.android.systemui.biometrics.promptInfo
+import com.android.systemui.coroutines.collectLastValue
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
@@ -36,42 +38,39 @@
@JvmField @Rule var mockitoRule = MockitoJUnit.rule()
- private val dispatcher = UnconfinedTestDispatcher()
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
private val biometricPromptRepository = FakePromptRepository()
private val credentialInteractor = FakeCredentialInteractor()
- private lateinit var interactor: BiometricPromptCredentialInteractor
+ private lateinit var interactor: PromptCredentialInteractor
@Before
fun setup() {
interactor =
- BiometricPromptCredentialInteractor(
- dispatcher,
+ PromptCredentialInteractor(
+ testDispatcher,
biometricPromptRepository,
- credentialInteractor
+ credentialInteractor,
)
}
@Test
fun testIsShowing() =
- runTest(dispatcher) {
- var showing = false
- val job = launch { interactor.isShowing.collect { showing = it } }
+ testScope.runTest {
+ val showing by collectLastValue(interactor.isShowing)
biometricPromptRepository.setIsShowing(false)
assertThat(showing).isFalse()
biometricPromptRepository.setIsShowing(true)
assertThat(showing).isTrue()
-
- job.cancel()
}
@Test
fun testShowError() =
- runTest(dispatcher) {
- var error: CredentialStatus.Fail? = null
- val job = launch { interactor.verificationError.collect { error = it } }
+ testScope.runTest {
+ val error by collectLastValue(interactor.verificationError)
for (msg in listOf("once", "again")) {
interactor.setVerificationError(error(msg))
@@ -80,19 +79,14 @@
interactor.resetVerificationError()
assertThat(error).isNull()
-
- job.cancel()
}
@Test
fun nullWhenNoPromptInfo() =
- runTest(dispatcher) {
- var prompt: BiometricPromptRequest? = null
- val job = launch { interactor.prompt.collect { prompt = it } }
+ testScope.runTest {
+ val prompt by collectLastValue(interactor.prompt)
assertThat(prompt).isNull()
-
- job.cancel()
}
@Test fun usePinCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PIN)
@@ -102,12 +96,11 @@
@Test fun usePatternCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PATTERN)
private fun useCredentialForPrompt(kind: Int) =
- runTest(dispatcher) {
+ testScope.runTest {
val isStealth = false
credentialInteractor.stealthMode = isStealth
- var prompt: BiometricPromptRequest? = null
- val job = launch { interactor.prompt.collect { prompt = it } }
+ val prompt by collectLastValue(interactor.prompt)
val title = "what a prompt"
val subtitle = "s"
@@ -124,14 +117,12 @@
challenge = OPERATION_ID
)
- val p = prompt as? BiometricPromptRequest.Credential
- assertThat(p).isNotNull()
- assertThat(p!!.title).isEqualTo(title)
- assertThat(p.subtitle).isEqualTo(subtitle)
- assertThat(p.description).isEqualTo(description)
- assertThat(p.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
- assertThat(p.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
- assertThat(p)
+ assertThat(prompt?.title).isEqualTo(title)
+ assertThat(prompt?.subtitle).isEqualTo(subtitle)
+ assertThat(prompt?.description).isEqualTo(description)
+ assertThat(prompt?.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
+ assertThat(prompt?.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
+ assertThat(prompt)
.isInstanceOf(
when (kind) {
Utils.CREDENTIAL_PIN -> BiometricPromptRequest.Credential.Pin::class.java
@@ -142,25 +133,25 @@
else -> throw Exception("wrong kind")
}
)
- if (p is BiometricPromptRequest.Credential.Pattern) {
- assertThat(p.stealthMode).isEqualTo(isStealth)
+ val pattern = prompt as? BiometricPromptRequest.Credential.Pattern
+ if (pattern != null) {
+ assertThat(pattern.stealthMode).isEqualTo(isStealth)
}
interactor.resetPrompt()
assertThat(prompt).isNull()
-
- job.cancel()
}
@Test
fun checkCredential() =
- runTest(dispatcher) {
+ testScope.runTest {
val hat = ByteArray(4)
credentialInteractor.verifyCredentialResponse = { _ -> flowOf(verified(hat)) }
val errors = mutableListOf<CredentialStatus.Fail?>()
val job = launch { interactor.verificationError.toList(errors) }
+ runCurrent()
val checked =
interactor.checkCredential(pinRequest(), text = "1234")
@@ -168,6 +159,8 @@
assertThat(checked).isNotNull()
assertThat(checked!!.hat).isSameInstanceAs(hat)
+
+ runCurrent()
assertThat(errors.map { it?.error }).containsExactly(null)
job.cancel()
@@ -175,7 +168,7 @@
@Test
fun checkCredentialWhenBad() =
- runTest(dispatcher) {
+ testScope.runTest {
val errorMessage = "bad"
val remainingAttempts = 12
credentialInteractor.verifyCredentialResponse = { _ ->
@@ -184,6 +177,7 @@
val errors = mutableListOf<CredentialStatus.Fail?>()
val job = launch { interactor.verificationError.toList(errors) }
+ runCurrent()
val checked =
interactor.checkCredential(pinRequest(), text = "1234")
@@ -192,6 +186,8 @@
assertThat(checked).isNotNull()
assertThat(checked!!.remainingAttempts).isEqualTo(remainingAttempts)
assertThat(checked.urgentMessage).isNull()
+
+ runCurrent()
assertThat(errors.map { it?.error }).containsExactly(null, errorMessage).inOrder()
job.cancel()
@@ -199,7 +195,7 @@
@Test
fun checkCredentialWhenBadAndUrgentMessage() =
- runTest(dispatcher) {
+ testScope.runTest {
val error = "not so bad"
val urgentMessage = "really bad"
credentialInteractor.verifyCredentialResponse = { _ ->
@@ -208,6 +204,7 @@
val errors = mutableListOf<CredentialStatus.Fail?>()
val job = launch { interactor.verificationError.toList(errors) }
+ runCurrent()
val checked =
interactor.checkCredential(pinRequest(), text = "1234")
@@ -215,6 +212,8 @@
assertThat(checked).isNotNull()
assertThat(checked!!.urgentMessage).isEqualTo(urgentMessage)
+
+ runCurrent()
assertThat(errors.map { it?.error }).containsExactly(null, error).inOrder()
assertThat(errors.last() as? CredentialStatus.Fail.Error)
.isEqualTo(error(error, 10, urgentMessage))
@@ -224,7 +223,7 @@
@Test
fun checkCredentialWhenBadAndThrottled() =
- runTest(dispatcher) {
+ testScope.runTest {
val remainingAttempts = 3
val error = ":("
val urgentMessage = ":D"
@@ -239,6 +238,7 @@
}
val errors = mutableListOf<CredentialStatus.Fail?>()
val job = launch { interactor.verificationError.toList(errors) }
+ runCurrent()
val checked =
interactor.checkCredential(pinRequest(), text = "1234")
@@ -246,6 +246,8 @@
assertThat(checked).isNotNull()
assertThat(checked!!.remainingAttempts).isEqualTo(remainingAttempts)
+
+ runCurrent()
assertThat(checked.urgentMessage).isEqualTo(urgentMessage)
assertThat(errors.map { it?.error })
.containsExactly(null, "1", "2", "3", error)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
new file mode 100644
index 0000000..a62ea3b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
@@ -0,0 +1,204 @@
+/*
+ * 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.systemui.biometrics.domain.interactor
+
+import android.app.admin.DevicePolicyManager
+import android.hardware.biometrics.BiometricManager.Authenticators
+import android.hardware.biometrics.PromptInfo
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.data.repository.FakePromptRepository
+import com.android.systemui.biometrics.domain.model.BiometricModalities
+import com.android.systemui.biometrics.faceSensorPropertiesInternal
+import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
+import com.android.systemui.biometrics.shared.model.PromptKind
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+
+private const val TITLE = "hey there"
+private const val SUBTITLE = "ok"
+private const val DESCRIPTION = "football"
+private const val NEGATIVE_TEXT = "escape"
+
+private const val USER_ID = 8
+private const val CHALLENGE = 999L
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class PromptSelectorInteractorImplTest : SysuiTestCase() {
+
+ @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var lockPatternUtils: LockPatternUtils
+
+ private val testScope = TestScope()
+ private val promptRepository = FakePromptRepository()
+
+ private lateinit var interactor: PromptSelectorInteractor
+
+ @Before
+ fun setup() {
+ interactor = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils)
+ }
+
+ @Test
+ fun useBiometricsAndReset() =
+ testScope.runTest { useBiometricsAndReset(allowCredentialFallback = true) }
+
+ @Test
+ fun useBiometricsAndResetWithoutFallback() =
+ testScope.runTest { useBiometricsAndReset(allowCredentialFallback = false) }
+
+ private fun TestScope.useBiometricsAndReset(allowCredentialFallback: Boolean) {
+ setUserCredentialType(isPassword = true)
+
+ val confirmationRequired = true
+ val info =
+ PromptInfo().apply {
+ title = TITLE
+ subtitle = SUBTITLE
+ description = DESCRIPTION
+ negativeButtonText = NEGATIVE_TEXT
+ isConfirmationRequested = confirmationRequired
+ authenticators =
+ if (allowCredentialFallback) {
+ Authenticators.BIOMETRIC_STRONG or Authenticators.DEVICE_CREDENTIAL
+ } else {
+ Authenticators.BIOMETRIC_STRONG
+ }
+ isDeviceCredentialAllowed = allowCredentialFallback
+ }
+ val modalities =
+ BiometricModalities(
+ fingerprintProperties = fingerprintSensorPropertiesInternal().first(),
+ faceProperties = faceSensorPropertiesInternal().first(),
+ )
+
+ val currentPrompt by collectLastValue(interactor.prompt)
+ val credentialKind by collectLastValue(interactor.credentialKind)
+ val isCredentialAllowed by collectLastValue(interactor.isCredentialAllowed)
+ val isExplicitConfirmationRequired by collectLastValue(interactor.isConfirmationRequested)
+
+ assertThat(currentPrompt).isNull()
+
+ interactor.useBiometricsForAuthentication(
+ info,
+ confirmationRequired,
+ USER_ID,
+ CHALLENGE,
+ modalities
+ )
+
+ assertThat(currentPrompt).isNotNull()
+ assertThat(currentPrompt?.title).isEqualTo(TITLE)
+ assertThat(currentPrompt?.description).isEqualTo(DESCRIPTION)
+ assertThat(currentPrompt?.subtitle).isEqualTo(SUBTITLE)
+ assertThat(currentPrompt?.negativeButtonText).isEqualTo(NEGATIVE_TEXT)
+
+ if (allowCredentialFallback) {
+ assertThat(credentialKind).isSameInstanceAs(PromptKind.Password)
+ assertThat(isCredentialAllowed).isTrue()
+ } else {
+ assertThat(credentialKind).isEqualTo(PromptKind.Biometric())
+ assertThat(isCredentialAllowed).isFalse()
+ }
+ assertThat(isExplicitConfirmationRequired).isEqualTo(confirmationRequired)
+
+ interactor.resetPrompt()
+ verifyUnset()
+ }
+
+ @Test
+ fun usePinCredentialAndReset() =
+ testScope.runTest { useCredentialAndReset(Utils.CREDENTIAL_PIN) }
+
+ @Test
+ fun usePattermCredentialAndReset() =
+ testScope.runTest { useCredentialAndReset(Utils.CREDENTIAL_PATTERN) }
+
+ @Test
+ fun usePasswordCredentialAndReset() =
+ testScope.runTest { useCredentialAndReset(Utils.CREDENTIAL_PASSWORD) }
+
+ private fun TestScope.useCredentialAndReset(@Utils.CredentialType kind: Int) {
+ setUserCredentialType(
+ isPin = kind == Utils.CREDENTIAL_PIN,
+ isPassword = kind == Utils.CREDENTIAL_PASSWORD,
+ )
+
+ val info =
+ PromptInfo().apply {
+ title = TITLE
+ subtitle = SUBTITLE
+ description = DESCRIPTION
+ negativeButtonText = NEGATIVE_TEXT
+ authenticators = Authenticators.DEVICE_CREDENTIAL
+ isDeviceCredentialAllowed = true
+ }
+
+ val currentPrompt by collectLastValue(interactor.prompt)
+ val credentialKind by collectLastValue(interactor.credentialKind)
+
+ assertThat(currentPrompt).isNull()
+
+ interactor.useCredentialsForAuthentication(info, kind, USER_ID, CHALLENGE)
+
+ // not using biometrics, should be null with no fallback option
+ assertThat(currentPrompt).isNull()
+ assertThat(credentialKind).isEqualTo(PromptKind.Biometric())
+
+ interactor.resetPrompt()
+ verifyUnset()
+ }
+
+ private fun TestScope.verifyUnset() {
+ val currentPrompt by collectLastValue(interactor.prompt)
+ val credentialKind by collectLastValue(interactor.credentialKind)
+
+ assertThat(currentPrompt).isNull()
+
+ val kind = credentialKind as? PromptKind.Biometric
+ assertThat(kind).isNotNull()
+ assertThat(kind?.activeModalities?.isEmpty).isTrue()
+ }
+
+ private fun setUserCredentialType(isPin: Boolean = false, isPassword: Boolean = false) {
+ whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(any()))
+ .thenReturn(
+ when {
+ isPin -> DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
+ isPassword -> DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
+ else -> DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
+ }
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricModalitiesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricModalitiesTest.kt
new file mode 100644
index 0000000..526b833
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricModalitiesTest.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.systemui.biometrics.domain.model
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.faceSensorPropertiesInternal
+import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class BiometricModalitiesTest : SysuiTestCase() {
+
+ @Test
+ fun isEmpty() {
+ assertThat(BiometricModalities().isEmpty).isTrue()
+ }
+
+ @Test
+ fun fingerprintOnly() {
+ with(
+ BiometricModalities(
+ fingerprintProperties = fingerprintSensorPropertiesInternal().first(),
+ )
+ ) {
+ assertThat(isEmpty).isFalse()
+ assertThat(hasFace).isFalse()
+ assertThat(hasFaceOnly).isFalse()
+ assertThat(hasFingerprint).isTrue()
+ assertThat(hasFingerprintOnly).isTrue()
+ assertThat(hasFaceAndFingerprint).isFalse()
+ }
+ }
+
+ @Test
+ fun faceOnly() {
+ with(BiometricModalities(faceProperties = faceSensorPropertiesInternal().first())) {
+ assertThat(isEmpty).isFalse()
+ assertThat(hasFace).isTrue()
+ assertThat(hasFaceOnly).isTrue()
+ assertThat(hasFingerprint).isFalse()
+ assertThat(hasFingerprintOnly).isFalse()
+ assertThat(hasFaceAndFingerprint).isFalse()
+ }
+ }
+
+ @Test
+ fun faceStrength() {
+ with(
+ BiometricModalities(
+ fingerprintProperties = fingerprintSensorPropertiesInternal(strong = false).first(),
+ faceProperties = faceSensorPropertiesInternal(strong = true).first()
+ )
+ ) {
+ assertThat(isFaceStrong).isTrue()
+ }
+
+ with(
+ BiometricModalities(
+ fingerprintProperties = fingerprintSensorPropertiesInternal(strong = false).first(),
+ faceProperties = faceSensorPropertiesInternal(strong = false).first()
+ )
+ ) {
+ assertThat(isFaceStrong).isFalse()
+ }
+ }
+
+ @Test
+ fun faceAndFingerprint() {
+ with(
+ BiometricModalities(
+ fingerprintProperties = fingerprintSensorPropertiesInternal().first(),
+ faceProperties = faceSensorPropertiesInternal().first(),
+ )
+ ) {
+ assertThat(isEmpty).isFalse()
+ assertThat(hasFace).isTrue()
+ assertThat(hasFingerprint).isTrue()
+ assertThat(hasFaceOnly).isFalse()
+ assertThat(hasFingerprintOnly).isFalse()
+ assertThat(hasFaceAndFingerprint).isTrue()
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
index 4c5e3c1..e352905 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
@@ -2,6 +2,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
import com.android.systemui.biometrics.promptInfo
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -21,11 +22,13 @@
val subtitle = "a"
val description = "request"
+ val fpPros = fingerprintSensorPropertiesInternal().first()
val request =
BiometricPromptRequest.Biometric(
promptInfo(title = title, subtitle = subtitle, description = description),
BiometricUserInfo(USER_ID),
- BiometricOperationInfo(OPERATION_ID)
+ BiometricOperationInfo(OPERATION_ID),
+ BiometricModalities(fingerprintProperties = fpPros),
)
assertThat(request.title).isEqualTo(title)
@@ -33,6 +36,8 @@
assertThat(request.description).isEqualTo(description)
assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
+ assertThat(request.modalities)
+ .isEqualTo(BiometricModalities(fingerprintProperties = fpPros))
}
@Test
@@ -51,19 +56,19 @@
description = description,
credentialTitle = null,
credentialSubtitle = null,
- credentialDescription = null
+ credentialDescription = null,
),
BiometricUserInfo(USER_ID),
- BiometricOperationInfo(OPERATION_ID)
+ BiometricOperationInfo(OPERATION_ID),
),
BiometricPromptRequest.Credential.Password(
promptInfo(
credentialTitle = title,
credentialSubtitle = subtitle,
- credentialDescription = description
+ credentialDescription = description,
),
BiometricUserInfo(USER_ID),
- BiometricOperationInfo(OPERATION_ID)
+ BiometricOperationInfo(OPERATION_ID),
),
BiometricPromptRequest.Credential.Pattern(
promptInfo(
@@ -71,11 +76,11 @@
description = description,
credentialTitle = title,
credentialSubtitle = null,
- credentialDescription = null
+ credentialDescription = null,
),
BiometricUserInfo(USER_ID),
BiometricOperationInfo(OPERATION_ID),
- stealth
+ stealth,
)
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
index d73cdfc..3245020 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
@@ -2,12 +2,12 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.data.model.PromptKind
import com.android.systemui.biometrics.data.repository.FakePromptRepository
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor
import com.android.systemui.biometrics.domain.interactor.CredentialStatus
import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor
import com.android.systemui.biometrics.promptInfo
+import com.android.systemui.biometrics.shared.model.PromptKind
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
@@ -40,17 +40,13 @@
viewModel =
CredentialViewModel(
mContext,
- BiometricPromptCredentialInteractor(
- dispatcher,
- promptRepository,
- credentialInteractor
- )
+ PromptCredentialInteractor(dispatcher, promptRepository, credentialInteractor)
)
}
- @Test fun setsPinInputFlags() = setsInputFlags(PromptKind.PIN, expectFlags = true)
- @Test fun setsPasswordInputFlags() = setsInputFlags(PromptKind.PASSWORD, expectFlags = false)
- @Test fun setsPatternInputFlags() = setsInputFlags(PromptKind.PATTERN, expectFlags = false)
+ @Test fun setsPinInputFlags() = setsInputFlags(PromptKind.Pin, expectFlags = true)
+ @Test fun setsPasswordInputFlags() = setsInputFlags(PromptKind.Password, expectFlags = false)
+ @Test fun setsPatternInputFlags() = setsInputFlags(PromptKind.Pattern, expectFlags = false)
private fun setsInputFlags(type: PromptKind, expectFlags: Boolean) =
runTestWithKind(type) {
@@ -65,10 +61,10 @@
job.cancel()
}
- @Test fun isStealthIgnoredByPin() = isStealthMode(PromptKind.PIN, expectStealth = false)
+ @Test fun isStealthIgnoredByPin() = isStealthMode(PromptKind.Pin, expectStealth = false)
@Test
- fun isStealthIgnoredByPassword() = isStealthMode(PromptKind.PASSWORD, expectStealth = false)
- @Test fun isStealthUsedByPattern() = isStealthMode(PromptKind.PATTERN, expectStealth = true)
+ fun isStealthIgnoredByPassword() = isStealthMode(PromptKind.Password, expectStealth = false)
+ @Test fun isStealthUsedByPattern() = isStealthMode(PromptKind.Pattern, expectStealth = true)
private fun isStealthMode(type: PromptKind, expectStealth: Boolean) =
runTestWithKind(type, init = { credentialInteractor.stealthMode = true }) {
@@ -119,7 +115,7 @@
val attestations = mutableListOf<ByteArray?>()
val remainingAttempts = mutableListOf<RemainingAttempts?>()
- var header: HeaderViewModel? = null
+ var header: CredentialHeaderViewModel? = null
val job = launch {
launch { viewModel.validatedAttestation.toList(attestations) }
launch { viewModel.remainingAttempts.toList(remainingAttempts) }
@@ -147,7 +143,7 @@
val attestations = mutableListOf<ByteArray?>()
val remainingAttempts = mutableListOf<RemainingAttempts?>()
- var header: HeaderViewModel? = null
+ var header: CredentialHeaderViewModel? = null
val job = launch {
launch { viewModel.validatedAttestation.toList(attestations) }
launch { viewModel.remainingAttempts.toList(remainingAttempts) }
@@ -169,7 +165,7 @@
}
private fun runTestWithKind(
- kind: PromptKind = PromptKind.PIN,
+ kind: PromptKind = PromptKind.Pin,
init: () -> Unit = {},
block: suspend TestScope.() -> Unit,
) =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt
new file mode 100644
index 0000000..689bb00
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.systemui.biometrics.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.domain.model.BiometricModality
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class PromptAuthStateTest : SysuiTestCase() {
+
+ @Test
+ fun notAuthenticated() {
+ with(PromptAuthState(isAuthenticated = false)) {
+ assertThat(isNotAuthenticated).isTrue()
+ assertThat(isAuthenticatedAndConfirmed).isFalse()
+ assertThat(isAuthenticatedByFace).isFalse()
+ assertThat(isAuthenticatedByFingerprint).isFalse()
+ }
+ }
+
+ @Test
+ fun authenticatedByUnknown() {
+ with(PromptAuthState(isAuthenticated = true)) {
+ assertThat(isNotAuthenticated).isFalse()
+ assertThat(isAuthenticatedAndConfirmed).isTrue()
+ assertThat(isAuthenticatedByFace).isFalse()
+ assertThat(isAuthenticatedByFingerprint).isFalse()
+ }
+
+ with(PromptAuthState(isAuthenticated = true, needsUserConfirmation = true)) {
+ assertThat(isNotAuthenticated).isFalse()
+ assertThat(isAuthenticatedAndConfirmed).isFalse()
+ assertThat(isAuthenticatedByFace).isFalse()
+ assertThat(isAuthenticatedByFingerprint).isFalse()
+
+ assertThat(asConfirmed().isAuthenticatedAndConfirmed).isTrue()
+ }
+ }
+
+ @Test
+ fun authenticatedWithFace() {
+ with(
+ PromptAuthState(isAuthenticated = true, authenticatedModality = BiometricModality.Face)
+ ) {
+ assertThat(isNotAuthenticated).isFalse()
+ assertThat(isAuthenticatedAndConfirmed).isTrue()
+ assertThat(isAuthenticatedByFace).isTrue()
+ assertThat(isAuthenticatedByFingerprint).isFalse()
+ }
+ }
+
+ @Test
+ fun authenticatedWithFingerprint() {
+ with(
+ PromptAuthState(
+ isAuthenticated = true,
+ authenticatedModality = BiometricModality.Fingerprint,
+ )
+ ) {
+ assertThat(isNotAuthenticated).isFalse()
+ assertThat(isAuthenticatedAndConfirmed).isTrue()
+ assertThat(isAuthenticatedByFace).isFalse()
+ assertThat(isAuthenticatedByFingerprint).isTrue()
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
new file mode 100644
index 0000000..3ba6004
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -0,0 +1,639 @@
+/*
+ * 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.systemui.biometrics.ui.viewmodel
+
+import android.hardware.biometrics.PromptInfo
+import android.hardware.face.FaceSensorPropertiesInternal
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.AuthBiometricView
+import com.android.systemui.biometrics.data.repository.FakePromptRepository
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
+import com.android.systemui.biometrics.domain.model.BiometricModalities
+import com.android.systemui.biometrics.domain.model.BiometricModality
+import com.android.systemui.biometrics.extractAuthenticatorTypes
+import com.android.systemui.biometrics.faceSensorPropertiesInternal
+import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
+import com.android.systemui.coroutines.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+
+private const val USER_ID = 4
+private const val CHALLENGE = 2L
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(Parameterized::class)
+internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCase() {
+
+ @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var lockPatternUtils: LockPatternUtils
+
+ private val testScope = TestScope()
+ private val promptRepository = FakePromptRepository()
+
+ private lateinit var selector: PromptSelectorInteractor
+ private lateinit var viewModel: PromptViewModel
+
+ @Before
+ fun setup() {
+ selector = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils)
+ selector.resetPrompt()
+
+ viewModel = PromptViewModel(selector)
+ }
+
+ @Test
+ fun `start idle and show authenticating`() =
+ runGenericTest(doNotStart = true) {
+ val expectedSize =
+ if (testCase.shouldStartAsImplicitFlow) PromptSize.SMALL else PromptSize.MEDIUM
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+ val modalities by collectLastValue(viewModel.modalities)
+ val message by collectLastValue(viewModel.message)
+ val size by collectLastValue(viewModel.size)
+ val legacyState by collectLastValue(viewModel.legacyState)
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isNotAuthenticated).isTrue()
+ with(modalities ?: throw Exception("missing modalities")) {
+ assertThat(hasFace).isEqualTo(testCase.face != null)
+ assertThat(hasFingerprint).isEqualTo(testCase.fingerprint != null)
+ }
+ assertThat(message).isEqualTo(PromptMessage.Empty)
+ assertThat(size).isEqualTo(expectedSize)
+ assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN)
+
+ val startMessage = "here we go"
+ viewModel.showAuthenticating(startMessage, isRetry = false)
+
+ assertThat(message).isEqualTo(PromptMessage.Help(startMessage))
+ assertThat(authenticating).isTrue()
+ assertThat(authenticated?.isNotAuthenticated).isTrue()
+ assertThat(size).isEqualTo(expectedSize)
+ assertButtonsVisible(negative = expectedSize != PromptSize.SMALL)
+ assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATING)
+ }
+
+ @Test
+ fun `shows authenticated - no errors`() = runGenericTest {
+ // this case can't happen until fingerprint is started
+ // trigger it now since no error has occurred in this test
+ val forceError = testCase.isCoex && testCase.authenticatedByFingerprint
+
+ if (forceError) {
+ assertThat(viewModel.fingerprintStartMode.first())
+ .isEqualTo(FingerprintStartMode.Pending)
+ viewModel.ensureFingerprintHasStarted(isDelayed = true)
+ }
+
+ showAuthenticated(
+ testCase.authenticatedModality,
+ testCase.expectConfirmation(atLeastOneFailure = forceError),
+ )
+ }
+
+ private suspend fun TestScope.showAuthenticated(
+ authenticatedModality: BiometricModality,
+ expectConfirmation: Boolean,
+ ) {
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+ val fpStartMode by collectLastValue(viewModel.fingerprintStartMode)
+ val size by collectLastValue(viewModel.size)
+ val legacyState by collectLastValue(viewModel.legacyState)
+
+ val authWithSmallPrompt =
+ testCase.shouldStartAsImplicitFlow &&
+ (fpStartMode == FingerprintStartMode.Pending || testCase.isFaceOnly)
+ assertThat(authenticating).isTrue()
+ assertThat(authenticated?.isNotAuthenticated).isTrue()
+ assertThat(size).isEqualTo(if (authWithSmallPrompt) PromptSize.SMALL else PromptSize.MEDIUM)
+ assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATING)
+ assertButtonsVisible(negative = !authWithSmallPrompt)
+
+ val delay = 1000L
+ viewModel.showAuthenticated(authenticatedModality, delay)
+
+ assertThat(authenticated?.isAuthenticated).isTrue()
+ assertThat(authenticated?.delay).isEqualTo(delay)
+ assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
+ assertThat(size)
+ .isEqualTo(
+ if (authenticatedModality == BiometricModality.Fingerprint || expectConfirmation) {
+ PromptSize.MEDIUM
+ } else {
+ PromptSize.SMALL
+ }
+ )
+ assertThat(legacyState)
+ .isEqualTo(
+ if (expectConfirmation) {
+ AuthBiometricView.STATE_PENDING_CONFIRMATION
+ } else {
+ AuthBiometricView.STATE_AUTHENTICATED
+ }
+ )
+ assertButtonsVisible(
+ cancel = expectConfirmation,
+ confirm = expectConfirmation,
+ )
+ }
+
+ @Test
+ fun `shows temporary errors`() = runGenericTest {
+ val checkAtEnd = suspend { assertButtonsVisible(negative = true) }
+
+ showTemporaryErrors(restart = false) { checkAtEnd() }
+ showTemporaryErrors(restart = false, helpAfterError = "foo") { checkAtEnd() }
+ showTemporaryErrors(restart = true) { checkAtEnd() }
+ }
+
+ private suspend fun TestScope.showTemporaryErrors(
+ restart: Boolean,
+ helpAfterError: String = "",
+ block: suspend TestScope.() -> Unit = {},
+ ) {
+ val errorMessage = "oh no!"
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+ val message by collectLastValue(viewModel.message)
+ val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible)
+ val size by collectLastValue(viewModel.size)
+ val legacyState by collectLastValue(viewModel.legacyState)
+ val canTryAgainNow by collectLastValue(viewModel.canTryAgainNow)
+
+ val errorJob = launch {
+ viewModel.showTemporaryError(
+ errorMessage,
+ authenticateAfterError = restart,
+ messageAfterError = helpAfterError,
+ )
+ }
+
+ assertThat(size).isEqualTo(PromptSize.MEDIUM)
+ assertThat(message).isEqualTo(PromptMessage.Error(errorMessage))
+ assertThat(messageVisible).isTrue()
+ assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_ERROR)
+
+ // temporary error should disappear after a delay
+ errorJob.join()
+ if (helpAfterError.isNotBlank()) {
+ assertThat(message).isEqualTo(PromptMessage.Help(helpAfterError))
+ assertThat(messageVisible).isTrue()
+ } else {
+ assertThat(message).isEqualTo(PromptMessage.Empty)
+ assertThat(messageVisible).isFalse()
+ }
+ assertThat(legacyState)
+ .isEqualTo(
+ if (restart) {
+ AuthBiometricView.STATE_AUTHENTICATING
+ } else {
+ AuthBiometricView.STATE_HELP
+ }
+ )
+
+ assertThat(authenticating).isEqualTo(restart)
+ assertThat(authenticated?.isNotAuthenticated).isTrue()
+ assertThat(canTryAgainNow).isFalse()
+
+ block()
+ }
+
+ @Test
+ fun `no errors or temporary help after authenticated`() = runGenericTest {
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+ val message by collectLastValue(viewModel.message)
+ val messageIsShowing by collectLastValue(viewModel.isIndicatorMessageVisible)
+ val canTryAgain by collectLastValue(viewModel.canTryAgainNow)
+
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+ val verifyNoError = {
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+ assertThat(message).isEqualTo(PromptMessage.Empty)
+ assertThat(canTryAgain).isFalse()
+ }
+
+ val errorJob = launch { viewModel.showTemporaryError("error") }
+ verifyNoError()
+ errorJob.join()
+ verifyNoError()
+
+ val helpJob = launch { viewModel.showTemporaryHelp("hi") }
+ verifyNoError()
+ helpJob.join()
+ verifyNoError()
+
+ // persistent help is allowed
+ val stickyHelpMessage = "blah"
+ viewModel.showHelp(stickyHelpMessage)
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+ assertThat(message).isEqualTo(PromptMessage.Help(stickyHelpMessage))
+ assertThat(messageIsShowing).isTrue()
+ }
+
+ // @Test
+ fun `suppress errors`() = runGenericTest {
+ val errorMessage = "woot"
+ val message by collectLastValue(viewModel.message)
+
+ val errorJob = launch { viewModel.showTemporaryError(errorMessage) }
+ }
+
+ @Test
+ fun `authenticated at most once`() = runGenericTest {
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+ }
+
+ @Test
+ fun `authenticating cannot restart after authenticated`() = runGenericTest {
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+
+ viewModel.showAuthenticating("again!")
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+ }
+
+ @Test
+ fun `confirm authentication`() = runGenericTest {
+ val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
+
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+ val message by collectLastValue(viewModel.message)
+ val size by collectLastValue(viewModel.size)
+ val legacyState by collectLastValue(viewModel.legacyState)
+ val canTryAgain by collectLastValue(viewModel.canTryAgainNow)
+
+ assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
+ if (expectConfirmation) {
+ assertThat(size).isEqualTo(PromptSize.MEDIUM)
+ assertButtonsVisible(
+ cancel = true,
+ confirm = true,
+ )
+
+ viewModel.confirmAuthenticated()
+ assertThat(message).isEqualTo(PromptMessage.Empty)
+ assertButtonsVisible()
+ }
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+ assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATED)
+ assertThat(canTryAgain).isFalse()
+ }
+
+ @Test
+ fun `cannot confirm unless authenticated`() = runGenericTest {
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+
+ viewModel.confirmAuthenticated()
+ assertThat(authenticating).isTrue()
+ assertThat(authenticated?.isNotAuthenticated).isTrue()
+
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+ // reconfirm should be a no-op
+ viewModel.confirmAuthenticated()
+ viewModel.confirmAuthenticated()
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isNotAuthenticated).isFalse()
+ }
+
+ @Test
+ fun `shows help - before authenticated`() = runGenericTest {
+ val helpMessage = "please help yourself to some cookies"
+ val message by collectLastValue(viewModel.message)
+ val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible)
+ val size by collectLastValue(viewModel.size)
+ val legacyState by collectLastValue(viewModel.legacyState)
+
+ viewModel.showHelp(helpMessage)
+
+ assertThat(size).isEqualTo(PromptSize.MEDIUM)
+ assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_HELP)
+ assertThat(message).isEqualTo(PromptMessage.Help(helpMessage))
+ assertThat(messageVisible).isTrue()
+
+ assertThat(viewModel.isAuthenticating.first()).isFalse()
+ assertThat(viewModel.isAuthenticated.first().isNotAuthenticated).isTrue()
+ }
+
+ @Test
+ fun `shows help - after authenticated`() = runGenericTest {
+ val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
+ val helpMessage = "more cookies please"
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+ val message by collectLastValue(viewModel.message)
+ val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible)
+ val size by collectLastValue(viewModel.size)
+ val legacyState by collectLastValue(viewModel.legacyState)
+
+ if (testCase.isCoex && testCase.authenticatedByFingerprint) {
+ viewModel.ensureFingerprintHasStarted(isDelayed = true)
+ }
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+ viewModel.showHelp(helpMessage)
+
+ assertThat(size).isEqualTo(PromptSize.MEDIUM)
+ assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_PENDING_CONFIRMATION)
+ assertThat(message).isEqualTo(PromptMessage.Help(helpMessage))
+ assertThat(messageVisible).isTrue()
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+ assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
+ assertButtonsVisible(
+ cancel = expectConfirmation,
+ confirm = expectConfirmation,
+ )
+ }
+
+ @Test
+ fun `retries after failure`() = runGenericTest {
+ val errorMessage = "bad"
+ val helpMessage = "again?"
+ val expectTryAgainButton = testCase.isFaceOnly
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+ val message by collectLastValue(viewModel.message)
+ val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible)
+ val canTryAgain by collectLastValue(viewModel.canTryAgainNow)
+
+ viewModel.showAuthenticating("go")
+ val errorJob = launch {
+ viewModel.showTemporaryError(
+ errorMessage,
+ messageAfterError = helpMessage,
+ authenticateAfterError = false,
+ failedModality = testCase.authenticatedModality
+ )
+ }
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isFalse()
+ assertThat(message).isEqualTo(PromptMessage.Error(errorMessage))
+ assertThat(messageVisible).isTrue()
+ assertThat(canTryAgain).isEqualTo(testCase.authenticatedByFace)
+ assertButtonsVisible(negative = true, tryAgain = expectTryAgainButton)
+
+ errorJob.join()
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isFalse()
+ assertThat(message).isEqualTo(PromptMessage.Help(helpMessage))
+ assertThat(messageVisible).isTrue()
+ assertThat(canTryAgain).isEqualTo(testCase.authenticatedByFace)
+ assertButtonsVisible(negative = true, tryAgain = expectTryAgainButton)
+
+ val helpMessage2 = "foo"
+ viewModel.showAuthenticating(helpMessage2, isRetry = true)
+ assertThat(authenticating).isTrue()
+ assertThat(authenticated?.isAuthenticated).isFalse()
+ assertThat(message).isEqualTo(PromptMessage.Help(helpMessage2))
+ assertThat(messageVisible).isTrue()
+ assertButtonsVisible(negative = true)
+ }
+
+ @Test
+ fun `switch to credential fallback`() = runGenericTest {
+ val size by collectLastValue(viewModel.size)
+
+ // TODO(b/251476085): remove Spaghetti, migrate logic, and update this test
+ viewModel.onSwitchToCredential()
+
+ assertThat(size).isEqualTo(PromptSize.LARGE)
+ }
+
+ /** Asserts that the selected buttons are visible now. */
+ private suspend fun TestScope.assertButtonsVisible(
+ tryAgain: Boolean = false,
+ confirm: Boolean = false,
+ cancel: Boolean = false,
+ negative: Boolean = false,
+ credential: Boolean = false,
+ ) {
+ runCurrent()
+ assertThat(viewModel.isTryAgainButtonVisible.first()).isEqualTo(tryAgain)
+ assertThat(viewModel.isConfirmButtonVisible.first()).isEqualTo(confirm)
+ assertThat(viewModel.isCancelButtonVisible.first()).isEqualTo(cancel)
+ assertThat(viewModel.isNegativeButtonVisible.first()).isEqualTo(negative)
+ assertThat(viewModel.isCredentialButtonVisible.first()).isEqualTo(credential)
+ }
+
+ private fun runGenericTest(
+ doNotStart: Boolean = false,
+ allowCredentialFallback: Boolean = false,
+ block: suspend TestScope.() -> Unit
+ ) {
+ selector.initializePrompt(
+ requireConfirmation = testCase.confirmationRequested,
+ allowCredentialFallback = allowCredentialFallback,
+ fingerprint = testCase.fingerprint,
+ face = testCase.face,
+ )
+
+ // put the view model in the initial authenticating state, unless explicitly skipped
+ val startMode =
+ when {
+ doNotStart -> null
+ testCase.isCoex -> FingerprintStartMode.Delayed
+ else -> FingerprintStartMode.Normal
+ }
+ when (startMode) {
+ FingerprintStartMode.Normal -> {
+ viewModel.ensureFingerprintHasStarted(isDelayed = false)
+ viewModel.showAuthenticating()
+ }
+ FingerprintStartMode.Delayed -> {
+ viewModel.showAuthenticating()
+ }
+ else -> {
+ /* skip */
+ }
+ }
+
+ testScope.runTest { block() }
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data(): Collection<TestCase> = singleModalityTestCases + coexTestCases
+
+ private val singleModalityTestCases =
+ listOf(
+ TestCase(
+ face = faceSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Face,
+ ),
+ TestCase(
+ fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Fingerprint,
+ ),
+ TestCase(
+ face = faceSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Face,
+ confirmationRequested = true,
+ ),
+ TestCase(
+ fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Fingerprint,
+ confirmationRequested = true,
+ ),
+ )
+
+ private val coexTestCases =
+ listOf(
+ TestCase(
+ face = faceSensorPropertiesInternal(strong = true).first(),
+ fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Face,
+ ),
+ TestCase(
+ face = faceSensorPropertiesInternal(strong = true).first(),
+ fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Fingerprint,
+ ),
+ TestCase(
+ face = faceSensorPropertiesInternal(strong = true).first(),
+ fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Face,
+ confirmationRequested = true,
+ ),
+ TestCase(
+ face = faceSensorPropertiesInternal(strong = true).first(),
+ fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Fingerprint,
+ confirmationRequested = true,
+ ),
+ )
+ }
+}
+
+internal data class TestCase(
+ val fingerprint: FingerprintSensorPropertiesInternal? = null,
+ val face: FaceSensorPropertiesInternal? = null,
+ val authenticatedModality: BiometricModality,
+ val confirmationRequested: Boolean = false,
+) {
+ override fun toString(): String {
+ val modality =
+ when {
+ fingerprint != null && face != null -> "coex"
+ fingerprint != null -> "fingerprint only"
+ face != null -> "face only"
+ else -> "?"
+ }
+ return "[$modality, by: $authenticatedModality, confirm: $confirmationRequested]"
+ }
+
+ fun expectConfirmation(atLeastOneFailure: Boolean): Boolean =
+ when {
+ isCoex && authenticatedModality == BiometricModality.Face ->
+ atLeastOneFailure || confirmationRequested
+ isFaceOnly -> confirmationRequested
+ else -> false
+ }
+
+ val authenticatedByFingerprint: Boolean
+ get() = authenticatedModality == BiometricModality.Fingerprint
+
+ val authenticatedByFace: Boolean
+ get() = authenticatedModality == BiometricModality.Face
+
+ val isFaceOnly: Boolean
+ get() = face != null && fingerprint == null
+
+ val isFingerprintOnly: Boolean
+ get() = face == null && fingerprint != null
+
+ val isCoex: Boolean
+ get() = face != null && fingerprint != null
+
+ val shouldStartAsImplicitFlow: Boolean
+ get() = (isFaceOnly || isCoex) && !confirmationRequested
+}
+
+/** Initialize the test by selecting the give [fingerprint] or [face] configuration(s). */
+private fun PromptSelectorInteractor.initializePrompt(
+ fingerprint: FingerprintSensorPropertiesInternal? = null,
+ face: FaceSensorPropertiesInternal? = null,
+ requireConfirmation: Boolean = false,
+ allowCredentialFallback: Boolean = false,
+) {
+ val info =
+ PromptInfo().apply {
+ title = "t"
+ subtitle = "s"
+ authenticators = listOf(face, fingerprint).extractAuthenticatorTypes()
+ isDeviceCredentialAllowed = allowCredentialFallback
+ isConfirmationRequested = requireConfirmation
+ }
+ useBiometricsForAuthentication(
+ info,
+ requireConfirmation,
+ USER_ID,
+ CHALLENGE,
+ BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face),
+ )
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
index 367d206..548d26f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
@@ -1,6 +1,7 @@
package com.android.systemui.keyguard
import android.content.ComponentCallbacks2
+import android.graphics.HardwareRenderer
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -9,7 +10,11 @@
import com.android.systemui.keyguard.data.repository.FakeCommandQueue
import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.shared.model.WakeSleepReason
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.shared.model.WakefulnessState
@@ -25,6 +30,7 @@
import org.mockito.Mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
@@ -37,6 +43,7 @@
private val testScope = TestScope(testDispatcher)
private val keyguardRepository = FakeKeyguardRepository()
private val featureFlags = FakeFeatureFlags()
+ private val keyguardTransitionRepository = FakeKeyguardTransitionRepository()
@Mock private lateinit var globalWindowManager: GlobalWindowManager
private lateinit var resourceTrimmer: ResourceTrimmer
@@ -45,13 +52,15 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
featureFlags.set(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK, true)
+ featureFlags.set(Flags.TRIM_FONT_CACHES_AT_UNLOCK, true)
featureFlags.set(Flags.FACE_AUTH_REFACTOR, false)
keyguardRepository.setWakefulnessModel(
WakefulnessModel(WakefulnessState.AWAKE, WakeSleepReason.OTHER, WakeSleepReason.OTHER)
)
keyguardRepository.setDozeAmount(0f)
+ keyguardRepository.setKeyguardGoingAway(false)
- val interactor =
+ val keyguardInteractor =
KeyguardInteractor(
keyguardRepository,
FakeCommandQueue(),
@@ -60,7 +69,8 @@
)
resourceTrimmer =
ResourceTrimmer(
- interactor,
+ keyguardInteractor,
+ KeyguardTransitionInteractor(keyguardTransitionRepository),
globalWindowManager,
testScope.backgroundScope,
testDispatcher,
@@ -191,4 +201,26 @@
verifyZeroInteractions(globalWindowManager)
}
}
+
+ @Test
+ fun keyguardTransitionsToGone_trimsFontCache() =
+ testScope.runTest {
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+ )
+ verify(globalWindowManager, times(1))
+ .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
+ verify(globalWindowManager, times(1)).trimCaches(HardwareRenderer.CACHE_TRIM_FONT)
+ verifyNoMoreInteractions(globalWindowManager)
+ }
+
+ @Test
+ fun keyguardTransitionsToGone_flagDisabled_doesNotTrimFontCache() =
+ testScope.runTest {
+ featureFlags.set(Flags.TRIM_FONT_CACHES_AT_UNLOCK, false)
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+ )
+ verifyNoMoreInteractions(globalWindowManager)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index f4cd383..1643e17 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -28,7 +28,6 @@
import android.content.ComponentName;
import android.graphics.Rect;
-import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.IBiometricSysuiReceiver;
import android.hardware.biometrics.PromptInfo;
import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
@@ -443,15 +442,13 @@
final long operationId = 1;
final String packageName = "test";
final long requestId = 10;
- final int multiSensorConfig = BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
mCommandQueue.showAuthenticationDialog(promptInfo, receiver, sensorIds,
- credentialAllowed, requireConfirmation, userId, operationId, packageName, requestId,
- multiSensorConfig);
+ credentialAllowed, requireConfirmation, userId, operationId, packageName, requestId);
waitForIdleSync();
verify(mCallbacks).showAuthenticationDialog(eq(promptInfo), eq(receiver), eq(sensorIds),
eq(credentialAllowed), eq(requireConfirmation), eq(userId), eq(operationId),
- eq(packageName), eq(requestId), eq(multiSensorConfig));
+ eq(packageName), eq(requestId));
}
@Test
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
index 96658c6..d270700 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
@@ -1,7 +1,7 @@
package com.android.systemui.biometrics.data.repository
import android.hardware.biometrics.PromptInfo
-import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.biometrics.shared.model.PromptKind
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -20,26 +20,32 @@
private var _challenge = MutableStateFlow<Long?>(null)
override val challenge = _challenge.asStateFlow()
- private val _kind = MutableStateFlow(PromptKind.ANY_BIOMETRIC)
+ private val _kind = MutableStateFlow<PromptKind>(PromptKind.Biometric())
override val kind = _kind.asStateFlow()
+ private val _isConfirmationRequired = MutableStateFlow(false)
+ override val isConfirmationRequired = _isConfirmationRequired.asStateFlow()
+
override fun setPrompt(
promptInfo: PromptInfo,
userId: Int,
gatekeeperChallenge: Long?,
- kind: PromptKind
+ kind: PromptKind,
+ requireConfirmation: Boolean,
) {
_promptInfo.value = promptInfo
_userId.value = userId
_challenge.value = gatekeeperChallenge
_kind.value = kind
+ _isConfirmationRequired.value = requireConfirmation
}
override fun unsetPrompt() {
_promptInfo.value = null
_userId.value = null
_challenge.value = null
- _kind.value = PromptKind.ANY_BIOMETRIC
+ _kind.value = PromptKind.Biometric()
+ _isConfirmationRequired.value = false
}
fun setIsShowing(showing: Boolean) {
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java
index 4aeb4a4..cd6de87 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java
@@ -349,7 +349,8 @@
private int isFieldDetectionServiceEnabled(PrintWriter pw) {
final int userId = getNextIntArgRequired();
String name = mService.getFieldDetectionServiceName(userId);
- boolean enabled = !TextUtils.isEmpty(name);
+ boolean pccFlagEnabled = mService.isPccClassificationFlagEnabled();
+ boolean enabled = (!TextUtils.isEmpty(name)) && pccFlagEnabled;
pw.println(enabled);
return 0;
}
diff --git a/services/core/java/com/android/server/DockObserver.java b/services/core/java/com/android/server/DockObserver.java
index 5156c54..fb527c1 100644
--- a/services/core/java/com/android/server/DockObserver.java
+++ b/services/core/java/com/android/server/DockObserver.java
@@ -37,6 +37,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FrameworkStatsLog;
import com.android.server.ExtconUEventObserver.ExtconInfo;
import java.io.FileDescriptor;
@@ -195,6 +196,8 @@
@Override
public void onStart() {
publishBinderService(TAG, new BinderService());
+ // Logs dock state after setDockStateFromProviderLocked sets mReportedDockState
+ FrameworkStatsLog.write(FrameworkStatsLog.DOCK_STATE_CHANGED, mReportedDockState);
}
@Override
@@ -256,7 +259,6 @@
+ mReportedDockState);
final int previousDockState = mPreviousDockState;
mPreviousDockState = mReportedDockState;
-
// Skip the dock intent if not yet provisioned.
final ContentResolver cr = getContext().getContentResolver();
if (!mDeviceProvisionedObserver.isDeviceProvisioned()) {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index f4c9d05..d7a5ee9 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -420,6 +420,20 @@
mBtHelper.stopBluetoothSco(eventSource);
}
+ // In BT classic for communication, the device changes from a2dp to sco device, but for
+ // LE Audio it stays the same and we must trigger the proper stream volume alignment, if
+ // LE Audio communication device is activated after the audio system has already switched to
+ // MODE_IN_CALL mode.
+ if (isBluetoothLeAudioRequested()) {
+ final int streamType = mAudioService.getBluetoothContextualVolumeStream();
+ final int leAudioVolIndex = getVssVolumeForDevice(streamType, device.getInternalType());
+ final int leAudioMaxVolIndex = getMaxVssVolumeForStream(streamType);
+ if (AudioService.DEBUG_COMM_RTE) {
+ Log.v(TAG, "setCommunicationRouteForClient restoring LE Audio device volume lvl.");
+ }
+ postSetLeAudioVolumeIndex(leAudioVolIndex, leAudioMaxVolIndex, streamType);
+ }
+
updateCommunicationRoute(eventSource);
}
@@ -633,6 +647,16 @@
}
/**
+ * Helper method on top of isDeviceRequestedForCommunication() indicating if
+ * Bluetooth LE Audio communication device is currently requested or not.
+ * @return true if Bluetooth LE Audio device is requested, false otherwise.
+ */
+ /*package*/ boolean isBluetoothLeAudioRequested() {
+ return isDeviceRequestedForCommunication(AudioDeviceInfo.TYPE_BLE_HEADSET)
+ || isDeviceRequestedForCommunication(AudioDeviceInfo.TYPE_BLE_SPEAKER);
+ }
+
+ /**
* Indicates if preferred route selection for communication is Bluetooth SCO.
* @return true if Bluetooth SCO is preferred , false otherwise.
*/
diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java
index bf5e8ee..1989bc7 100644
--- a/services/core/java/com/android/server/biometrics/AuthSession.java
+++ b/services/core/java/com/android/server/biometrics/AuthSession.java
@@ -21,8 +21,6 @@
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR_BASE;
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED;
@@ -44,7 +42,6 @@
import android.hardware.biometrics.BiometricAuthenticator.Modality;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricManager;
-import android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.biometrics.IBiometricSensorReceiver;
@@ -68,7 +65,6 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.Collection;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
@@ -134,7 +130,6 @@
// The current state, which can be either idle, called, or started
private @SessionState int mState = STATE_AUTH_IDLE;
- private @BiometricMultiSensorMode int mMultiSensorMode;
private int[] mSensors;
// TODO(b/197265902): merge into state
private boolean mCancelled;
@@ -255,7 +250,6 @@
// SystemUI invokes that path.
mState = STATE_SHOWING_DEVICE_CREDENTIAL;
mSensors = new int[0];
- mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT;
mStatusBarService.showAuthenticationDialog(
mPromptInfo,
@@ -266,8 +260,7 @@
mUserId,
mOperationId,
mOpPackageName,
- mRequestId,
- mMultiSensorMode);
+ mRequestId);
} else if (!mPreAuthInfo.eligibleSensors.isEmpty()) {
// Some combination of biometric or biometric|credential is requested
setSensorsToStateWaitingForCookie(false /* isTryAgain */);
@@ -310,8 +303,6 @@
for (int i = 0; i < mPreAuthInfo.eligibleSensors.size(); i++) {
mSensors[i] = mPreAuthInfo.eligibleSensors.get(i).id;
}
- mMultiSensorMode = getMultiSensorModeForNewSession(
- mPreAuthInfo.eligibleSensors);
mStatusBarService.showAuthenticationDialog(mPromptInfo,
mSysuiReceiver,
@@ -321,8 +312,7 @@
mUserId,
mOperationId,
mOpPackageName,
- mRequestId,
- mMultiSensorMode);
+ mRequestId);
mState = STATE_AUTH_STARTED;
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception", e);
@@ -438,7 +428,6 @@
mPromptInfo.setAuthenticators(authenticators);
mState = STATE_SHOWING_DEVICE_CREDENTIAL;
- mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT;
mSensors = new int[0];
mStatusBarService.showAuthenticationDialog(
@@ -450,8 +439,7 @@
mUserId,
mOperationId,
mOpPackageName,
- mRequestId,
- mMultiSensorMode);
+ mRequestId);
} else {
mClientReceiver.onError(modality, error, vendorCode);
return true;
@@ -545,13 +533,30 @@
}
}
- void onDialogAnimatedIn() {
+ void onDialogAnimatedIn(boolean startFingerprintNow) {
if (mState != STATE_AUTH_STARTED) {
Slog.e(TAG, "onDialogAnimatedIn, unexpected state: " + mState);
return;
}
mState = STATE_AUTH_STARTED_UI_SHOWING;
+ if (startFingerprintNow) {
+ startAllPreparedFingerprintSensors();
+ } else {
+ Slog.d(TAG, "delaying fingerprint sensor start");
+ }
+ }
+
+ // call once anytime after onDialogAnimatedIn() to indicate it's appropriate to start the
+ // fingerprint sensor (i.e. face auth has failed or is not available)
+ void onStartFingerprint() {
+ if (mState != STATE_AUTH_STARTED
+ && mState != STATE_AUTH_STARTED_UI_SHOWING
+ && mState != STATE_AUTH_PAUSED
+ && mState != STATE_ERROR_PENDING_SYSUI) {
+ Slog.w(TAG, "onStartFingerprint, started from unexpected state: " + mState);
+ }
+
startAllPreparedFingerprintSensors();
}
@@ -919,25 +924,6 @@
}
}
- @BiometricMultiSensorMode
- private static int getMultiSensorModeForNewSession(Collection<BiometricSensor> sensors) {
- boolean hasFace = false;
- boolean hasFingerprint = false;
-
- for (BiometricSensor sensor: sensors) {
- if (sensor.modality == TYPE_FACE) {
- hasFace = true;
- } else if (sensor.modality == TYPE_FINGERPRINT) {
- hasFingerprint = true;
- }
- }
-
- if (hasFace && hasFingerprint) {
- return BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
- }
- return BIOMETRIC_MULTI_SENSOR_DEFAULT;
- }
-
@Override
public String toString() {
return "State: " + mState
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 4488434..0942d85 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -480,8 +480,13 @@
}
@Override
- public void onDialogAnimatedIn() {
- mHandler.post(() -> handleOnDialogAnimatedIn(requestId));
+ public void onDialogAnimatedIn(boolean startFingerprintNow) {
+ mHandler.post(() -> handleOnDialogAnimatedIn(requestId, startFingerprintNow));
+ }
+
+ @Override
+ public void onStartFingerprintNow() {
+ mHandler.post(() -> handleOnStartFingerprintNow(requestId));
}
};
}
@@ -1237,7 +1242,7 @@
}
}
- private void handleOnDialogAnimatedIn(long requestId) {
+ private void handleOnDialogAnimatedIn(long requestId, boolean startFingerprintNow) {
Slog.d(TAG, "handleOnDialogAnimatedIn");
final AuthSession session = getAuthSessionIfCurrent(requestId);
@@ -1246,7 +1251,19 @@
return;
}
- session.onDialogAnimatedIn();
+ session.onDialogAnimatedIn(startFingerprintNow);
+ }
+
+ private void handleOnStartFingerprintNow(long requestId) {
+ Slog.d(TAG, "handleOnStartFingerprintNow");
+
+ final AuthSession session = getAuthSessionIfCurrent(requestId);
+ if (session == null) {
+ Slog.w(TAG, "handleOnStartFingerprintNow: AuthSession is not current");
+ return;
+ }
+
+ session.onStartFingerprint();
}
/**
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
index 6a01042..42b2682 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
@@ -16,6 +16,8 @@
package com.android.server.broadcastradio;
+import android.Manifest;
+import android.content.pm.PackageManager;
import android.hardware.broadcastradio.IBroadcastRadio;
import android.hardware.radio.IAnnouncementListener;
import android.hardware.radio.ICloseHandle;
@@ -23,6 +25,7 @@
import android.hardware.radio.ITuner;
import android.hardware.radio.ITunerCallback;
import android.hardware.radio.RadioManager;
+import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -112,6 +115,13 @@
@Override
protected void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
+ if (mService.getContext().checkCallingOrSelfPermission(Manifest.permission.DUMP)
+ != PackageManager.PERMISSION_GRANTED) {
+ printWriter.println("Permission Denial: can't dump AIDL BroadcastRadioService from "
+ + "from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+ + " without permission " + Manifest.permission.DUMP);
+ return;
+ }
IndentingPrintWriter radioPrintWriter = new IndentingPrintWriter(printWriter);
radioPrintWriter.printf("BroadcastRadioService\n");
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
index 408fba1..bc72a4b 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
@@ -16,12 +16,15 @@
package com.android.server.broadcastradio;
+import android.Manifest;
+import android.content.pm.PackageManager;
import android.hardware.radio.IAnnouncementListener;
import android.hardware.radio.ICloseHandle;
import android.hardware.radio.IRadioService;
import android.hardware.radio.ITuner;
import android.hardware.radio.ITunerCallback;
import android.hardware.radio.RadioManager;
+import android.os.Binder;
import android.os.RemoteException;
import android.util.IndentingPrintWriter;
import android.util.Log;
@@ -129,6 +132,13 @@
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (mService.getContext().checkCallingOrSelfPermission(Manifest.permission.DUMP)
+ != PackageManager.PERMISSION_GRANTED) {
+ pw.println("Permission Denial: can't dump HIDL BroadcastRadioService from "
+ + "from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+ + " without permission " + Manifest.permission.DUMP);
+ return;
+ }
IndentingPrintWriter radioPw = new IndentingPrintWriter(pw);
radioPw.printf("BroadcastRadioService\n");
diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java
index 3e31bd1..a339756 100644
--- a/services/core/java/com/android/server/content/SyncManager.java
+++ b/services/core/java/com/android/server/content/SyncManager.java
@@ -104,7 +104,6 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
-import com.android.internal.config.appcloning.AppCloningDeviceConfigHelper;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.os.BackgroundThread;
@@ -257,8 +256,6 @@
private final SyncLogger mLogger;
- private final AppCloningDeviceConfigHelper mAppCloningDeviceConfigHelper;
-
private boolean isJobIdInUseLockedH(int jobId, List<JobInfo> pendingJobs) {
for (int i = 0, size = pendingJobs.size(); i < size; i++) {
JobInfo job = pendingJobs.get(i);
@@ -684,7 +681,6 @@
}, mSyncHandler);
mConstants = new SyncManagerConstants(context);
- mAppCloningDeviceConfigHelper = AppCloningDeviceConfigHelper.getInstance(context);
IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
context.registerReceiver(mConnectivityIntentReceiver, intentFilter);
@@ -892,8 +888,7 @@
* @return true/false if contact sharing is enabled/disabled
*/
protected boolean isContactSharingAllowedForCloneProfile() {
- return mContext.getResources().getBoolean(R.bool.config_enableAppCloningBuildingBlocks)
- && mAppCloningDeviceConfigHelper.getEnableAppCloningBuildingBlocks();
+ return mContext.getResources().getBoolean(R.bool.config_enableAppCloningBuildingBlocks);
}
/**
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index 8b8c5f6..65e7a00 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -152,9 +152,6 @@
// The security strength of the synthetic password, in bytes
private static final int SYNTHETIC_PASSWORD_SECURITY_STRENGTH = 256 / 8;
- public static final short PASSWORD_DATA_V1 = 1;
- public static final short PASSWORD_DATA_V2 = 2;
-
private static final int PASSWORD_SCRYPT_LOG_N = 11;
private static final int PASSWORD_SCRYPT_LOG_R = 3;
private static final int PASSWORD_SCRYPT_LOG_P = 1;
@@ -379,21 +376,18 @@
buffer.put(data, 0, data.length);
buffer.flip();
- /*
- * Originally this file did not contain a version number. However, its first field was
- * 'credentialType' as an 'int'. Since 'credentialType' could only be in the range
- * [-1, 4] and this file uses big endian byte order, the first two bytes were redundant,
- * and when interpreted as a 'short' could only contain -1 or 0. Therefore, we've now
- * reclaimed these two bytes for a 'short' version number and shrunk 'credentialType'
- * to a 'short'.
- */
- short version = buffer.getShort();
- if (version == ((short) 0) || version == (short) -1) {
- version = PASSWORD_DATA_V1;
- } else if (version != PASSWORD_DATA_V2) {
- throw new IllegalArgumentException("Unknown PasswordData version: " + version);
- }
- result.credentialType = buffer.getShort();
+ /*
+ * The serialized PasswordData is supposed to begin with credentialType as an int.
+ * However, all credentialType values fit in a short and the byte order is big endian,
+ * so the first two bytes don't convey any non-redundant information. For this reason,
+ * temporarily during development of Android 14, the first two bytes were "stolen" from
+ * credentialType to use for a data format version number.
+ *
+ * However, this change was reverted as it was a non-forwards-compatible change. (See
+ * toBytes() for why this data format needs to be forwards-compatible.) Therefore,
+ * recover from this misstep by ignoring the first two bytes.
+ */
+ result.credentialType = (short) buffer.getInt();
result.scryptLogN = buffer.get();
result.scryptLogR = buffer.get();
result.scryptLogP = buffer.get();
@@ -407,7 +401,7 @@
} else {
result.passwordHandle = null;
}
- if (version == PASSWORD_DATA_V2) {
+ if (buffer.remaining() >= Integer.BYTES) {
result.pinLength = buffer.getInt();
} else {
result.pinLength = PIN_LENGTH_UNAVAILABLE;
@@ -415,16 +409,25 @@
return result;
}
+ /**
+ * Serializes this PasswordData into a byte array.
+ * <p>
+ * Careful: all changes to the format of the serialized PasswordData must be forwards
+ * compatible. I.e., older versions of Android must still accept the latest PasswordData.
+ * This is because a serialized PasswordData is stored in the Factory Reset Protection (FRP)
+ * persistent data block. It's possible that a device has FRP set up on a newer version of
+ * Android, is factory reset, and then is set up with an older version of Android.
+ */
public byte[] toBytes() {
- ByteBuffer buffer = ByteBuffer.allocate(2 * Short.BYTES + 3 * Byte.BYTES
+ ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + 3 * Byte.BYTES
+ Integer.BYTES + salt.length + Integer.BYTES +
(passwordHandle != null ? passwordHandle.length : 0) + Integer.BYTES);
+ // credentialType must fit in a short. For an explanation, see fromBytes().
if (credentialType < Short.MIN_VALUE || credentialType > Short.MAX_VALUE) {
throw new IllegalArgumentException("Unknown credential type: " + credentialType);
}
- buffer.putShort(PASSWORD_DATA_V2);
- buffer.putShort((short) credentialType);
+ buffer.putInt(credentialType);
buffer.put(scryptLogN);
buffer.put(scryptLogR);
buffer.put(scryptLogP);
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index a97126c..5324acd 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -72,6 +72,7 @@
import android.view.ContentRecordingSession;
import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
@@ -111,7 +112,11 @@
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
static final long MEDIA_PROJECTION_PREVENTS_REUSING_CONSENT = 266201607L; // buganizer id
- private final Object mLock = new Object(); // Protects the list of media projections
+ // Protects access to state at service level & IMediaProjection level.
+ // Invocation order while holding locks must follow below to avoid deadlock:
+ // WindowManagerService -> MediaProjectionManagerService -> DisplayManagerService
+ // See mediaprojection.md
+ private final Object mLock = new Object();
private final Map<IBinder, IBinder.DeathRecipient> mDeathEaters;
private final CallbackDelegate mCallbackDelegate;
@@ -127,7 +132,9 @@
private final MediaRouterCallback mMediaRouterCallback;
private MediaRouter.RouteInfo mMediaRouteInfo;
+ @GuardedBy("mLock")
private IBinder mProjectionToken;
+ @GuardedBy("mLock")
private MediaProjection mProjectionGrant;
public MediaProjectionManagerService(Context context) {
@@ -314,9 +321,11 @@
*/
@VisibleForTesting
boolean setContentRecordingSession(@Nullable ContentRecordingSession incomingSession) {
+ // NEVER lock while calling into WindowManagerService, since WindowManagerService is
+ // ALWAYS locked when it invokes MediaProjectionManagerService.
+ final boolean setSessionSucceeded = mWmInternal.setContentRecordingSession(incomingSession);
synchronized (mLock) {
- if (!mWmInternal.setContentRecordingSession(
- incomingSession)) {
+ if (!setSessionSucceeded) {
// Unable to start mirroring, so tear down this projection.
if (mProjectionGrant != null) {
mProjectionGrant.stop();
@@ -359,13 +368,20 @@
*/
@VisibleForTesting
void requestConsentForInvalidProjection() {
+ Intent reviewConsentIntent;
+ int uid;
synchronized (mLock) {
- Slog.v(TAG, "Reusing token: Reshow dialog for due to invalid projection.");
- // Trigger the permission dialog again in SysUI
- // Do not handle the result; SysUI will update us when the user has consented.
- mContext.startActivityAsUser(buildReviewGrantedConsentIntent(),
- UserHandle.getUserHandleForUid(mProjectionGrant.uid));
+ reviewConsentIntent = buildReviewGrantedConsentIntentLocked();
+ uid = mProjectionGrant.uid;
}
+ // NEVER lock while calling into a method that eventually acquires the WindowManagerService
+ // lock, since WindowManagerService is ALWAYS locked when it invokes
+ // MediaProjectionManagerService.
+ Slog.v(TAG, "Reusing token: Reshow dialog for due to invalid projection.");
+ // Trigger the permission dialog again in SysUI
+ // Do not handle the result; SysUI will update us when the user has consented.
+ mContext.startActivityAsUser(reviewConsentIntent,
+ UserHandle.getUserHandleForUid(uid));
}
/**
@@ -375,7 +391,7 @@
* <p>Consent dialog result handled in
* {@link BinderService#setUserReviewGrantedConsentResult(int)}.
*/
- private Intent buildReviewGrantedConsentIntent() {
+ private Intent buildReviewGrantedConsentIntentLocked() {
final String permissionDialogString = mContext.getResources().getString(
R.string.config_mediaProjectionPermissionDialogComponent);
final ComponentName mediaProjectionPermissionDialogComponent =
@@ -388,7 +404,8 @@
}
/**
- * Handles result of dialog shown from {@link BinderService#buildReviewGrantedConsentIntent()}.
+ * Handles result of dialog shown from
+ * {@link BinderService#buildReviewGrantedConsentIntentLocked()}.
*
* <p>Tears down session if user did not consent, or starts mirroring if user did consent.
*/
@@ -490,23 +507,26 @@
MediaProjection getProjectionInternal(int uid, String packageName) {
final long callingToken = Binder.clearCallingIdentity();
try {
- // Supposedly the package has re-used the user's consent; confirm the provided details
- // against the current projection token before re-using the current projection.
- if (mProjectionGrant == null || mProjectionGrant.mSession == null
- || !mProjectionGrant.mSession.isWaitingForConsent()) {
- Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
- + "instance");
- return null;
- }
+ synchronized (mLock) {
+ // Supposedly the package has re-used the user's consent; confirm the provided
+ // details against the current projection token before re-using the current
+ // projection.
+ if (mProjectionGrant == null || mProjectionGrant.mSession == null
+ || !mProjectionGrant.mSession.isWaitingForConsent()) {
+ Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
+ + "instance");
+ return null;
+ }
// The package matches, go ahead and re-use the token for this request.
- if (mProjectionGrant.uid == uid
- && Objects.equals(mProjectionGrant.packageName, packageName)) {
- Slog.v(TAG, "Reusing token: getProjection can reuse the current projection");
- return mProjectionGrant;
- } else {
- Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
- + "instance due to package details mismatching");
- return null;
+ if (mProjectionGrant.uid == uid
+ && Objects.equals(mProjectionGrant.packageName, packageName)) {
+ Slog.v(TAG, "Reusing token: getProjection can reuse the current projection");
+ return mProjectionGrant;
+ } else {
+ Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
+ + "instance due to package details mismatching");
+ return null;
+ }
}
} finally {
Binder.restoreCallingIdentity(callingToken);
@@ -626,8 +646,10 @@
}
final long token = Binder.clearCallingIdentity();
try {
- if (mProjectionGrant != null) {
- mProjectionGrant.stop();
+ synchronized (mLock) {
+ if (mProjectionGrant != null) {
+ mProjectionGrant.stop();
+ }
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -641,13 +663,17 @@
throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify "
+ "on captured content resize");
}
- if (!isCurrentProjection(mProjectionGrant)) {
- return;
+ synchronized (mLock) {
+ if (!isCurrentProjection(mProjectionGrant)) {
+ return;
+ }
}
final long token = Binder.clearCallingIdentity();
try {
- if (mProjectionGrant != null && mCallbackDelegate != null) {
- mCallbackDelegate.dispatchResize(mProjectionGrant, width, height);
+ synchronized (mLock) {
+ if (mProjectionGrant != null && mCallbackDelegate != null) {
+ mCallbackDelegate.dispatchResize(mProjectionGrant, width, height);
+ }
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -661,13 +687,17 @@
throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify "
+ "on captured content visibility changed");
}
- if (!isCurrentProjection(mProjectionGrant)) {
- return;
+ synchronized (mLock) {
+ if (!isCurrentProjection(mProjectionGrant)) {
+ return;
+ }
}
final long token = Binder.clearCallingIdentity();
try {
- if (mProjectionGrant != null && mCallbackDelegate != null) {
- mCallbackDelegate.dispatchVisibilityChanged(mProjectionGrant, isVisible);
+ synchronized (mLock) {
+ if (mProjectionGrant != null && mCallbackDelegate != null) {
+ mCallbackDelegate.dispatchVisibilityChanged(mProjectionGrant, isVisible);
+ }
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -712,9 +742,11 @@
throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to set session "
+ "details.");
}
- if (!isCurrentProjection(projection)) {
- throw new SecurityException("Unable to set ContentRecordingSession on "
- + "non-current MediaProjection");
+ synchronized (mLock) {
+ if (!isCurrentProjection(projection)) {
+ throw new SecurityException("Unable to set ContentRecordingSession on "
+ + "non-current MediaProjection");
+ }
}
final long origId = Binder.clearCallingIdentity();
try {
@@ -732,10 +764,12 @@
throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to check if the given"
+ "projection is valid.");
}
- if (!isCurrentProjection(projection)) {
- Slog.v(TAG, "Reusing token: Won't request consent again for a token that "
- + "isn't current");
- return;
+ synchronized (mLock) {
+ if (!isCurrentProjection(projection)) {
+ Slog.v(TAG, "Reusing token: Won't request consent again for a token that "
+ + "isn't current");
+ return;
+ }
}
// Remove calling app identity before performing any privileged operations.
diff --git a/services/core/java/com/android/server/media/projection/mediaprojection.md b/services/core/java/com/android/server/media/projection/mediaprojection.md
new file mode 100644
index 0000000..bccdf34
--- /dev/null
+++ b/services/core/java/com/android/server/media/projection/mediaprojection.md
@@ -0,0 +1,30 @@
+# MediaProjection
+
+## Locking model
+`MediaProjectionManagerService` needs to have consistent lock ordering with its interactions with
+`WindowManagerService` to prevent deadlock.
+
+### TLDR
+`MediaProjectionManagerService` must lock when updating its own fields.
+
+Calls must follow the below invocation order while holding locks:
+
+`WindowManagerService -> MediaProjectionManagerService -> DisplayManagerService`
+
+### Justification
+
+`MediaProjectionManagerService` calls into `WindowManagerService` in the below cases. While handling
+each invocation, `WindowManagerService` acquires its own lock:
+* setting a `ContentRecordingSession`
+ * starting a new `MediaProjection` recording session through
+`MediaProjection#createVirtualDisplay`
+ * indicating the user has granted consent to reuse the consent token
+
+`WindowManagerService` calls into `MediaProjectionManagerService`, always while holding
+`WindowManagerGlobalLock`:
+* `ContentRecorder` handling various events such as resizing recorded content
+
+
+Since `WindowManagerService -> MediaProjectionManagerService` is guaranteed to always hold the
+`WindowManagerService` lock, we must ensure that `MediaProjectionManagerService ->
+WindowManagerService` is NEVER holding the `MediaProjectionManagerService` lock.
diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
index b5c0417..e149b04 100644
--- a/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
+++ b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
@@ -37,7 +37,6 @@
import android.util.Slog;
import android.util.SparseArray;
-import com.android.internal.config.appcloning.AppCloningDeviceConfigHelper;
import com.android.server.LocalServices;
import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
@@ -62,8 +61,6 @@
private final Context mContext;
private final UserManagerInternal mUserManagerInternal;
- private AppCloningDeviceConfigHelper mAppCloningDeviceConfigHelper;
-
public CrossProfileIntentResolverEngine(UserManagerService userManager,
DomainVerificationManagerInternal domainVerificationManager,
DefaultAppProvider defaultAppProvider, Context context) {
@@ -253,12 +250,7 @@
* We would return NoFilteringResolver only if it is allowed(feature flag is set).
*/
if (shouldUseNoFilteringResolver(sourceUserId, targetUserId)) {
- if (mAppCloningDeviceConfigHelper == null) {
- //lazy initialization of helper till required, to improve performance.
- mAppCloningDeviceConfigHelper = AppCloningDeviceConfigHelper.getInstance(mContext);
- }
- if (NoFilteringResolver.isIntentRedirectionAllowed(mContext,
- mAppCloningDeviceConfigHelper, resolveForStart, flags)) {
+ if (NoFilteringResolver.isIntentRedirectionAllowed(mContext, resolveForStart, flags)) {
return new NoFilteringResolver(computer.getComponentResolver(),
mUserManager);
} else {
diff --git a/services/core/java/com/android/server/pm/NoFilteringResolver.java b/services/core/java/com/android/server/pm/NoFilteringResolver.java
index b87256d..817e1f6 100644
--- a/services/core/java/com/android/server/pm/NoFilteringResolver.java
+++ b/services/core/java/com/android/server/pm/NoFilteringResolver.java
@@ -24,7 +24,6 @@
import android.os.Binder;
import com.android.internal.R;
-import com.android.internal.config.appcloning.AppCloningDeviceConfigHelper;
import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.resolution.ComponentResolverApi;
import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
@@ -57,10 +56,9 @@
* (PackageManager.MATCH_CLONE_PROFILE) bit set.
* @return true if resolver would be used for cross profile resolution.
*/
- public static boolean isIntentRedirectionAllowed(Context context,
- AppCloningDeviceConfigHelper appCloningDeviceConfigHelper, boolean resolveForStart,
+ public static boolean isIntentRedirectionAllowed(Context context, boolean resolveForStart,
long flags) {
- return isAppCloningBuildingBlocksEnabled(context, appCloningDeviceConfigHelper)
+ return isAppCloningBuildingBlocksEnabled(context)
&& (resolveForStart || (((flags & PackageManager.MATCH_CLONE_PROFILE) != 0)
&& hasPermission(context, Manifest.permission.QUERY_CLONED_APPS)));
}
@@ -142,14 +140,12 @@
}
/**
- * Checks if the AppCloningBuildingBlocks flag is enabled.
+ * Checks if the AppCloningBuildingBlocks config is enabled.
*/
- private static boolean isAppCloningBuildingBlocksEnabled(Context context,
- AppCloningDeviceConfigHelper appCloningDeviceConfigHelper) {
+ private static boolean isAppCloningBuildingBlocksEnabled(Context context) {
final long token = Binder.clearCallingIdentity();
try {
- return context.getResources().getBoolean(R.bool.config_enableAppCloningBuildingBlocks)
- && appCloningDeviceConfigHelper.getEnableAppCloningBuildingBlocks();
+ return context.getResources().getBoolean(R.bool.config_enableAppCloningBuildingBlocks);
} finally {
Binder.restoreCallingIdentity(token);
}
diff --git a/services/core/java/com/android/server/policy/AppOpsPolicy.java b/services/core/java/com/android/server/policy/AppOpsPolicy.java
index 7a5664f..5288e85 100644
--- a/services/core/java/com/android/server/policy/AppOpsPolicy.java
+++ b/services/core/java/com/android/server/policy/AppOpsPolicy.java
@@ -37,6 +37,7 @@
import android.os.IBinder;
import android.os.PackageTagsList;
import android.os.Process;
+import android.os.SystemProperties;
import android.os.UserHandle;
import android.service.voice.VoiceInteractionManagerInternal;
import android.service.voice.VoiceInteractionManagerInternal.HotwordDetectionServiceIdentity;
@@ -68,6 +69,8 @@
private static final String ACTIVITY_RECOGNITION_TAGS =
"android:activity_recognition_allow_listed_tags";
private static final String ACTIVITY_RECOGNITION_TAGS_SEPARATOR = ";";
+ private static final boolean SYSPROP_HOTWORD_DETECTION_SERVICE_REQUIRED =
+ SystemProperties.getBoolean("ro.hotword.detection_service_required", false);
@NonNull
private final Object mLock = new Object();
@@ -199,10 +202,16 @@
}
}
- private static boolean isHotwordDetectionServiceRequired(PackageManager pm) {
+ /**
+ * @hide
+ */
+ public static boolean isHotwordDetectionServiceRequired(PackageManager pm) {
// The HotwordDetectionService APIs aren't ready yet for Auto or TV.
- return !(pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
- || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK));
+ if (pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
+ || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
+ return false;
+ }
+ return SYSPROP_HOTWORD_DETECTION_SERVICE_REQUIRED;
}
@Override
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 363d2fd..044d30b 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -53,7 +53,6 @@
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Icon;
import android.hardware.biometrics.BiometricAuthenticator.Modality;
-import android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
import android.hardware.biometrics.IBiometricContextListener;
import android.hardware.biometrics.IBiometricSysuiReceiver;
import android.hardware.biometrics.PromptInfo;
@@ -949,14 +948,12 @@
@Override
public void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver,
int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation,
- int userId, long operationId, String opPackageName, long requestId,
- @BiometricMultiSensorMode int multiSensorConfig) {
+ int userId, long operationId, String opPackageName, long requestId) {
enforceBiometricDialog();
if (mBar != null) {
try {
mBar.showAuthenticationDialog(promptInfo, receiver, sensorIds, credentialAllowed,
- requireConfirmation, userId, operationId, opPackageName, requestId,
- multiSensorConfig);
+ requireConfirmation, userId, operationId, opPackageName, requestId);
} catch (RemoteException ex) {
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d84c013..3db0315 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -3570,6 +3570,14 @@
// Tell window manager to prepare for this one to be removed.
setVisibility(false);
+ // Propagate the last IME visibility in the same task, so the IME can show
+ // automatically if the next activity has a focused editable view.
+ if (mLastImeShown && mTransitionController.isShellTransitionsEnabled()) {
+ final ActivityRecord nextRunning = task.topRunningActivity();
+ if (nextRunning != null) {
+ nextRunning.mLastImeShown = true;
+ }
+ }
if (getTaskFragment().getPausingActivity() == null) {
ProtoLog.v(WM_DEBUG_STATES, "Finish needs to pause: %s", this);
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 322c11a..e33c6f0 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9254,7 +9254,6 @@
boolean shouldRestoreImeVisibility(IBinder imeTargetWindowToken) {
final Task imeTargetWindowTask;
- boolean hadRequestedShowIme = false;
synchronized (mGlobalLock) {
final WindowState imeTargetWindow = mWindowMap.get(imeTargetWindowToken);
if (imeTargetWindow == null) {
@@ -9264,14 +9263,15 @@
if (imeTargetWindowTask == null) {
return false;
}
- if (imeTargetWindow.mActivityRecord != null) {
- hadRequestedShowIme = imeTargetWindow.mActivityRecord.mLastImeShown;
+ if (imeTargetWindow.mActivityRecord != null
+ && imeTargetWindow.mActivityRecord.mLastImeShown) {
+ return true;
}
}
final TaskSnapshot snapshot = getTaskSnapshot(imeTargetWindowTask.mTaskId,
imeTargetWindowTask.mUserId, false /* isLowResolution */,
false /* restoreFromDisk */);
- return snapshot != null && snapshot.hasImeSurface() || hadRequestedShowIme;
+ return snapshot != null && snapshot.hasImeSurface();
}
@Override
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
index 154aa7d4..4268eb9 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
@@ -231,7 +231,14 @@
public void testMultiAuth_singleSensor_fingerprintSensorStartsAfterDialogAnimationCompletes()
throws Exception {
setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
- testMultiAuth_fingerprintSensorStartsAfterUINotifies();
+ testMultiAuth_fingerprintSensorStartsAfterUINotifies(true /* startFingerprintNow */);
+ }
+
+ @Test
+ public void testMultiAuth_singleSensor_fingerprintSensorDoesNotStartAfterDialogAnimationCompletes()
+ throws Exception {
+ setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
+ testMultiAuth_fingerprintSensorStartsAfterUINotifies(false /* startFingerprintNow */);
}
@Test
@@ -239,10 +246,18 @@
throws Exception {
setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class));
- testMultiAuth_fingerprintSensorStartsAfterUINotifies();
+ testMultiAuth_fingerprintSensorStartsAfterUINotifies(true /* startFingerprintNow */);
}
- public void testMultiAuth_fingerprintSensorStartsAfterUINotifies()
+ @Test
+ public void testMultiAuth_fingerprintSensorDoesNotStartAfterDialogAnimationCompletes()
+ throws Exception {
+ setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
+ setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class));
+ testMultiAuth_fingerprintSensorStartsAfterUINotifies(false /* startFingerprintNow */);
+ }
+
+ public void testMultiAuth_fingerprintSensorStartsAfterUINotifies(boolean startFingerprintNow)
throws Exception {
final long operationId = 123;
final int userId = 10;
@@ -282,13 +297,21 @@
// fingerprint sensor does not start even if all cookies are received
assertEquals(STATE_AUTH_STARTED, session.getState());
verify(mStatusBarService).showAuthenticationDialog(any(), any(), any(),
- anyBoolean(), anyBoolean(), anyInt(), anyLong(), any(), anyLong(), anyInt());
+ anyBoolean(), anyBoolean(), anyInt(), anyLong(), any(), anyLong());
// Notify AuthSession that the UI is shown. Then, fingerprint sensor should be started.
- session.onDialogAnimatedIn();
+ session.onDialogAnimatedIn(startFingerprintNow);
assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
- assertEquals(BiometricSensor.STATE_AUTHENTICATING,
+ assertEquals(startFingerprintNow ? BiometricSensor.STATE_AUTHENTICATING
+ : BiometricSensor.STATE_COOKIE_RETURNED,
session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState());
+
+ // start fingerprint sensor if it was delayed
+ if (!startFingerprintNow) {
+ session.onStartFingerprint();
+ assertEquals(BiometricSensor.STATE_AUTHENTICATING,
+ session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState());
+ }
}
@Test
@@ -316,14 +339,14 @@
verify(impl, never()).startPreparedClient(anyInt());
// First invocation should start the client monitor.
- session.onDialogAnimatedIn();
+ session.onDialogAnimatedIn(true /* startFingerprintNow */);
assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
verify(impl).startPreparedClient(anyInt());
// Subsequent invocations should not start the client monitor again.
- session.onDialogAnimatedIn();
- session.onDialogAnimatedIn();
- session.onDialogAnimatedIn();
+ session.onDialogAnimatedIn(true /* startFingerprintNow */);
+ session.onDialogAnimatedIn(false /* startFingerprintNow */);
+ session.onDialogAnimatedIn(true /* startFingerprintNow */);
assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
verify(impl, times(1)).startPreparedClient(anyInt());
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
index 520e1c8..67be376 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
@@ -18,7 +18,7 @@
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.Authenticators;
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
+import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI;
@@ -311,8 +311,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(TEST_REQUEST_ID),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(TEST_REQUEST_ID));
}
@Test
@@ -397,8 +396,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(TEST_REQUEST_ID),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(TEST_REQUEST_ID));
}
@Test
@@ -516,7 +514,7 @@
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
// startPreparedClient invoked
- mBiometricService.mAuthSession.onDialogAnimatedIn();
+ mBiometricService.mAuthSession.onDialogAnimatedIn(true /* startFingerprintNow */);
verify(mBiometricService.mSensors.get(0).impl)
.startPreparedClient(cookieCaptor.getValue());
@@ -530,8 +528,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(TEST_REQUEST_ID),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(TEST_REQUEST_ID));
// Hardware authenticated
final byte[] HAT = generateRandomHAT();
@@ -587,8 +584,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(TEST_REQUEST_ID),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(TEST_REQUEST_ID));
}
@Test
@@ -752,8 +748,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
anyString(),
- anyLong() /* requestId */,
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ anyLong() /* requestId */);
}
@Test
@@ -854,8 +849,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(TEST_REQUEST_ID),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(TEST_REQUEST_ID));
}
@Test
@@ -935,8 +929,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(TEST_REQUEST_ID),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(TEST_REQUEST_ID));
}
@Test
@@ -1432,8 +1425,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(requestId),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(requestId));
// Requesting strong and credential, when credential is setup
resetReceivers();
@@ -1456,8 +1448,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(requestId),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(requestId));
// Un-downgrading the authenticator allows successful strong auth
for (BiometricSensor sensor : mBiometricService.mSensors) {
@@ -1482,8 +1473,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(requestId),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(requestId));
}
@Test(expected = IllegalStateException.class)
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index bfb6b0f1..067feae 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -23,6 +23,7 @@
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD_OR_PIN;
+import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN;
import static com.android.internal.widget.LockPatternUtils.PIN_LENGTH_UNAVAILABLE;
import static org.junit.Assert.assertEquals;
@@ -625,11 +626,9 @@
}
@Test
- public void testPasswordDataV2VersionCredentialTypePin_deserialize() {
- // Test that we can deserialize existing PasswordData and don't inadvertently change the
- // wire format.
+ public void testDeserializePasswordData_forPinWithLengthAvailable() {
byte[] serialized = new byte[] {
- 0, 2, 0, 2, /* CREDENTIAL_TYPE_PASSWORD_OR_PIN */
+ 0, 0, 0, 3, /* CREDENTIAL_TYPE_PIN */
11, /* scryptLogN */
22, /* scryptLogR */
33, /* scryptLogP */
@@ -637,25 +636,23 @@
1, 2, -1, -2, 55, /* salt */
0, 0, 0, 6, /* passwordHandle.length */
2, 3, -2, -3, 44, 1, /* passwordHandle */
- 0, 0, 0, 5, /* pinLength */
+ 0, 0, 0, 6, /* pinLength */
};
PasswordData deserialized = PasswordData.fromBytes(serialized);
assertEquals(11, deserialized.scryptLogN);
assertEquals(22, deserialized.scryptLogR);
assertEquals(33, deserialized.scryptLogP);
- assertEquals(5, deserialized.pinLength);
- assertEquals(CREDENTIAL_TYPE_PASSWORD_OR_PIN, deserialized.credentialType);
+ assertEquals(CREDENTIAL_TYPE_PIN, deserialized.credentialType);
assertArrayEquals(PAYLOAD, deserialized.salt);
assertArrayEquals(PAYLOAD2, deserialized.passwordHandle);
+ assertEquals(6, deserialized.pinLength);
}
@Test
- public void testPasswordDataV2VersionNegativePinLengthNoCredential_deserialize() {
- // Test that we can deserialize existing PasswordData and don't inadvertently change the
- // wire format.
+ public void testDeserializePasswordData_forPinWithLengthExplicitlyUnavailable() {
byte[] serialized = new byte[] {
- 0, 2, -1, -1, /* CREDENTIAL_TYPE_NONE */
+ 0, 0, 0, 3, /* CREDENTIAL_TYPE_PIN */
11, /* scryptLogN */
22, /* scryptLogR */
33, /* scryptLogP */
@@ -663,23 +660,52 @@
1, 2, -1, -2, 55, /* salt */
0, 0, 0, 6, /* passwordHandle.length */
2, 3, -2, -3, 44, 1, /* passwordHandle */
- -1, -1, -1, -2, /* pinLength */
+ -1, -1, -1, -1, /* pinLength */
};
PasswordData deserialized = PasswordData.fromBytes(serialized);
assertEquals(11, deserialized.scryptLogN);
assertEquals(22, deserialized.scryptLogR);
assertEquals(33, deserialized.scryptLogP);
- assertEquals(-2, deserialized.pinLength);
- assertEquals(CREDENTIAL_TYPE_NONE, deserialized.credentialType);
+ assertEquals(CREDENTIAL_TYPE_PIN, deserialized.credentialType);
assertArrayEquals(PAYLOAD, deserialized.salt);
assertArrayEquals(PAYLOAD2, deserialized.passwordHandle);
+ assertEquals(PIN_LENGTH_UNAVAILABLE, deserialized.pinLength);
}
@Test
- public void testPasswordDataV1VersionNoCredential_deserialize() {
- // Test that we can deserialize existing PasswordData and don't inadvertently change the
- // wire format.
+ public void testDeserializePasswordData_forPinWithVersionNumber() {
+ // Test deserializing a PasswordData that has a version number in the first two bytes.
+ // Files like this were created by some Android 14 beta versions. This version number was a
+ // mistake and should be ignored by the deserializer.
+ byte[] serialized = new byte[] {
+ 0, 2, /* version 2 */
+ 0, 3, /* CREDENTIAL_TYPE_PIN */
+ 11, /* scryptLogN */
+ 22, /* scryptLogR */
+ 33, /* scryptLogP */
+ 0, 0, 0, 5, /* salt.length */
+ 1, 2, -1, -2, 55, /* salt */
+ 0, 0, 0, 6, /* passwordHandle.length */
+ 2, 3, -2, -3, 44, 1, /* passwordHandle */
+ 0, 0, 0, 6, /* pinLength */
+ };
+ PasswordData deserialized = PasswordData.fromBytes(serialized);
+
+ assertEquals(11, deserialized.scryptLogN);
+ assertEquals(22, deserialized.scryptLogR);
+ assertEquals(33, deserialized.scryptLogP);
+ assertEquals(CREDENTIAL_TYPE_PIN, deserialized.credentialType);
+ assertArrayEquals(PAYLOAD, deserialized.salt);
+ assertArrayEquals(PAYLOAD2, deserialized.passwordHandle);
+ assertEquals(6, deserialized.pinLength);
+ }
+
+ @Test
+ public void testDeserializePasswordData_forNoneCred() {
+ // Test that a PasswordData that uses CREDENTIAL_TYPE_NONE and lacks the PIN length field
+ // can be deserialized. Files like this were created by Android 13 and earlier. Android 14
+ // and later no longer create PasswordData for CREDENTIAL_TYPE_NONE.
byte[] serialized = new byte[] {
-1, -1, -1, -1, /* CREDENTIAL_TYPE_NONE */
11, /* scryptLogN */
@@ -695,16 +721,17 @@
assertEquals(11, deserialized.scryptLogN);
assertEquals(22, deserialized.scryptLogR);
assertEquals(33, deserialized.scryptLogP);
- assertEquals(PIN_LENGTH_UNAVAILABLE, deserialized.pinLength);
assertEquals(CREDENTIAL_TYPE_NONE, deserialized.credentialType);
assertArrayEquals(PAYLOAD, deserialized.salt);
assertArrayEquals(PAYLOAD2, deserialized.passwordHandle);
+ assertEquals(PIN_LENGTH_UNAVAILABLE, deserialized.pinLength);
}
@Test
- public void testPasswordDataV1VersionCredentialTypePin_deserialize() {
- // Test that we can deserialize existing PasswordData and don't inadvertently change the
- // wire format.
+ public void testDeserializePasswordData_forPasswordOrPin() {
+ // Test that a PasswordData that uses CREDENTIAL_TYPE_PASSWORD_OR_PIN and lacks the PIN
+ // length field can be deserialized. Files like this were created by Android 10 and
+ // earlier. Android 11 eliminated CREDENTIAL_TYPE_PASSWORD_OR_PIN.
byte[] serialized = new byte[] {
0, 0, 0, 2, /* CREDENTIAL_TYPE_PASSWORD_OR_PIN */
11, /* scryptLogN */
@@ -720,10 +747,10 @@
assertEquals(11, deserialized.scryptLogN);
assertEquals(22, deserialized.scryptLogR);
assertEquals(33, deserialized.scryptLogP);
- assertEquals(PIN_LENGTH_UNAVAILABLE, deserialized.pinLength);
assertEquals(CREDENTIAL_TYPE_PASSWORD_OR_PIN, deserialized.credentialType);
assertArrayEquals(PAYLOAD, deserialized.salt);
assertArrayEquals(PAYLOAD2, deserialized.passwordHandle);
+ assertEquals(PIN_LENGTH_UNAVAILABLE, deserialized.pinLength);
}
@Test
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
index 3a65104..7598952 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
@@ -84,6 +84,7 @@
import com.android.internal.app.IHotwordRecognitionStatusCallback;
import com.android.internal.infra.AndroidFuture;
import com.android.server.LocalServices;
+import com.android.server.policy.AppOpsPolicy;
import com.android.server.voiceinteraction.VoiceInteractionManagerServiceImpl.DetectorRemoteExceptionListener;
import java.io.Closeable;
@@ -742,18 +743,24 @@
void enforcePermissionsForDataDelivery() {
Binder.withCleanCallingIdentity(() -> {
synchronized (mLock) {
- int result = PermissionChecker.checkPermissionForPreflight(
- mContext, RECORD_AUDIO, /* pid */ -1, mVoiceInteractorIdentity.uid,
- mVoiceInteractorIdentity.packageName);
- if (result != PermissionChecker.PERMISSION_GRANTED) {
- throw new SecurityException(
- "Failed to obtain permission RECORD_AUDIO for identity "
- + mVoiceInteractorIdentity);
+ if (AppOpsPolicy.isHotwordDetectionServiceRequired(mContext.getPackageManager())) {
+ int result = PermissionChecker.checkPermissionForPreflight(
+ mContext, RECORD_AUDIO, /* pid */ -1, mVoiceInteractorIdentity.uid,
+ mVoiceInteractorIdentity.packageName);
+ if (result != PermissionChecker.PERMISSION_GRANTED) {
+ throw new SecurityException(
+ "Failed to obtain permission RECORD_AUDIO for identity "
+ + mVoiceInteractorIdentity);
+ }
+ int hotwordOp = AppOpsManager.strOpToOp(
+ AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD);
+ mAppOpsManager.noteOpNoThrow(hotwordOp,
+ mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
+ mVoiceInteractorIdentity.attributionTag, HOTWORD_DETECTION_OP_MESSAGE);
+ } else {
+ enforcePermissionForDataDelivery(mContext, mVoiceInteractorIdentity,
+ RECORD_AUDIO, HOTWORD_DETECTION_OP_MESSAGE);
}
- int hotwordOp = AppOpsManager.strOpToOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD);
- mAppOpsManager.noteOpNoThrow(hotwordOp,
- mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
- mVoiceInteractorIdentity.attributionTag, HOTWORD_DETECTION_OP_MESSAGE);
enforcePermissionForDataDelivery(mContext, mVoiceInteractorIdentity,
CAPTURE_AUDIO_HOTWORD, HOTWORD_DETECTION_OP_MESSAGE);
}