diff options
277 files changed, 7726 insertions, 2623 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 48d6392f8b9b..8591a9c4a195 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -77,6 +77,7 @@ aconfig_declarations_group { "camera_platform_flags_core_java_lib", "com.android.hardware.input-aconfig-java", "com.android.input.flags-aconfig-java", + "com.android.internal.compat.flags-aconfig-java", "com.android.internal.foldables.flags-aconfig-java", "com.android.internal.pm.pkg.component.flags-aconfig-java", "com.android.media.flags.bettertogether-aconfig-java", @@ -663,6 +664,13 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +// Platform Compat +java_aconfig_library { + name: "com.android.internal.compat.flags-aconfig-java", + aconfig_declarations: "compat_logging_flags", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Multi user aconfig_declarations { name: "android.multiuser.flags-aconfig", diff --git a/apex/jobscheduler/service/Android.bp b/apex/jobscheduler/service/Android.bp index 0104ee14fec4..ace56d42ddd1 100644 --- a/apex/jobscheduler/service/Android.bp +++ b/apex/jobscheduler/service/Android.bp @@ -20,6 +20,7 @@ java_library { ], libs: [ + "androidx.annotation_annotation", "app-compat-annotations", "error_prone_annotations", "framework", diff --git a/core/api/current.txt b/core/api/current.txt index 05b67eef0985..7552b5c00acc 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -4482,7 +4482,7 @@ package android.app { method @CallSuper public void onActionModeStarted(android.view.ActionMode); method public void onActivityReenter(int, android.content.Intent); method protected void onActivityResult(int, int, android.content.Intent); - method @FlaggedApi("android.security.content_uri_permission_apis") public void onActivityResult(int, int, @NonNull android.content.Intent, @NonNull android.app.ComponentCaller); + method @FlaggedApi("android.security.content_uri_permission_apis") public void onActivityResult(int, int, @Nullable android.content.Intent, @NonNull android.app.ComponentCaller); method @Deprecated public void onAttachFragment(android.app.Fragment); method public void onAttachedToWindow(); method @Deprecated public void onBackPressed(); @@ -9602,8 +9602,8 @@ package android.appwidget { method public static android.appwidget.AppWidgetManager getInstance(android.content.Context); method @FlaggedApi("android.appwidget.flags.generated_previews") @Nullable public android.widget.RemoteViews getWidgetPreview(@NonNull android.content.ComponentName, @Nullable android.os.UserHandle, int); method public boolean isRequestPinAppWidgetSupported(); - method public void notifyAppWidgetViewDataChanged(int[], int); - method public void notifyAppWidgetViewDataChanged(int, int); + method @Deprecated public void notifyAppWidgetViewDataChanged(int[], int); + method @Deprecated public void notifyAppWidgetViewDataChanged(int, int); method public void partiallyUpdateAppWidget(int[], android.widget.RemoteViews); method public void partiallyUpdateAppWidget(int, android.widget.RemoteViews); method @FlaggedApi("android.appwidget.flags.generated_previews") public void removeWidgetPreview(@NonNull android.content.ComponentName, int); @@ -28054,7 +28054,7 @@ package android.media.tv.ad { method public void sendSigningResult(@NonNull String, @NonNull byte[]); method public void sendTrackInfoList(@Nullable java.util.List<android.media.tv.TvTrackInfo>); method public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdView.TvAdCallback); - method public void setOnUnhandledInputEventListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdView.OnUnhandledInputEventListener); + method public void setOnUnhandledInputEventListener(@NonNull android.media.tv.ad.TvAdView.OnUnhandledInputEventListener); method public boolean setTvView(@Nullable android.media.tv.TvView); method public void startAdService(); method public void stopAdService(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index fb2a4ac944a9..4b04d10a6e2c 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -4912,7 +4912,6 @@ package android.hardware.camera2.extension { method @FlaggedApi("com.android.internal.camera.flags.concert_mode") public int getImageFormat(); method @FlaggedApi("com.android.internal.camera.flags.concert_mode") @NonNull public android.util.Size getSize(); method @FlaggedApi("com.android.internal.camera.flags.concert_mode") @NonNull public android.view.Surface getSurface(); - method @FlaggedApi("com.android.internal.camera.flags.extension_10_bit") public void setColorSpace(int); method @FlaggedApi("com.android.internal.camera.flags.extension_10_bit") public void setDynamicRangeProfile(long); } @@ -4923,6 +4922,7 @@ package android.hardware.camera2.extension { @FlaggedApi("com.android.internal.camera.flags.concert_mode") public class ExtensionConfiguration { ctor @FlaggedApi("com.android.internal.camera.flags.concert_mode") public ExtensionConfiguration(int, int, @NonNull java.util.List<android.hardware.camera2.extension.ExtensionOutputConfiguration>, @Nullable android.hardware.camera2.CaptureRequest); + method @FlaggedApi("com.android.internal.camera.flags.extension_10_bit") public void setColorSpace(int); } @FlaggedApi("com.android.internal.camera.flags.concert_mode") public class ExtensionOutputConfiguration { @@ -6261,7 +6261,7 @@ package android.hardware.radio { method public void addOnCompleteListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.radio.ProgramList.OnCompleteListener); method public void addOnCompleteListener(@NonNull android.hardware.radio.ProgramList.OnCompleteListener); method public void close(); - method @Nullable public android.hardware.radio.RadioManager.ProgramInfo get(@NonNull android.hardware.radio.ProgramSelector.Identifier); + method @Deprecated @Nullable public android.hardware.radio.RadioManager.ProgramInfo get(@NonNull android.hardware.radio.ProgramSelector.Identifier); method @FlaggedApi("android.hardware.radio.hd_radio_improved") @NonNull public java.util.List<android.hardware.radio.RadioManager.ProgramInfo> getProgramInfos(@NonNull android.hardware.radio.ProgramSelector.Identifier); method public void registerListCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.radio.ProgramList.ListCallback); method public void registerListCallback(@NonNull android.hardware.radio.ProgramList.ListCallback); @@ -6313,7 +6313,7 @@ package android.hardware.radio { field @Deprecated public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5; // 0x5 field @Deprecated public static final int IDENTIFIER_TYPE_DAB_SID_EXT = 5; // 0x5 field public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10; // 0xa - field public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11; // 0xb + field @Deprecated public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11; // 0xb field public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9; // 0x9 field public static final int IDENTIFIER_TYPE_HD_STATION_ID_EXT = 3; // 0x3 field @FlaggedApi("android.hardware.radio.hd_radio_improved") public static final int IDENTIFIER_TYPE_HD_STATION_LOCATION = 15; // 0xf @@ -6321,8 +6321,8 @@ package android.hardware.radio { field @Deprecated public static final int IDENTIFIER_TYPE_HD_SUBCHANNEL = 4; // 0x4 field public static final int IDENTIFIER_TYPE_INVALID = 0; // 0x0 field public static final int IDENTIFIER_TYPE_RDS_PI = 2; // 0x2 - field public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13; // 0xd - field public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12; // 0xc + field @Deprecated public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13; // 0xd + field @Deprecated public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12; // 0xc field public static final int IDENTIFIER_TYPE_VENDOR_END = 1999; // 0x7cf field @Deprecated public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_END = 1999; // 0x7cf field @Deprecated public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_START = 1000; // 0x3e8 @@ -6375,7 +6375,7 @@ package android.hardware.radio { field public static final int CONFIG_DAB_DAB_SOFT_LINKING = 8; // 0x8 field public static final int CONFIG_DAB_FM_LINKING = 7; // 0x7 field public static final int CONFIG_DAB_FM_SOFT_LINKING = 9; // 0x9 - field public static final int CONFIG_FORCE_ANALOG = 2; // 0x2 + field @Deprecated public static final int CONFIG_FORCE_ANALOG = 2; // 0x2 field @FlaggedApi("android.hardware.radio.hd_radio_improved") public static final int CONFIG_FORCE_ANALOG_AM = 11; // 0xb field @FlaggedApi("android.hardware.radio.hd_radio_improved") public static final int CONFIG_FORCE_ANALOG_FM = 10; // 0xa field public static final int CONFIG_FORCE_DIGITAL = 3; // 0x3 diff --git a/core/java/Android.bp b/core/java/Android.bp index ab1c9a4ef48d..4f96206bfd08 100644 --- a/core/java/Android.bp +++ b/core/java/Android.bp @@ -167,6 +167,9 @@ java_library { "com/android/internal/logging/UiEventLoggerImpl.java", ":statslog-framework-java-gen", ], + libs: [ + "androidx.annotation_annotation", + ], static_libs: ["modules-utils-uieventlogger-interface"], } diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index afbefca0cefe..1cc2d25fb76d 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -7473,7 +7473,7 @@ public class Activity extends ContextThemeWrapper * intent. */ @FlaggedApi(android.security.Flags.FLAG_CONTENT_URI_PERMISSION_APIS) - public void onActivityResult(int requestCode, int resultCode, @NonNull Intent data, + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data, @NonNull ComponentCaller caller) { onActivityResult(requestCode, resultCode, data); } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 1d39186de183..ae5cacd18aa2 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -974,6 +974,7 @@ public final class ActivityThread extends ClientTransactionHandler ContentCaptureOptions contentCaptureOptions; long[] disabledCompatChanges; + long[] mLoggableCompatChanges; SharedMemory mSerializedSystemFontMap; @@ -1283,6 +1284,7 @@ public final class ActivityThread extends ClientTransactionHandler AutofillOptions autofillOptions, ContentCaptureOptions contentCaptureOptions, long[] disabledCompatChanges, + long[] loggableCompatChanges, SharedMemory serializedSystemFontMap, long startRequestedElapsedTime, long startRequestedUptime) { @@ -1337,6 +1339,7 @@ public final class ActivityThread extends ClientTransactionHandler data.autofillOptions = autofillOptions; data.contentCaptureOptions = contentCaptureOptions; data.disabledCompatChanges = disabledCompatChanges; + data.mLoggableCompatChanges = loggableCompatChanges; data.mSerializedSystemFontMap = serializedSystemFontMap; data.startRequestedElapsedTime = startRequestedElapsedTime; data.startRequestedUptime = startRequestedUptime; @@ -4073,6 +4076,13 @@ public final class ActivityThread extends ClientTransactionHandler ActivityManager.getService().waitForNetworkStateUpdate(mNetworkBlockSeq); mNetworkBlockSeq = INVALID_PROC_STATE_SEQ; } catch (RemoteException ignored) {} + if (Flags.clearDnsCacheOnNetworkRulesUpdate()) { + // InetAddress will cache UnknownHostException failures. If the rules got + // updated and the app has network access now, we need to clear the negative + // cache to ensure valid dns queries can work immediately. + // TODO: b/329133769 - Clear only the negative cache once it is available. + InetAddress.clearDnsCache(); + } } } } @@ -7123,7 +7133,7 @@ public final class ActivityThread extends ClientTransactionHandler Process.setStartTimes(SystemClock.elapsedRealtime(), SystemClock.uptimeMillis(), data.startRequestedElapsedTime, data.startRequestedUptime); - AppCompatCallbacks.install(data.disabledCompatChanges); + AppCompatCallbacks.install(data.disabledCompatChanges, data.mLoggableCompatChanges); // Let libcore handle any compat changes after installing the list of compat changes. AppSpecializationHooks.handleCompatChangesBeforeBindingApplication(); diff --git a/core/java/android/app/AppCompatCallbacks.java b/core/java/android/app/AppCompatCallbacks.java index 134cef5b6bfa..f2debfcfa6b1 100644 --- a/core/java/android/app/AppCompatCallbacks.java +++ b/core/java/android/app/AppCompatCallbacks.java @@ -30,41 +30,59 @@ import java.util.Arrays; */ public final class AppCompatCallbacks implements Compatibility.BehaviorChangeDelegate { private final long[] mDisabledChanges; + private final long[] mLoggableChanges; private final ChangeReporter mChangeReporter; /** - * Install this class into the current process. + * Install this class into the current process using the disabled and loggable changes lists. * * @param disabledChanges Set of compatibility changes that are disabled for this process. + * @param loggableChanges Set of compatibility changes that we want to log. */ - public static void install(long[] disabledChanges) { - Compatibility.setBehaviorChangeDelegate(new AppCompatCallbacks(disabledChanges)); + public static void install(long[] disabledChanges, long[] loggableChanges) { + Compatibility.setBehaviorChangeDelegate( + new AppCompatCallbacks(disabledChanges, loggableChanges)); } - private AppCompatCallbacks(long[] disabledChanges) { + private AppCompatCallbacks(long[] disabledChanges, long[] loggableChanges) { mDisabledChanges = Arrays.copyOf(disabledChanges, disabledChanges.length); + mLoggableChanges = Arrays.copyOf(loggableChanges, loggableChanges.length); Arrays.sort(mDisabledChanges); - mChangeReporter = new ChangeReporter( - ChangeReporter.SOURCE_APP_PROCESS); + Arrays.sort(mLoggableChanges); + mChangeReporter = new ChangeReporter(ChangeReporter.SOURCE_APP_PROCESS); + } + + /** + * Helper to determine if a list contains a changeId. + * + * @param list to search through + * @param changeId for which to search in the list + * @return true if the given changeId is found in the provided array. + */ + private boolean changeIdInChangeList(long[] list, long changeId) { + return Arrays.binarySearch(list, changeId) >= 0; } public void onChangeReported(long changeId) { - reportChange(changeId, ChangeReporter.STATE_LOGGED); + boolean isLoggable = changeIdInChangeList(mLoggableChanges, changeId); + reportChange(changeId, ChangeReporter.STATE_LOGGED, isLoggable); } public boolean isChangeEnabled(long changeId) { - if (Arrays.binarySearch(mDisabledChanges, changeId) < 0) { - // Not present in the disabled array - reportChange(changeId, ChangeReporter.STATE_ENABLED); + boolean isEnabled = !changeIdInChangeList(mDisabledChanges, changeId); + boolean isLoggable = changeIdInChangeList(mLoggableChanges, changeId); + if (isEnabled) { + // Not present in the disabled changeId array + reportChange(changeId, ChangeReporter.STATE_ENABLED, isLoggable); return true; } - reportChange(changeId, ChangeReporter.STATE_DISABLED); + reportChange(changeId, ChangeReporter.STATE_DISABLED, isLoggable); return false; } - private void reportChange(long changeId, int state) { + private void reportChange(long changeId, int state, boolean isLoggable) { int uid = Process.myUid(); - mChangeReporter.reportChange(uid, changeId, state); + mChangeReporter.reportChange(uid, changeId, state, isLoggable); } } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index af56cb4d55b2..ed0c9338d612 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -272,10 +272,17 @@ class ContextImpl extends Context { @UnsupportedAppUsage private Context mOuterContext; + + private final Object mThemeLock = new Object(); + @UnsupportedAppUsage + @GuardedBy("mThemeLock") private int mThemeResource = 0; + @UnsupportedAppUsage + @GuardedBy("mThemeLock") private Resources.Theme mTheme = null; + @UnsupportedAppUsage private PackageManager mPackageManager; private Context mReceiverRestrictedContext = null; @@ -288,7 +295,6 @@ class ContextImpl extends Context { private ContentCaptureOptions mContentCaptureOptions = null; - private final Object mSync = new Object(); /** * Indicates this {@link Context} can not handle UI components properly and is not associated * with a {@link Display} instance. @@ -340,21 +346,18 @@ class ContextImpl extends Context { */ private boolean mOwnsToken = false; - @GuardedBy("mSync") - private File mDatabasesDir; - @GuardedBy("mSync") - @UnsupportedAppUsage - private File mPreferencesDir; - @GuardedBy("mSync") - private File mFilesDir; - @GuardedBy("mSync") - private File mCratesDir; - @GuardedBy("mSync") - private File mNoBackupFilesDir; - @GuardedBy("mSync") - private File mCacheDir; - @GuardedBy("mSync") - private File mCodeCacheDir; + private final Object mDirsLock = new Object(); + private volatile File mDatabasesDir; + @UnsupportedAppUsage private volatile File mPreferencesDir; + private volatile File mFilesDir; + private volatile File mCratesDir; + private volatile File mNoBackupFilesDir; + private volatile File[] mExternalFilesDirs; + private volatile File[] mObbDirs; + private volatile File mCacheDir; + private volatile File mCodeCacheDir; + private volatile File[] mExternalCacheDirs; + private volatile File[] mExternalMediaDirs; // The system service cache for the system services that are cached per-ContextImpl. @UnsupportedAppUsage @@ -458,7 +461,7 @@ class ContextImpl extends Context { @Override public void setTheme(int resId) { - synchronized (mSync) { + synchronized (mThemeLock) { if (mThemeResource != resId) { mThemeResource = resId; initializeTheme(); @@ -468,14 +471,14 @@ class ContextImpl extends Context { @Override public int getThemeResId() { - synchronized (mSync) { + synchronized (mThemeLock) { return mThemeResource; } } @Override public Resources.Theme getTheme() { - synchronized (mSync) { + synchronized (mThemeLock) { if (mTheme != null) { return mTheme; } @@ -488,6 +491,7 @@ class ContextImpl extends Context { } } + @GuardedBy("mThemeLock") private void initializeTheme() { if (mTheme == null) { mTheme = mResources.newTheme(); @@ -597,12 +601,18 @@ class ContextImpl extends Context { if (sp == null) { checkMode(mode); if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) { - if (isCredentialProtectedStorage() - && !getSystemService(UserManager.class) - .isUserUnlockingOrUnlocked(UserHandle.myUserId())) { - throw new IllegalStateException("SharedPreferences in credential encrypted " - + "storage are not available until after user (id " - + UserHandle.myUserId() + ") is unlocked"); + if (isCredentialProtectedStorage()) { + final UserManager um = getSystemService(UserManager.class); + if (um == null) { + throw new IllegalStateException("SharedPreferences cannot be accessed " + + "if UserManager is not available. " + + "(e.g. from inside an isolated process)"); + } + if (!um.isUserUnlockingOrUnlocked(UserHandle.myUserId())) { + throw new IllegalStateException("SharedPreferences in " + + "credential encrypted storage are not available until after " + + "user (id " + UserHandle.myUserId() + ") is unlocked"); + } } } sp = new SharedPreferencesImpl(file, mode); @@ -731,12 +741,18 @@ class ContextImpl extends Context { @UnsupportedAppUsage private File getPreferencesDir() { - synchronized (mSync) { - if (mPreferencesDir == null) { - mPreferencesDir = new File(getDataDir(), "shared_prefs"); + File localPreferencesDir = mPreferencesDir; + if (localPreferencesDir == null) { + synchronized (mDirsLock) { + localPreferencesDir = mPreferencesDir; + if (localPreferencesDir == null) { + localPreferencesDir = new File(getDataDir(), "shared_prefs"); + ensurePrivateDirExists(localPreferencesDir); + mPreferencesDir = localPreferencesDir; + } } - return ensurePrivateDirExists(mPreferencesDir); } + return localPreferencesDir; } @Override @@ -778,16 +794,16 @@ class ContextImpl extends Context { /** * Common-path handling of app data dir creation */ - private static File ensurePrivateDirExists(File file) { - return ensurePrivateDirExists(file, 0771, -1, null); + private static void ensurePrivateDirExists(File file) { + ensurePrivateDirExists(file, 0771, -1, null); } - private static File ensurePrivateCacheDirExists(File file, String xattr) { + private static void ensurePrivateCacheDirExists(File file, String xattr) { final int gid = UserHandle.getCacheAppGid(Process.myUid()); - return ensurePrivateDirExists(file, 02771, gid, xattr); + ensurePrivateDirExists(file, 02771, gid, xattr); } - private static File ensurePrivateDirExists(File file, int mode, int gid, String xattr) { + private static void ensurePrivateDirExists(File file, int mode, int gid, String xattr) { if (!file.exists()) { final String path = file.getAbsolutePath(); try { @@ -815,17 +831,22 @@ class ContextImpl extends Context { } } } - return file; } @Override public File getFilesDir() { - synchronized (mSync) { - if (mFilesDir == null) { - mFilesDir = new File(getDataDir(), "files"); + File localFilesDir = mFilesDir; + if (localFilesDir == null) { + localFilesDir = mFilesDir; + synchronized (mDirsLock) { + if (localFilesDir == null) { + localFilesDir = new File(getDataDir(), "files"); + ensurePrivateDirExists(localFilesDir); + mFilesDir = localFilesDir; + } } - return ensurePrivateDirExists(mFilesDir); } + return localFilesDir; } @Override @@ -835,25 +856,37 @@ class ContextImpl extends Context { final Path absoluteNormalizedCratePath = cratesRootPath.resolve(crateId) .toAbsolutePath().normalize(); - synchronized (mSync) { - if (mCratesDir == null) { - mCratesDir = cratesRootPath.toFile(); + File localCratesDir = mCratesDir; + if (localCratesDir == null) { + synchronized (mDirsLock) { + localCratesDir = mCratesDir; + if (localCratesDir == null) { + localCratesDir = cratesRootPath.toFile(); + ensurePrivateDirExists(localCratesDir); + mCratesDir = localCratesDir; + } } - ensurePrivateDirExists(mCratesDir); } - File cratedDir = absoluteNormalizedCratePath.toFile(); - return ensurePrivateDirExists(cratedDir); + File crateDir = absoluteNormalizedCratePath.toFile(); + ensurePrivateDirExists(crateDir); + return crateDir; } @Override public File getNoBackupFilesDir() { - synchronized (mSync) { - if (mNoBackupFilesDir == null) { - mNoBackupFilesDir = new File(getDataDir(), "no_backup"); + File localNoBackupFilesDir = mNoBackupFilesDir; + if (localNoBackupFilesDir == null) { + synchronized (mDirsLock) { + localNoBackupFilesDir = mNoBackupFilesDir; + if (localNoBackupFilesDir == null) { + localNoBackupFilesDir = new File(getDataDir(), "no_backup"); + ensurePrivateDirExists(localNoBackupFilesDir); + mNoBackupFilesDir = localNoBackupFilesDir; + } } - return ensurePrivateDirExists(mNoBackupFilesDir); } + return localNoBackupFilesDir; } @Override @@ -865,13 +898,24 @@ class ContextImpl extends Context { @Override public File[] getExternalFilesDirs(String type) { - synchronized (mSync) { - File[] dirs = Environment.buildExternalStorageAppFilesDirs(getPackageName()); - if (type != null) { - dirs = Environment.buildPaths(dirs, type); + File[] localExternalFilesDirs = mExternalFilesDirs; + if (localExternalFilesDirs == null) { + synchronized (mDirsLock) { + localExternalFilesDirs = mExternalFilesDirs; + if (localExternalFilesDirs == null) { + localExternalFilesDirs = + Environment.buildExternalStorageAppFilesDirs(getPackageName()); + if (type != null) { + localExternalFilesDirs = + Environment.buildPaths(localExternalFilesDirs, type); + } + localExternalFilesDirs = ensureExternalDirsExistOrFilter( + localExternalFilesDirs, true /* tryCreateInProcess */); + mExternalFilesDirs = localExternalFilesDirs; + } } - return ensureExternalDirsExistOrFilter(dirs, true /* tryCreateInProcess */); } + return localExternalFilesDirs; } @Override @@ -883,30 +927,51 @@ class ContextImpl extends Context { @Override public File[] getObbDirs() { - synchronized (mSync) { - File[] dirs = Environment.buildExternalStorageAppObbDirs(getPackageName()); - return ensureExternalDirsExistOrFilter(dirs, true /* tryCreateInProcess */); + File[] localObbDirs = mObbDirs; + if (mObbDirs == null) { + synchronized (mDirsLock) { + localObbDirs = mObbDirs; + if (localObbDirs == null) { + localObbDirs = Environment.buildExternalStorageAppObbDirs(getPackageName()); + localObbDirs = ensureExternalDirsExistOrFilter( + localObbDirs, true /* tryCreateInProcess */); + mObbDirs = localObbDirs; + } + } } + return localObbDirs; } @Override public File getCacheDir() { - synchronized (mSync) { - if (mCacheDir == null) { - mCacheDir = new File(getDataDir(), "cache"); + File localCacheDir = mCacheDir; + if (localCacheDir == null) { + synchronized (mDirsLock) { + localCacheDir = mCacheDir; + if (localCacheDir == null) { + localCacheDir = new File(getDataDir(), "cache"); + ensurePrivateCacheDirExists(localCacheDir, XATTR_INODE_CACHE); + mCacheDir = localCacheDir; + } } - return ensurePrivateCacheDirExists(mCacheDir, XATTR_INODE_CACHE); } + return localCacheDir; } @Override public File getCodeCacheDir() { - synchronized (mSync) { - if (mCodeCacheDir == null) { - mCodeCacheDir = getCodeCacheDirBeforeBind(getDataDir()); + File localCodeCacheDir = mCodeCacheDir; + if (localCodeCacheDir == null) { + synchronized (mDirsLock) { + localCodeCacheDir = mCodeCacheDir; + if (localCodeCacheDir == null) { + localCodeCacheDir = getCodeCacheDirBeforeBind(getDataDir()); + ensurePrivateCacheDirExists(localCodeCacheDir, XATTR_INODE_CODE_CACHE); + mCodeCacheDir = localCodeCacheDir; + } } - return ensurePrivateCacheDirExists(mCodeCacheDir, XATTR_INODE_CODE_CACHE); } + return localCodeCacheDir; } /** @@ -927,21 +992,37 @@ class ContextImpl extends Context { @Override public File[] getExternalCacheDirs() { - synchronized (mSync) { - File[] dirs = Environment.buildExternalStorageAppCacheDirs(getPackageName()); - // We don't try to create cache directories in-process, because they need special - // setup for accurate quota tracking. This ensures the cache dirs are always - // created through StorageManagerService. - return ensureExternalDirsExistOrFilter(dirs, false /* tryCreateInProcess */); + File[] localExternalCacheDirs = mExternalCacheDirs; + if (localExternalCacheDirs == null) { + synchronized (mDirsLock) { + localExternalCacheDirs = mExternalCacheDirs; + if (localExternalCacheDirs == null) { + localExternalCacheDirs = + Environment.buildExternalStorageAppCacheDirs(getPackageName()); + localExternalCacheDirs = ensureExternalDirsExistOrFilter( + localExternalCacheDirs, false /* tryCreateInProcess */); + mExternalCacheDirs = localExternalCacheDirs; + } + } } + return localExternalCacheDirs; } @Override public File[] getExternalMediaDirs() { - synchronized (mSync) { - File[] dirs = Environment.buildExternalStorageAppMediaDirs(getPackageName()); - return ensureExternalDirsExistOrFilter(dirs, true /* tryCreateInProcess */); + File[] localExternalMediaDirs = mExternalMediaDirs; + if (localExternalMediaDirs == null) { + synchronized (mDirsLock) { + localExternalMediaDirs = mExternalMediaDirs; + if (localExternalMediaDirs == null) { + localExternalMediaDirs = Environment.buildExternalStorageAppMediaDirs(getPackageName()); + localExternalMediaDirs = ensureExternalDirsExistOrFilter( + localExternalMediaDirs, true /* tryCreateInProcess */); + mExternalMediaDirs = localExternalMediaDirs; + } + } } + return localExternalMediaDirs; } /** @@ -1040,16 +1121,22 @@ class ContextImpl extends Context { } private File getDatabasesDir() { - synchronized (mSync) { - if (mDatabasesDir == null) { - if ("android".equals(getPackageName())) { - mDatabasesDir = new File("/data/system"); - } else { - mDatabasesDir = new File(getDataDir(), "databases"); + File localDatabasesDir = mDatabasesDir; + if (localDatabasesDir == null) { + synchronized (mDirsLock) { + localDatabasesDir = mDatabasesDir; + if (localDatabasesDir == null) { + if ("android".equals(getPackageName())) { + localDatabasesDir = new File("/data/system"); + } else { + localDatabasesDir = new File(getDataDir(), "databases"); + } + ensurePrivateDirExists(localDatabasesDir); + mDatabasesDir = localDatabasesDir; } } - return ensurePrivateDirExists(mDatabasesDir); } + return localDatabasesDir; } @Override diff --git a/core/java/android/app/GrammaticalInflectionManager.java b/core/java/android/app/GrammaticalInflectionManager.java index 4ce983f6019b..3e7d66563c5e 100644 --- a/core/java/android/app/GrammaticalInflectionManager.java +++ b/core/java/android/app/GrammaticalInflectionManager.java @@ -125,7 +125,10 @@ public class GrammaticalInflectionManager { /** * Get the current grammatical gender of privileged application from the encrypted file. * - * @return the value of grammatical gender + * @return the value of system grammatical gender only if the calling app has the permission, + * otherwise throwing an exception. + * + * @throws SecurityException if the caller does not have the required permission. * * @see Configuration#getGrammaticalGender */ diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl index a04620cafd75..251e4e8ad834 100644 --- a/core/java/android/app/IApplicationThread.aidl +++ b/core/java/android/app/IApplicationThread.aidl @@ -90,7 +90,7 @@ oneway interface IApplicationThread { in CompatibilityInfo compatInfo, in Map services, in Bundle coreSettings, in String buildSerial, in AutofillOptions autofillOptions, in ContentCaptureOptions contentCaptureOptions, in long[] disabledCompatChanges, - in SharedMemory serializedSystemFontMap, + in long[] loggableCompatChanges, in SharedMemory serializedSystemFontMap, long startRequestedElapsedTime, long startRequestedUptime); void runIsolatedEntryPoint(in String entryPoint, in String[] entryPointArgs); void scheduleExit(); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 79cb09d5baea..cd4c0bc4524d 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -7531,6 +7531,9 @@ public class Notification implements Parcelable /** * Calls {@link android.app.Notification.Builder#build()} on the Builder this Style is * attached to. + * <p> + * Note: Calling build() multiple times returns the same Notification instance, + * so reusing a builder to create multiple Notifications is discouraged. * * @return the fully constructed Notification. */ diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java index 986205a346f7..9ef8b38666c6 100644 --- a/core/java/android/app/admin/DeviceAdminInfo.java +++ b/core/java/android/app/admin/DeviceAdminInfo.java @@ -189,10 +189,13 @@ public final class DeviceAdminInfo implements Parcelable { @FlaggedApi(FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED) public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2; + /** + * @hide + */ @IntDef({HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED, HEADLESS_DEVICE_OWNER_MODE_AFFILIATED, HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER}) @Retention(RetentionPolicy.SOURCE) - private @interface HeadlessDeviceOwnerMode {} + public @interface HeadlessDeviceOwnerMode {} /** @hide */ public static class PolicyInfo { diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 620bbaf4bbf5..cb4ed058af33 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -53,6 +53,7 @@ import static android.Manifest.permission.QUERY_ADMIN_POLICY; import static android.Manifest.permission.REQUEST_PASSWORD_COMPLEXITY; import static android.Manifest.permission.SET_TIME; import static android.Manifest.permission.SET_TIME_ZONE; +import static android.app.admin.DeviceAdminInfo.HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED; import static android.app.admin.flags.Flags.FLAG_DEVICE_THEFT_API_ENABLED; import static android.app.admin.flags.Flags.FLAG_ESIM_MANAGEMENT_ENABLED; import static android.app.admin.flags.Flags.FLAG_DEVICE_POLICY_SIZE_TRACKING_ENABLED; @@ -93,6 +94,7 @@ import android.app.Activity; import android.app.IServiceConnection; import android.app.KeyguardManager; import android.app.admin.SecurityLog.SecurityEvent; +import android.app.admin.flags.Flags; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; @@ -17526,4 +17528,25 @@ public class DevicePolicyManager { } return -1; } + + /** + * @return The headless device owner mode for the current set DO, returns + * {@link DeviceAdminInfo#HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED} if no DO is set. + * + * @hide + */ + @DeviceAdminInfo.HeadlessDeviceOwnerMode + public int getHeadlessDeviceOwnerMode() { + if (!Flags.headlessDeviceOwnerProvisioningFixEnabled()) { + return HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED; + } + if (mService != null) { + try { + return mService.getHeadlessDeviceOwnerMode(mContext.getPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED; + } }
\ No newline at end of file diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 3a7a891c7995..03d0b0f88bc0 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -625,4 +625,6 @@ interface IDevicePolicyManager { void setMaxPolicyStorageLimit(String packageName, int storageLimit); int getMaxPolicyStorageLimit(String packageName); + + int getHeadlessDeviceOwnerMode(String callerPackageName); } diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 19270199696e..c29ea6d95dcc 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -163,3 +163,13 @@ flag { description: "Enable UX changes for esim management" bug: "295301164" } + +flag { + name: "headless_device_owner_provisioning_fix_enabled" + namespace: "enterprise" + description: "Fix provisioning for single-user headless DO" + bug: "289515470" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/app/network-policy.aconfig b/core/java/android/app/network-policy.aconfig new file mode 100644 index 000000000000..88f386f6025d --- /dev/null +++ b/core/java/android/app/network-policy.aconfig @@ -0,0 +1,11 @@ +package: "android.app" + +flag { + namespace: "backstage_power" + name: "clear_dns_cache_on_network_rules_update" + description: "Clears the DNS cache when the network rules update" + bug: "237556596" + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index cda4d89b828f..2c0e035e80c4 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -822,7 +822,18 @@ public class AppWidgetManager { * * @param appWidgetIds The AppWidget instances to notify of view data changes. * @param viewId The collection view id. - */ + * @deprecated The corresponding API + * {@link RemoteViews#setRemoteAdapter(int, Intent)} associated with this method has been + * deprecated. Moving forward please use + * {@link RemoteViews#setRemoteAdapter(int, android.widget.RemoteViews.RemoteCollectionItems)} + * instead to set {@link android.widget.RemoteViews.RemoteCollectionItems} for the remote + * adapter and update the widget views by calling {@link #updateAppWidget(int[], RemoteViews)}, + * {@link #updateAppWidget(int, RemoteViews)}, + * {@link #updateAppWidget(ComponentName, RemoteViews)}, + * {@link #partiallyUpdateAppWidget(int[], RemoteViews)}, + * or {@link #partiallyUpdateAppWidget(int, RemoteViews)}, whichever applicable. + */ + @Deprecated public void notifyAppWidgetViewDataChanged(int[] appWidgetIds, int viewId) { if (mService == null) { return; @@ -873,7 +884,18 @@ public class AppWidgetManager { * * @param appWidgetId The AppWidget instance to notify of view data changes. * @param viewId The collection view id. - */ + * @deprecated The corresponding API + * {@link RemoteViews#setRemoteAdapter(int, Intent)} associated with this method has been + * deprecated. Moving forward please use + * {@link RemoteViews#setRemoteAdapter(int, android.widget.RemoteViews.RemoteCollectionItems)} + * instead to set {@link android.widget.RemoteViews.RemoteCollectionItems} for the remote + * adapter and update the widget views by calling {@link #updateAppWidget(int[], RemoteViews)}, + * {@link #updateAppWidget(int, RemoteViews)}, + * {@link #updateAppWidget(ComponentName, RemoteViews)}, + * {@link #partiallyUpdateAppWidget(int[], RemoteViews)}, + * or {@link #partiallyUpdateAppWidget(int, RemoteViews)}, whichever applicable. + */ + @Deprecated public void notifyAppWidgetViewDataChanged(int appWidgetId, int viewId) { if (mService == null) { return; diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl index 533fa512dae8..55957bf887f8 100644 --- a/core/java/android/content/pm/ILauncherApps.aidl +++ b/core/java/android/content/pm/ILauncherApps.aidl @@ -133,4 +133,7 @@ interface ILauncherApps { void setArchiveCompatibilityOptions(boolean enableIconOverlay, boolean enableUnarchivalConfirmation); List<UserHandle> getUserProfiles(); + + /** Saves view capture data to the wm trace directory. */ + void saveViewCaptureData(); } diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 41c1f17ce978..3a5383d9537b 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -1328,6 +1328,19 @@ public class LauncherApps { } /** + * Saves view capture data to the default location. + * @hide + */ + @RequiresPermission(READ_FRAME_BUFFER) + public void saveViewCaptureData() { + try { + mService.saveViewCaptureData(); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + /** * Unregister a callback, so that it won't be called when LauncherApps dumps. * @hide */ diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig index f660770d2fc8..7fd0b03b213d 100644 --- a/core/java/android/content/res/flags.aconfig +++ b/core/java/android/content/res/flags.aconfig @@ -38,7 +38,7 @@ flag { name: "nine_patch_frro" namespace: "resource_manager" description: "Feature flag for creating an frro from a 9-patch" - bug: "309232726" + bug: "296324826" } flag { diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java index 749f218b0e6a..083d49f36b03 100644 --- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java @@ -180,6 +180,16 @@ public final class CameraExtensionCharacteristics { EXTENSION_HDR, EXTENSION_NIGHT}; + /** + * List of synthetic CameraCharacteristics keys that are supported in the extensions. + */ + private static final List<CameraCharacteristics.Key> + SUPPORTED_SYNTHETIC_CAMERA_CHARACTERISTICS = + Arrays.asList( + CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES, + CameraCharacteristics.REQUEST_AVAILABLE_COLOR_SPACE_PROFILES + ); + private final Context mContext; private final String mCameraId; private final Map<String, CameraCharacteristics> mCharacteristicsMap; @@ -874,11 +884,17 @@ public final class CameraExtensionCharacteristics { Class<CameraCharacteristics.Key<?>> keyTyped = (Class<CameraCharacteristics.Key<?>>) key; - // Do not include synthetic keys. Including synthetic keys leads to undefined - // behavior. This causes inclusion of capabilities that may not be supported in - // camera extensions. ret.addAll(chars.getAvailableKeyList(CameraCharacteristics.class, keyTyped, keys, /*includeSynthetic*/ false)); + + // Add synthetic keys to the available key list if they are part of the supported + // synthetic camera characteristic key list + for (CameraCharacteristics.Key charKey : + SUPPORTED_SYNTHETIC_CAMERA_CHARACTERISTICS) { + if (chars.get(charKey) != null) { + ret.add(charKey); + } + } } } catch (RemoteException e) { Log.e(TAG, "Failed to query the extension for all available keys! Extension " @@ -990,6 +1006,7 @@ public final class CameraExtensionCharacteristics { case ImageFormat.YUV_420_888: case ImageFormat.JPEG: case ImageFormat.JPEG_R: + case ImageFormat.YCBCR_P010: break; default: throw new IllegalArgumentException("Unsupported format: " + format); @@ -1021,8 +1038,9 @@ public final class CameraExtensionCharacteristics { return generateJpegSupportedSizes( extenders.second.getSupportedPostviewResolutions(sz), streamMap); - } else if (format == ImageFormat.JPEG_R) { - // Jpeg_R/UltraHDR is currently not supported in the basic extension case + } else if (format == ImageFormat.JPEG_R || format == ImageFormat.YCBCR_P010) { + // Jpeg_R/UltraHDR + YCBCR_P010 is currently not supported in the basic + // extension case return new ArrayList<>(); } else { throw new IllegalArgumentException("Unsupported format: " + format); @@ -1118,16 +1136,16 @@ public final class CameraExtensionCharacteristics { * * <p>Device-specific extensions currently support at most three * multi-frame capture surface formats. ImageFormat.JPEG will be supported by all - * extensions while ImageFormat.YUV_420_888 and ImageFormat.JPEG_R may or may not be - * supported.</p> + * extensions while ImageFormat.YUV_420_888, ImageFormat.JPEG_R, or ImageFormat.YCBCR_P010 + * may or may not be supported.</p> * * @param extension the extension type * @param format device-specific extension output format * @return non-modifiable list of available sizes or an empty list if the format is not * supported. * @throws IllegalArgumentException in case of format different from ImageFormat.JPEG, - * ImageFormat.YUV_420_888, ImageFormat.JPEG_R; or - * unsupported extension. + * ImageFormat.YUV_420_888, ImageFormat.JPEG_R, + * ImageFormat.YCBCR_P010; or unsupported extension. */ public @NonNull List<Size> getExtensionSupportedSizes(@Extension int extension, int format) { @@ -1151,6 +1169,7 @@ public final class CameraExtensionCharacteristics { case ImageFormat.YUV_420_888: case ImageFormat.JPEG: case ImageFormat.JPEG_R: + case ImageFormat.YCBCR_P010: break; default: throw new IllegalArgumentException("Unsupported format: " + format); @@ -1183,8 +1202,9 @@ public final class CameraExtensionCharacteristics { } else { return generateSupportedSizes(null, format, streamMap); } - } else if (format == ImageFormat.JPEG_R) { - // Jpeg_R/UltraHDR is currently not supported in the basic extension case + } else if (format == ImageFormat.JPEG_R || format == ImageFormat.YCBCR_P010) { + // Jpeg_R/UltraHDR + YCBCR_P010 is currently not supported in the + // basic extension case return new ArrayList<>(); } else { throw new IllegalArgumentException("Unsupported format: " + format); @@ -1213,7 +1233,8 @@ public final class CameraExtensionCharacteristics { * @return the range of estimated minimal and maximal capture latency in milliseconds * or null if no capture latency info can be provided * @throws IllegalArgumentException in case of format different from {@link ImageFormat#JPEG}, - * {@link ImageFormat#YUV_420_888}, {@link ImageFormat#JPEG_R}; + * {@link ImageFormat#YUV_420_888}, {@link ImageFormat#JPEG_R} + * {@link ImageFormat#YCBCR_P010}; * or unsupported extension. */ public @Nullable Range<Long> getEstimatedCaptureLatencyRangeMillis(@Extension int extension, @@ -1222,6 +1243,7 @@ public final class CameraExtensionCharacteristics { case ImageFormat.YUV_420_888: case ImageFormat.JPEG: case ImageFormat.JPEG_R: + case ImageFormat.YCBCR_P010: //No op break; default: @@ -1269,8 +1291,8 @@ public final class CameraExtensionCharacteristics { // specific and cannot be estimated accurately enough. return null; } - if (format == ImageFormat.JPEG_R) { - // JpegR/UltraHDR is not supported for basic extensions + if (format == ImageFormat.JPEG_R || format == ImageFormat.YCBCR_P010) { + // JpegR/UltraHDR + YCBCR_P010 is not supported for basic extensions return null; } diff --git a/core/java/android/hardware/camera2/extension/AdvancedExtender.java b/core/java/android/hardware/camera2/extension/AdvancedExtender.java index 4895f38d7328..8fa09a802aa4 100644 --- a/core/java/android/hardware/camera2/extension/AdvancedExtender.java +++ b/core/java/android/hardware/camera2/extension/AdvancedExtender.java @@ -61,7 +61,6 @@ public abstract class AdvancedExtender { private CameraUsageTracker mCameraUsageTracker; private static final String TAG = "AdvancedExtender"; - /** * Initialize a camera extension advanced extender instance. * @@ -263,6 +262,13 @@ public abstract class AdvancedExtender { * * <p>For example, an extension may limit the zoom ratio range. In this case, an OEM can return * a new zoom ratio range for the key {@link CameraCharacteristics#CONTROL_ZOOM_RATIO_RANGE}. + * + * <p> Currently, the only synthetic keys supported for override are + * {@link CameraCharacteristics#REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES} and + * {@link CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES}. To enable them, an OEM + * should override the respective native keys + * {@link CameraCharacteristics#REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES_MAP} and + * {@link CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP}. */ @FlaggedApi(Flags.FLAG_CAMERA_EXTENSIONS_CHARACTERISTICS_GET) @NonNull diff --git a/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl b/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl index 509bcb8e3d23..5567bed7f128 100644 --- a/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl +++ b/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl @@ -27,6 +27,7 @@ parcelable CameraOutputConfig int imageFormat; int capacity; long usage; + long dynamicRangeProfile; const int TYPE_SURFACE = 0; const int TYPE_IMAGEREADER = 1; diff --git a/core/java/android/hardware/camera2/extension/CameraOutputSurface.java b/core/java/android/hardware/camera2/extension/CameraOutputSurface.java index 53f56bc9f896..001b79499b1a 100644 --- a/core/java/android/hardware/camera2/extension/CameraOutputSurface.java +++ b/core/java/android/hardware/camera2/extension/CameraOutputSurface.java @@ -133,15 +133,4 @@ public final class CameraOutputSurface { @DynamicRangeProfiles.Profile long dynamicRangeProfile) { mOutputSurface.dynamicRangeProfile = dynamicRangeProfile; } - - /** - * Set the color space. The default colorSpace - * will be - * {@link android.hardware.camera2.params.ColorSpaceProfiles.UNSPECIFIED} - * unless explicitly set using this method. - */ - @FlaggedApi(Flags.FLAG_EXTENSION_10_BIT) - public void setColorSpace(int colorSpace) { - mOutputSurface.colorSpace = colorSpace; - } } diff --git a/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl b/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl index 84ca2b63fbcf..c4f653cdfed7 100644 --- a/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl +++ b/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl @@ -25,4 +25,5 @@ parcelable CameraSessionConfig CameraMetadataNative sessionParameter; int sessionTemplateId; int sessionType; + int colorSpace; } diff --git a/core/java/android/hardware/camera2/extension/ExtensionConfiguration.java b/core/java/android/hardware/camera2/extension/ExtensionConfiguration.java index 96c88e660e10..84b7a7fc1349 100644 --- a/core/java/android/hardware/camera2/extension/ExtensionConfiguration.java +++ b/core/java/android/hardware/camera2/extension/ExtensionConfiguration.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import android.annotation.SystemApi; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.ColorSpaceProfiles; import android.os.IBinder; import com.android.internal.camera.flags.Flags; @@ -48,6 +49,7 @@ public class ExtensionConfiguration { private final int mSessionTemplateId; private final List<ExtensionOutputConfiguration> mOutputs; private final CaptureRequest mSessionParameters; + private int mColorSpace; /** * Initialize an extension configuration instance @@ -72,6 +74,18 @@ public class ExtensionConfiguration { mSessionTemplateId = sessionTemplateId; mOutputs = outputs; mSessionParameters = sessionParams; + mColorSpace = ColorSpaceProfiles.UNSPECIFIED; + } + + /** + * Set the color space using the ordinal value of a + * {@link android.graphics.ColorSpace.Named}. + * The default will be -1, indicating an unspecified ColorSpace, + * unless explicitly set using this method. + */ + @FlaggedApi(Flags.FLAG_EXTENSION_10_BIT) + public void setColorSpace(int colorSpace) { + mColorSpace = colorSpace; } @FlaggedApi(Flags.FLAG_CONCERT_MODE) @@ -84,6 +98,11 @@ public class ExtensionConfiguration { ret.sessionTemplateId = mSessionTemplateId; ret.sessionType = mSessionType; ret.outputConfigs = new ArrayList<>(mOutputs.size()); + if (Flags.extension10Bit()) { + ret.colorSpace = mColorSpace; + } else { + ret.colorSpace = ColorSpaceProfiles.UNSPECIFIED; + } for (ExtensionOutputConfiguration outputConfig : mOutputs) { ret.outputConfigs.add(outputConfig.getOutputConfig()); } diff --git a/core/java/android/hardware/camera2/extension/ExtensionOutputConfiguration.java b/core/java/android/hardware/camera2/extension/ExtensionOutputConfiguration.java index 9dc6d7bf94b3..3a67d6192f5e 100644 --- a/core/java/android/hardware/camera2/extension/ExtensionOutputConfiguration.java +++ b/core/java/android/hardware/camera2/extension/ExtensionOutputConfiguration.java @@ -20,6 +20,7 @@ import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; +import android.hardware.camera2.params.DynamicRangeProfiles; import com.android.internal.camera.flags.Flags; @@ -79,6 +80,11 @@ public class ExtensionOutputConfiguration { config.outputId = new OutputConfigId(); config.outputId.id = mOutputConfigId; config.surfaceGroupId = mSurfaceGroupId; + if (Flags.extension10Bit()) { + config.dynamicRangeProfile = surface.getDynamicRangeProfile(); + } else { + config.dynamicRangeProfile = DynamicRangeProfiles.STANDARD; + } } @Nullable CameraOutputConfig getOutputConfig() { diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java index a7d6caf9d9df..5b7f8bb00f25 100644 --- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.content.Context; +import android.graphics.ColorSpace; import android.graphics.ImageFormat; import android.graphics.SurfaceTexture; import android.hardware.SyncFence; @@ -49,6 +50,7 @@ import android.hardware.camera2.extension.ParcelCaptureResult; import android.hardware.camera2.extension.ParcelImage; import android.hardware.camera2.extension.ParcelTotalCaptureResult; import android.hardware.camera2.extension.Request; +import android.hardware.camera2.params.ColorSpaceProfiles; import android.hardware.camera2.params.DynamicRangeProfiles; import android.hardware.camera2.params.ExtensionSessionConfiguration; import android.hardware.camera2.params.OutputConfiguration; @@ -62,6 +64,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.RemoteException; +import android.util.IntArray; import android.util.Log; import android.util.Size; import android.view.Surface; @@ -97,6 +100,9 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes private Surface mClientRepeatingRequestSurface; private Surface mClientCaptureSurface; private Surface mClientPostviewSurface; + private OutputConfiguration mClientRepeatingRequestOutputConfig; + private OutputConfiguration mClientCaptureOutputConfig; + private OutputConfiguration mClientPostviewOutputConfig; private CameraCaptureSession mCaptureSession = null; private ISessionProcessorImpl mSessionProcessor = null; private final InitializeSessionHandler mInitializeHandler; @@ -142,8 +148,19 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes for (OutputConfiguration c : config.getOutputConfigurations()) { if (c.getDynamicRangeProfile() != DynamicRangeProfiles.STANDARD) { - throw new IllegalArgumentException("Unsupported dynamic range profile: " + - c.getDynamicRangeProfile()); + if (Flags.extension10Bit() && Flags.cameraExtensionsCharacteristicsGet()) { + DynamicRangeProfiles dynamicProfiles = extensionChars.get( + config.getExtension(), + CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES); + if (dynamicProfiles == null || !dynamicProfiles.getSupportedProfiles() + .contains(c.getDynamicRangeProfile())) { + throw new IllegalArgumentException("Unsupported dynamic range profile: " + + c.getDynamicRangeProfile()); + } + } else { + throw new IllegalArgumentException("Unsupported dynamic range profile: " + + c.getDynamicRangeProfile()); + } } if (c.getStreamUseCase() != CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT) { @@ -157,12 +174,26 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes config.getExtension(), SurfaceTexture.class); Surface repeatingRequestSurface = CameraExtensionUtils.getRepeatingRequestSurface( config.getOutputConfigurations(), supportedPreviewSizes); + OutputConfiguration repeatingRequestOutputConfig = null; if (repeatingRequestSurface != null) { + for (OutputConfiguration outputConfig : config.getOutputConfigurations()) { + if (outputConfig.getSurface() == repeatingRequestSurface) { + repeatingRequestOutputConfig = outputConfig; + } + } suitableSurfaceCount++; } HashMap<Integer, List<Size>> supportedCaptureSizes = new HashMap<>(); - for (int format : CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS) { + + IntArray supportedCaptureOutputFormats = + new IntArray(CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS.length); + supportedCaptureOutputFormats.addAll( + CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS); + if (Flags.extension10Bit()) { + supportedCaptureOutputFormats.add(ImageFormat.YCBCR_P010); + } + for (int format : supportedCaptureOutputFormats.toArray()) { List<Size> supportedSizes = extensionChars.getExtensionSupportedSizes( config.getExtension(), format); if (supportedSizes != null) { @@ -171,7 +202,13 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes } Surface burstCaptureSurface = CameraExtensionUtils.getBurstCaptureSurface( config.getOutputConfigurations(), supportedCaptureSizes); + OutputConfiguration burstCaptureOutputConfig = null; if (burstCaptureSurface != null) { + for (OutputConfiguration outputConfig : config.getOutputConfigurations()) { + if (outputConfig.getSurface() == burstCaptureSurface) { + burstCaptureOutputConfig = outputConfig; + } + } suitableSurfaceCount++; } @@ -180,13 +217,14 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes } Surface postviewSurface = null; + OutputConfiguration postviewOutputConfig = config.getPostviewOutputConfiguration(); if (burstCaptureSurface != null && config.getPostviewOutputConfiguration() != null) { CameraExtensionUtils.SurfaceInfo burstCaptureSurfaceInfo = CameraExtensionUtils.querySurface(burstCaptureSurface); Size burstCaptureSurfaceSize = new Size(burstCaptureSurfaceInfo.mWidth, burstCaptureSurfaceInfo.mHeight); HashMap<Integer, List<Size>> supportedPostviewSizes = new HashMap<>(); - for (int format : CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS) { + for (int format : supportedCaptureOutputFormats.toArray()) { List<Size> supportedSizesPostview = extensionChars.getPostviewSupportedSizes( config.getExtension(), burstCaptureSurfaceSize, format); if (supportedSizesPostview != null) { @@ -207,8 +245,8 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes extender.init(cameraId, characteristicsMapNative); CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(ctx, - extender, cameraDevice, characteristicsMapNative, repeatingRequestSurface, - burstCaptureSurface, postviewSurface, config.getStateCallback(), + extender, cameraDevice, characteristicsMapNative, repeatingRequestOutputConfig, + burstCaptureOutputConfig, postviewOutputConfig, config.getStateCallback(), config.getExecutor(), sessionId, token, config.getExtension()); ret.mStatsAggregator.setClientName(ctx.getOpPackageName()); @@ -223,8 +261,9 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes @NonNull IAdvancedExtenderImpl extender, @NonNull CameraDeviceImpl cameraDevice, Map<String, CameraMetadataNative> characteristicsMap, - @Nullable Surface repeatingRequestSurface, @Nullable Surface burstCaptureSurface, - @Nullable Surface postviewSurface, + @Nullable OutputConfiguration repeatingRequestOutputConfig, + @Nullable OutputConfiguration burstCaptureOutputConfig, + @Nullable OutputConfiguration postviewOutputConfig, @NonNull StateCallback callback, @NonNull Executor executor, int sessionId, @NonNull IBinder token, @@ -235,9 +274,18 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes mCharacteristicsMap = characteristicsMap; mCallbacks = callback; mExecutor = executor; - mClientRepeatingRequestSurface = repeatingRequestSurface; - mClientCaptureSurface = burstCaptureSurface; - mClientPostviewSurface = postviewSurface; + mClientRepeatingRequestOutputConfig = repeatingRequestOutputConfig; + mClientCaptureOutputConfig = burstCaptureOutputConfig; + mClientPostviewOutputConfig = postviewOutputConfig; + if (repeatingRequestOutputConfig != null) { + mClientRepeatingRequestSurface = repeatingRequestOutputConfig.getSurface(); + } + if (burstCaptureOutputConfig != null) { + mClientCaptureSurface = burstCaptureOutputConfig.getSurface(); + } + if (postviewOutputConfig != null) { + mClientPostviewSurface = postviewOutputConfig.getSurface(); + } mHandlerThread = new HandlerThread(TAG); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); @@ -262,9 +310,9 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes return; } - OutputSurface previewSurface = initializeParcelable(mClientRepeatingRequestSurface); - OutputSurface captureSurface = initializeParcelable(mClientCaptureSurface); - OutputSurface postviewSurface = initializeParcelable(mClientPostviewSurface); + OutputSurface previewSurface = initializeParcelable(mClientRepeatingRequestOutputConfig); + OutputSurface captureSurface = initializeParcelable(mClientCaptureOutputConfig); + OutputSurface postviewSurface = initializeParcelable(mClientPostviewOutputConfig); mSessionProcessor = mAdvancedExtender.getSessionProcessor(); CameraSessionConfig sessionConfig = mSessionProcessor.initSession(mToken, @@ -300,6 +348,7 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes cameraOutput.setTimestampBase(OutputConfiguration.TIMESTAMP_BASE_SENSOR); cameraOutput.setReadoutTimestampEnabled(false); cameraOutput.setPhysicalCameraId(output.physicalCameraId); + cameraOutput.setDynamicRangeProfile(output.dynamicRangeProfile); outputList.add(cameraOutput); mCameraConfigMap.put(cameraOutput.getSurface(), output); } @@ -314,7 +363,10 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes SessionConfiguration sessionConfiguration = new SessionConfiguration(sessionType, outputList, new CameraExtensionUtils.HandlerExecutor(mHandler), new SessionStateHandler()); - + if (sessionConfig.colorSpace != ColorSpaceProfiles.UNSPECIFIED) { + sessionConfiguration.setColorSpace( + ColorSpace.Named.values()[sessionConfig.colorSpace]); + } if ((sessionConfig.sessionParameter != null) && (!sessionConfig.sessionParameter.isEmpty())) { CaptureRequest.Builder requestBuilder = mCameraDevice.createCaptureRequest( @@ -362,21 +414,38 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes return ret; } - private static OutputSurface initializeParcelable(Surface s) { + private static OutputSurface initializeParcelable(OutputConfiguration o) { OutputSurface ret = new OutputSurface(); - if (s != null) { + + if (o != null && o.getSurface() != null) { + Surface s = o.getSurface(); ret.surface = s; ret.size = new android.hardware.camera2.extension.Size(); Size surfaceSize = SurfaceUtils.getSurfaceSize(s); ret.size.width = surfaceSize.getWidth(); ret.size.height = surfaceSize.getHeight(); ret.imageFormat = SurfaceUtils.getSurfaceFormat(s); + + if (Flags.extension10Bit()) { + ret.dynamicRangeProfile = o.getDynamicRangeProfile(); + ColorSpace colorSpace = o.getColorSpace(); + if (colorSpace != null) { + ret.colorSpace = colorSpace.getId(); + } else { + ret.colorSpace = ColorSpaceProfiles.UNSPECIFIED; + } + } else { + ret.dynamicRangeProfile = DynamicRangeProfiles.STANDARD; + ret.colorSpace = ColorSpaceProfiles.UNSPECIFIED; + } } else { ret.surface = null; ret.size = new android.hardware.camera2.extension.Size(); ret.size.width = -1; ret.size.height = -1; ret.imageFormat = ImageFormat.UNKNOWN; + ret.dynamicRangeProfile = DynamicRangeProfiles.STANDARD; + ret.colorSpace = ColorSpaceProfiles.UNSPECIFIED; } return ret; diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java index 725b4139bb95..5b32f33777fa 100644 --- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java @@ -58,12 +58,15 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.RemoteException; +import android.util.IntArray; import android.util.Log; import android.util.LongSparseArray; import android.util.Pair; import android.util.Size; import android.view.Surface; +import com.android.internal.camera.flags.Flags; + import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; @@ -183,7 +186,14 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { } HashMap<Integer, List<Size>> supportedCaptureSizes = new HashMap<>(); - for (int format : CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS) { + IntArray supportedCaptureOutputFormats = + new IntArray(CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS.length); + supportedCaptureOutputFormats.addAll( + CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS); + if (Flags.extension10Bit()) { + supportedCaptureOutputFormats.add(ImageFormat.YCBCR_P010); + } + for (int format : supportedCaptureOutputFormats.toArray()) { List<Size> supportedSizes = extensionChars.getExtensionSupportedSizes( config.getExtension(), format); if (supportedSizes != null) { @@ -207,7 +217,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { Size burstCaptureSurfaceSize = new Size(burstCaptureSurfaceInfo.mWidth, burstCaptureSurfaceInfo.mHeight); HashMap<Integer, List<Size>> supportedPostviewSizes = new HashMap<>(); - for (int format : CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS) { + for (int format : supportedCaptureOutputFormats.toArray()) { List<Size> supportedSizesPostview = extensionChars.getPostviewSupportedSizes( config.getExtension(), burstCaptureSurfaceSize, format); if (supportedSizesPostview != null) { diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java b/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java index a8066aa74f95..f0c6e2e4e123 100644 --- a/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java +++ b/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java @@ -29,10 +29,13 @@ import android.hardware.camera2.utils.SurfaceUtils; import android.media.Image; import android.media.ImageWriter; import android.os.Handler; +import android.util.IntArray; import android.util.Log; import android.util.Size; import android.view.Surface; +import com.android.internal.camera.flags.Flags; + import java.util.HashMap; import java.util.List; import java.util.Map; @@ -130,9 +133,16 @@ public final class CameraExtensionUtils { public static Surface getBurstCaptureSurface( @NonNull List<OutputConfiguration> outputConfigs, @NonNull HashMap<Integer, List<Size>> supportedCaptureSizes) { + IntArray supportedCaptureOutputFormats = + new IntArray(CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS.length); + supportedCaptureOutputFormats.addAll( + CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS); + if (Flags.extension10Bit()) { + supportedCaptureOutputFormats.add(ImageFormat.YCBCR_P010); + } for (OutputConfiguration config : outputConfigs) { SurfaceInfo surfaceInfo = querySurface(config.getSurface()); - for (int supportedFormat : SUPPORTED_CAPTURE_OUTPUT_FORMATS) { + for (int supportedFormat : supportedCaptureOutputFormats.toArray()) { if (surfaceInfo.mFormat == supportedFormat) { Size captureSize = new Size(surfaceInfo.mWidth, surfaceInfo.mHeight); if (supportedCaptureSizes.containsKey(supportedFormat)) { diff --git a/core/java/android/hardware/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java index 905d911248ca..76888f338615 100644 --- a/core/java/android/hardware/devicestate/DeviceState.java +++ b/core/java/android/hardware/devicestate/DeviceState.java @@ -543,11 +543,13 @@ public final class DeviceState { int identifier = source.readInt(); String name = source.readString8(); ArraySet<@DeviceStateProperties Integer> systemProperties = new ArraySet<>(); - for (int i = 0; i < source.readInt(); i++) { + int systemPropertySize = source.readInt(); + for (int i = 0; i < systemPropertySize; i++) { systemProperties.add(source.readInt()); } ArraySet<@DeviceStateProperties Integer> physicalProperties = new ArraySet<>(); - for (int j = 0; j < source.readInt(); j++) { + int physicalPropertySize = source.readInt(); + for (int j = 0; j < physicalPropertySize; j++) { physicalProperties.add(source.readInt()); } return new DeviceState.Configuration(identifier, name, systemProperties, diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java index a3a2a2e6fd16..c5167dbc7d4c 100644 --- a/core/java/android/hardware/radio/ProgramList.java +++ b/core/java/android/hardware/radio/ProgramList.java @@ -304,7 +304,11 @@ public final class ProgramList implements AutoCloseable { * * @param id primary identifier of a program to fetch * @return the program info, or null if there is no such program on the list + * + * @deprecated Use {@link #getProgramInfos(ProgramSelector.Identifier)} to get all programs + * with the given primary identifier */ + @Deprecated public @Nullable RadioManager.ProgramInfo get(@NonNull ProgramSelector.Identifier id) { Map<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries; synchronized (mLock) { diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java index a968c6f0ad05..0740374ad8e2 100644 --- a/core/java/android/hardware/radio/ProgramSelector.java +++ b/core/java/android/hardware/radio/ProgramSelector.java @@ -312,15 +312,23 @@ public final class ProgramSelector implements Parcelable { public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10; /** * 1: AM, 2:FM + * @deprecated use {@link #IDENTIFIER_TYPE_DRMO_FREQUENCY} instead */ + @Deprecated public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11; /** * 32bit primary identifier for SiriusXM Satellite Radio. + * + * @deprecated SiriusXM Satellite Radio is not supported */ + @Deprecated public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12; /** * 0-999 range + * + * @deprecated SiriusXM Satellite Radio is not supported */ + @Deprecated public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13; /** * 44bit compound primary identifier for Digital Audio Broadcasting and diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java index 61cf8901c454..da6c68646820 100644 --- a/core/java/android/hardware/radio/RadioManager.java +++ b/core/java/android/hardware/radio/RadioManager.java @@ -166,7 +166,12 @@ public class RadioManager { * analog handover state managed from the HAL implementation side. * * <p>Some radio technologies may not support this, i.e. DAB. + * + * @deprecated Use {@link #CONFIG_FORCE_ANALOG_FM} instead. If {@link #CONFIG_FORCE_ANALOG_FM} + * is supported in HAL, {@link RadioTuner#setConfigFlag} and {@link RadioTuner#isConfigFlagSet} + * with CONFIG_FORCE_ANALOG will set/get the value of {@link #CONFIG_FORCE_ANALOG_FM}. */ + @Deprecated public static final int CONFIG_FORCE_ANALOG = 2; /** * Forces the digital playback for the supporting radio technology. diff --git a/core/java/android/service/dreams/IDreamManager.aidl b/core/java/android/service/dreams/IDreamManager.aidl index 09e6b5de8a3c..c489c5808728 100644 --- a/core/java/android/service/dreams/IDreamManager.aidl +++ b/core/java/android/service/dreams/IDreamManager.aidl @@ -38,7 +38,6 @@ interface IDreamManager { boolean isDreaming(); @UnsupportedAppUsage boolean isDreamingOrInPreview(); - @UnsupportedAppUsage boolean canStartDreaming(boolean isScreenOn); void finishSelf(in IBinder token, boolean immediate); void startDozing(in IBinder token, int screenState, int screenBrightness); diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index ce74848705e4..82e613e18d41 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -77,4 +77,12 @@ flag { description: "Properties to allow apps and activities to opt-in to cover display rendering" bug: "312530526" is_fixed_read_only: true +} + +flag { + namespace: "windowing_sdk" + name: "enable_wm_extensions_for_all_flag" + description: "Whether to enable WM Extensions for all devices" + bug: "306666082" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/core/java/com/android/internal/compat/Android.bp b/core/java/com/android/internal/compat/Android.bp new file mode 100644 index 000000000000..9ff05a6589b7 --- /dev/null +++ b/core/java/com/android/internal/compat/Android.bp @@ -0,0 +1,7 @@ +aconfig_declarations { + name: "compat_logging_flags", + package: "com.android.internal.compat.flags", + srcs: [ + "compat_logging_flags.aconfig", + ], +} diff --git a/core/java/com/android/internal/compat/ChangeReporter.java b/core/java/com/android/internal/compat/ChangeReporter.java index b9d3df678a91..6ff546fd77f8 100644 --- a/core/java/com/android/internal/compat/ChangeReporter.java +++ b/core/java/com/android/internal/compat/ChangeReporter.java @@ -24,6 +24,7 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.compat.flags.Flags; import com.android.internal.util.FrameworkStatsLog; import java.lang.annotation.Retention; @@ -40,7 +41,7 @@ import java.util.Set; * @hide */ public final class ChangeReporter { - private static final String TAG = "CompatibilityChangeReporter"; + private static final String TAG = "CompatChangeReporter"; private int mSource; private static final class ChangeReport { @@ -84,22 +85,37 @@ public final class ChangeReporter { * Report the change to stats log and to the debug log if the change was not previously * logged already. * - * @param uid affected by the change - * @param changeId the reported change id - * @param state of the reported change - enabled/disabled/only logged + * @param uid affected by the change + * @param changeId the reported change id + * @param state of the reported change - enabled/disabled/only logged + * @param isLoggableBySdk whether debug logging is allowed for this change based on target + * SDK version. This is combined with other logic to determine whether to + * actually log. If the sdk version does not matter, should be true. */ - public void reportChange(int uid, long changeId, int state) { + public void reportChange(int uid, long changeId, int state, boolean isLoggableBySdk) { if (shouldWriteToStatsLog(uid, changeId, state)) { FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPATIBILITY_CHANGE_REPORTED, uid, changeId, state, mSource); } - if (shouldWriteToDebug(uid, changeId, state)) { + if (shouldWriteToDebug(uid, changeId, state, isLoggableBySdk)) { debugLog(uid, changeId, state); } markAsReported(uid, new ChangeReport(changeId, state)); } /** + * Report the change to stats log and to the debug log if the change was not previously + * logged already. + * + * @param uid affected by the change + * @param changeId the reported change id + * @param state of the reported change - enabled/disabled/only logged + */ + public void reportChange(int uid, long changeId, int state) { + reportChange(uid, changeId, state, true); + } + + /** * Start logging all the time to logcat. */ public void startDebugLogAll() { @@ -130,14 +146,43 @@ public final class ChangeReporter { /** * Returns whether the next report should be logged to logcat. * - * @param uid affected by the change - * @param changeId the reported change id - * @param state of the reported change - enabled/disabled/only logged + * @param uid affected by the change + * @param changeId the reported change id + * @param state of the reported change - enabled/disabled/only logged + * @param isLoggableBySdk whether debug logging is allowed for this change based on target + * SDK version. This is combined with other logic to determine whether to + * actually log. If the sdk version does not matter, should be true. + * @return true if the report should be logged + */ + @VisibleForTesting + public boolean shouldWriteToDebug( + int uid, long changeId, int state, boolean isLoggableBySdk) { + // If log all bit is on, always return true. + if (mDebugLogAll) return true; + // If the change has already been reported, do not write. + if (isAlreadyReported(uid, new ChangeReport(changeId, state))) return false; + + // If the flag is turned off or the TAG's logging is forced to debug level with + // `adb setprop log.tag.CompatChangeReporter=DEBUG`, write to debug since the above checks + // have already passed. + boolean skipLoggingFlag = Flags.skipOldAndDisabledCompatLogging(); + if (!skipLoggingFlag || Log.isLoggable(TAG, Log.DEBUG)) return true; + + // Log if the change is enabled and targets the latest sdk version. + return isLoggableBySdk && state != STATE_DISABLED; + } + + /** + * Returns whether the next report should be logged to logcat. + * + * @param uid affected by the change + * @param changeId the reported change id + * @param state of the reported change - enabled/disabled/only logged * @return true if the report should be logged */ @VisibleForTesting public boolean shouldWriteToDebug(int uid, long changeId, int state) { - return mDebugLogAll || !isAlreadyReported(uid, new ChangeReport(changeId, state)); + return shouldWriteToDebug(uid, changeId, state, true); } private boolean isAlreadyReported(int uid, ChangeReport report) { diff --git a/core/java/com/android/internal/compat/compat_logging_flags.aconfig b/core/java/com/android/internal/compat/compat_logging_flags.aconfig new file mode 100644 index 000000000000..fab3856daca7 --- /dev/null +++ b/core/java/com/android/internal/compat/compat_logging_flags.aconfig @@ -0,0 +1,9 @@ +package: "com.android.internal.compat.flags" + +flag { + name: "skip_old_and_disabled_compat_logging" + namespace: "platform_compat" + description: "Feature flag for skipping debug logging for changes that do not target the latest sdk or are disabled" + bug: "323949942" + is_fixed_read_only: true +}
\ No newline at end of file diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java index cbe070048811..d4dcec948e31 100644 --- a/core/java/com/android/internal/os/ZygoteConnection.java +++ b/core/java/com/android/internal/os/ZygoteConnection.java @@ -93,6 +93,9 @@ class ZygoteConnection { throw ex; } + if (peer.getUid() != Process.SYSTEM_UID) { + throw new ZygoteSecurityException("Only system UID is allowed to connect to Zygote."); + } isEof = false; } diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index dc3b5a8846cf..025703364f0f 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -33,8 +33,10 @@ import com.android.internal.inputmethod.IRemoteInputConnection; import com.android.internal.inputmethod.InputBindResult; /** - * Public interface to the global input method manager, used by all client - * applications. + * Public interface to the global input method manager, used by all client applications. + * + * When adding new methods, make sure the associated user can be inferred from the arguments. + * Consider passing the associated userId when not already passing a display id or a window token. */ interface IInputMethodManager { void addClient(in IInputMethodClient client, in IRemoteInputConnection inputmethod, diff --git a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp index 54c4cd50a902..e0cc055a62a6 100644 --- a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp +++ b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp @@ -354,6 +354,18 @@ jstring com_android_internal_os_ZygoteCommandBuffer_nativeNextArg(JNIEnv* env, j return result; } +static uid_t getSocketPeerUid(int socket, const std::function<void(const std::string&)>& fail_fn) { + struct ucred credentials; + socklen_t cred_size = sizeof credentials; + if (getsockopt(socket, SOL_SOCKET, SO_PEERCRED, &credentials, &cred_size) == -1 + || cred_size != sizeof credentials) { + fail_fn(CREATE_ERROR("Failed to get socket credentials, %s", + strerror(errno))); + } + + return credentials.uid; +} + // Read all lines from the current command into the buffer, and then reset the buffer, so // we will start reading again at the beginning of the command, starting with the argument // count. And we don't need access to the fd to do so. @@ -413,19 +425,12 @@ jboolean com_android_internal_os_ZygoteCommandBuffer_nativeForkRepeatedly( fail_fn_z("Failed to retrieve session socket timeout"); } - struct ucred credentials; - socklen_t cred_size = sizeof credentials; - if (getsockopt(n_buffer->getFd(), SOL_SOCKET, SO_PEERCRED, &credentials, &cred_size) == -1 - || cred_size != sizeof credentials) { - fail_fn_1(CREATE_ERROR("ForkRepeatedly failed to get initial credentials, %s", - strerror(errno))); + uid_t peerUid = getSocketPeerUid(session_socket, fail_fn_1); + if (peerUid != static_cast<uid_t>(expected_uid)) { + return JNI_FALSE; } - bool first_time = true; do { - if (credentials.uid != static_cast<uid_t>(expected_uid)) { - return JNI_FALSE; - } n_buffer->readAllLines(first_time ? fail_fn_1 : fail_fn_n); n_buffer->reset(); int pid = zygote::forkApp(env, /* no pipe FDs */ -1, -1, session_socket_fds, @@ -453,6 +458,7 @@ jboolean com_android_internal_os_ZygoteCommandBuffer_nativeForkRepeatedly( } } for (;;) { + bool valid_session_socket = true; // Clear buffer and get count from next command. n_buffer->clear(); // Poll isn't strictly necessary for now. But without it, disconnect is hard to detect. @@ -463,25 +469,50 @@ jboolean com_android_internal_os_ZygoteCommandBuffer_nativeForkRepeatedly( if ((fd_structs[SESSION_IDX].revents & POLLIN) != 0) { if (n_buffer->getCount(fail_fn_z) != 0) { break; - } // else disconnected; + } else { + // Session socket was disconnected + valid_session_socket = false; + close(session_socket); + } } else if (poll_res == 0 || (fd_structs[ZYGOTE_IDX].revents & POLLIN) == 0) { fail_fn_z( CREATE_ERROR("Poll returned with no descriptors ready! Poll returned %d", poll_res)); } - // We've now seen either a disconnect or connect request. - close(session_socket); - int new_fd = TEMP_FAILURE_RETRY(accept(zygote_socket_fd, nullptr, nullptr)); + int new_fd = -1; + do { + // We've now seen either a disconnect or connect request. + new_fd = TEMP_FAILURE_RETRY(accept(zygote_socket_fd, nullptr, nullptr)); + if (new_fd == -1) { + fail_fn_z(CREATE_ERROR("Accept(%d) failed: %s", zygote_socket_fd, strerror(errno))); + } + uid_t newPeerUid = getSocketPeerUid(new_fd, fail_fn_1); + if (newPeerUid != static_cast<uid_t>(expected_uid)) { + ALOGW("Dropping new connection with a mismatched uid %d\n", newPeerUid); + close(new_fd); + new_fd = -1; + } else { + // If we still have a valid session socket, close it now + if (valid_session_socket) { + close(session_socket); + } + valid_session_socket = true; + } + } while (!valid_session_socket); + + // At this point we either have a valid new connection (new_fd > 0), or + // an existing session socket we can poll on if (new_fd == -1) { - fail_fn_z(CREATE_ERROR("Accept(%d) failed: %s", zygote_socket_fd, strerror(errno))); + // The new connection wasn't valid, and we still have an old one; retry polling + continue; } if (new_fd != session_socket) { - // Move new_fd back to the old value, so that we don't have to change Java-level data - // structures to reflect a change. This implicitly closes the old one. - if (TEMP_FAILURE_RETRY(dup2(new_fd, session_socket)) != session_socket) { - fail_fn_z(CREATE_ERROR("Failed to move fd %d to %d: %s", - new_fd, session_socket, strerror(errno))); - } - close(new_fd); // On Linux, fd is closed even if EINTR is returned. + // Move new_fd back to the old value, so that we don't have to change Java-level data + // structures to reflect a change. This implicitly closes the old one. + if (TEMP_FAILURE_RETRY(dup2(new_fd, session_socket)) != session_socket) { + fail_fn_z(CREATE_ERROR("Failed to move fd %d to %d: %s", + new_fd, session_socket, strerror(errno))); + } + close(new_fd); // On Linux, fd is closed even if EINTR is returned. } // If we ever return, we effectively reuse the old Java ZygoteConnection. // None of its state needs to change. @@ -493,13 +524,6 @@ jboolean com_android_internal_os_ZygoteCommandBuffer_nativeForkRepeatedly( fail_fn_z(CREATE_ERROR("Failed to set send timeout for socket %d: %s", session_socket, strerror(errno))); } - if (getsockopt(session_socket, SOL_SOCKET, SO_PEERCRED, &credentials, &cred_size) == -1) { - fail_fn_z(CREATE_ERROR("ForkMany failed to get credentials: %s", strerror(errno))); - } - if (cred_size != sizeof credentials) { - fail_fn_z(CREATE_ERROR("ForkMany credential size = %d, should be %d", - cred_size, static_cast<int>(sizeof credentials))); - } } first_time = false; } while (n_buffer->isSimpleForkCommand(minUid, fail_fn_n)); diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java index a0346538ddc5..10ac05d66c62 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java @@ -88,13 +88,6 @@ public final class RadioModuleTest { } @Test - public void setInternalHalCallback_callbackSetInHal() throws Exception { - mRadioModule.setInternalHalCallback(); - - verify(mBroadcastRadioMock).setTunerCallback(any()); - } - - @Test public void getImage_withValidIdFromRadioModule() { int imageId = 1; diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java index 262f167078e2..755bcdb7df20 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java @@ -192,66 +192,6 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { mHalTunerCallback = (ITunerCallback) invocation.getArguments()[0]; return null; }).when(mBroadcastRadioMock).setTunerCallback(any()); - mRadioModule.setInternalHalCallback(); - - doAnswer(invocation -> { - android.hardware.broadcastradio.ProgramSelector halSel = - (android.hardware.broadcastradio.ProgramSelector) invocation.getArguments()[0]; - mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo(halSel, SIGNAL_QUALITY); - if (halSel.primaryId.type != IdentifierType.AMFM_FREQUENCY_KHZ) { - throw new ServiceSpecificException(Result.NOT_SUPPORTED); - } - mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo); - return Result.OK; - }).when(mBroadcastRadioMock).tune(any()); - - doAnswer(invocation -> { - if ((boolean) invocation.getArguments()[0]) { - mHalCurrentInfo.selector.primaryId.value += AM_FM_FREQUENCY_SPACING; - } else { - mHalCurrentInfo.selector.primaryId.value -= AM_FM_FREQUENCY_SPACING; - } - mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId; - mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId; - mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo); - return Result.OK; - }).when(mBroadcastRadioMock).step(anyBoolean()); - - doAnswer(invocation -> { - if (mHalCurrentInfo == null) { - android.hardware.broadcastradio.ProgramSelector placeHolderSelector = - AidlTestUtils.makeHalFmSelector(/* freq= */ 97300); - - mHalTunerCallback.onTuneFailed(Result.TIMEOUT, placeHolderSelector); - return Result.OK; - } - mHalCurrentInfo.selector.primaryId.value = getSeekFrequency( - mHalCurrentInfo.selector.primaryId.value, - !(boolean) invocation.getArguments()[0]); - mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId; - mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId; - mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo); - return Result.OK; - }).when(mBroadcastRadioMock).seek(anyBoolean(), anyBoolean()); - - doReturn(null).when(mBroadcastRadioMock).getImage(anyInt()); - - doAnswer(invocation -> { - int configFlag = (int) invocation.getArguments()[0]; - if (configFlag == UNSUPPORTED_CONFIG_FLAG) { - throw new ServiceSpecificException(Result.NOT_SUPPORTED); - } - return mHalConfigMap.getOrDefault(configFlag, false); - }).when(mBroadcastRadioMock).isConfigFlagSet(anyInt()); - - doAnswer(invocation -> { - int configFlag = (int) invocation.getArguments()[0]; - if (configFlag == UNSUPPORTED_CONFIG_FLAG) { - throw new ServiceSpecificException(Result.NOT_SUPPORTED); - } - mHalConfigMap.put(configFlag, (boolean) invocation.getArguments()[1]); - return null; - }).when(mBroadcastRadioMock).setConfigFlag(anyInt(), anyBoolean()); } @After @@ -330,6 +270,7 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { expect.withMessage("Close state of broadcast radio service session") .that(mTunerSessions[0].isClosed()).isTrue(); + verify(mBroadcastRadioMock).unsetTunerCallback(); } @Test @@ -351,6 +292,7 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { .that(mTunerSessions[index].isClosed()).isFalse(); } } + verify(mBroadcastRadioMock, never()).unsetTunerCallback(); } @Test @@ -378,6 +320,7 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { expect.withMessage("Close state of broadcast radio service session of index %s", index) .that(mTunerSessions[index].isClosed()).isTrue(); } + verify(mBroadcastRadioMock).unsetTunerCallback(); } @Test @@ -1295,6 +1238,71 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { mAidlTunerCallbackMocks[index] = mock(android.hardware.radio.ITunerCallback.class); mTunerSessions[index] = mRadioModule.openSession(mAidlTunerCallbackMocks[index]); } + setupMockedHalTunerSession(); + } + + private void setupMockedHalTunerSession() throws Exception { + expect.withMessage("Registered HAL tuner callback").that(mHalTunerCallback) + .isNotNull(); + + doAnswer(invocation -> { + android.hardware.broadcastradio.ProgramSelector halSel = + (android.hardware.broadcastradio.ProgramSelector) invocation.getArguments()[0]; + mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo(halSel, SIGNAL_QUALITY); + if (halSel.primaryId.type != IdentifierType.AMFM_FREQUENCY_KHZ) { + throw new ServiceSpecificException(Result.NOT_SUPPORTED); + } + mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo); + return Result.OK; + }).when(mBroadcastRadioMock).tune(any()); + + doAnswer(invocation -> { + if ((boolean) invocation.getArguments()[0]) { + mHalCurrentInfo.selector.primaryId.value += AM_FM_FREQUENCY_SPACING; + } else { + mHalCurrentInfo.selector.primaryId.value -= AM_FM_FREQUENCY_SPACING; + } + mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId; + mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId; + mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo); + return Result.OK; + }).when(mBroadcastRadioMock).step(anyBoolean()); + + doAnswer(invocation -> { + if (mHalCurrentInfo == null) { + android.hardware.broadcastradio.ProgramSelector placeHolderSelector = + AidlTestUtils.makeHalFmSelector(/* freq= */ 97300); + + mHalTunerCallback.onTuneFailed(Result.TIMEOUT, placeHolderSelector); + return Result.OK; + } + mHalCurrentInfo.selector.primaryId.value = getSeekFrequency( + mHalCurrentInfo.selector.primaryId.value, + !(boolean) invocation.getArguments()[0]); + mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId; + mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId; + mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo); + return Result.OK; + }).when(mBroadcastRadioMock).seek(anyBoolean(), anyBoolean()); + + doReturn(null).when(mBroadcastRadioMock).getImage(anyInt()); + + doAnswer(invocation -> { + int configFlag = (int) invocation.getArguments()[0]; + if (configFlag == UNSUPPORTED_CONFIG_FLAG) { + throw new ServiceSpecificException(Result.NOT_SUPPORTED); + } + return mHalConfigMap.getOrDefault(configFlag, false); + }).when(mBroadcastRadioMock).isConfigFlagSet(anyInt()); + + doAnswer(invocation -> { + int configFlag = (int) invocation.getArguments()[0]; + if (configFlag == UNSUPPORTED_CONFIG_FLAG) { + throw new ServiceSpecificException(Result.NOT_SUPPORTED); + } + mHalConfigMap.put(configFlag, (boolean) invocation.getArguments()[1]); + return null; + }).when(mBroadcastRadioMock).setConfigFlag(anyInt(), anyBoolean()); } private long getSeekFrequency(long currentFrequency, boolean seekDown) { diff --git a/core/tests/PlatformCompatFramework/Android.bp b/core/tests/PlatformCompatFramework/Android.bp index 95e23ad396af..2621d280bd9d 100644 --- a/core/tests/PlatformCompatFramework/Android.bp +++ b/core/tests/PlatformCompatFramework/Android.bp @@ -18,6 +18,7 @@ android_test { static_libs: [ "junit", "androidx.test.rules", + "flag-junit", ], platform_apis: true, } diff --git a/core/tests/PlatformCompatFramework/src/com/android/internal/compat/ChangeReporterTest.java b/core/tests/PlatformCompatFramework/src/com/android/internal/compat/ChangeReporterTest.java index a052543c6446..12a42f975cd7 100644 --- a/core/tests/PlatformCompatFramework/src/com/android/internal/compat/ChangeReporterTest.java +++ b/core/tests/PlatformCompatFramework/src/com/android/internal/compat/ChangeReporterTest.java @@ -19,9 +19,17 @@ package com.android.internal.compat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import android.platform.test.flag.junit.SetFlagsRule; + +import com.android.internal.compat.flags.Flags; + +import org.junit.Rule; import org.junit.Test; public class ChangeReporterTest { + + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Test public void testStatsLogOnce() { ChangeReporter reporter = new ChangeReporter(ChangeReporter.SOURCE_UNKNOWN_SOURCE); @@ -63,7 +71,7 @@ public class ChangeReporterTest { ChangeReporter reporter = new ChangeReporter(ChangeReporter.SOURCE_UNKNOWN_SOURCE); int myUid = 1022, otherUid = 1023; long myChangeId = 500L, otherChangeId = 600L; - int myState = ChangeReporter.STATE_ENABLED, otherState = ChangeReporter.STATE_DISABLED; + int myState = ChangeReporter.STATE_ENABLED, otherState = ChangeReporter.STATE_LOGGED; assertTrue(reporter.shouldWriteToDebug(myUid, myChangeId, myState)); reporter.reportChange(myUid, myChangeId, myState); @@ -112,4 +120,80 @@ public class ChangeReporterTest { reporter.stopDebugLogAll(); assertFalse(reporter.shouldWriteToDebug(myUid, myChangeId, myState)); } + + @Test + public void testDebugLogWithFlagOnAndOldSdk() { + mSetFlagsRule.enableFlags(Flags.FLAG_SKIP_OLD_AND_DISABLED_COMPAT_LOGGING); + ChangeReporter reporter = new ChangeReporter(ChangeReporter.SOURCE_UNKNOWN_SOURCE); + int myUid = 1022; + long myChangeId = 500L; + int myEnabledState = ChangeReporter.STATE_ENABLED; + int myDisabledState = ChangeReporter.STATE_DISABLED; + + // Report will not log if target sdk is before the previous version. + assertFalse(reporter.shouldWriteToDebug(myUid, myChangeId, myEnabledState, false)); + + reporter.resetReportedChanges(myUid); + + // Report will be logged if target sdk is the latest version. + assertTrue(reporter.shouldWriteToDebug(myUid, myChangeId, myEnabledState, true)); + + reporter.resetReportedChanges(myUid); + + // If the report is disabled, the sdk version shouldn't matter. + assertFalse(reporter.shouldWriteToDebug(myUid, myChangeId, myDisabledState, true)); + } + + @Test + public void testDebugLogWithFlagOnAndDisabledChange() { + mSetFlagsRule.enableFlags(Flags.FLAG_SKIP_OLD_AND_DISABLED_COMPAT_LOGGING); + ChangeReporter reporter = new ChangeReporter(ChangeReporter.SOURCE_UNKNOWN_SOURCE); + int myUid = 1022; + long myChangeId = 500L; + int myEnabledState = ChangeReporter.STATE_ENABLED; + int myDisabledState = ChangeReporter.STATE_DISABLED; + + // Report will not log if the change is disabled. + assertFalse(reporter.shouldWriteToDebug(myUid, myChangeId, myDisabledState, true)); + + reporter.resetReportedChanges(myUid); + + // Report will be logged if the change is enabled. + assertTrue(reporter.shouldWriteToDebug(myUid, myChangeId, myEnabledState, true)); + + reporter.resetReportedChanges(myUid); + + // If the report is not the latest version, the disabled state doesn't matter. + assertFalse(reporter.shouldWriteToDebug(myUid, myChangeId, myEnabledState, false)); + } + + @Test + public void testDebugLogWithFlagOff() { + mSetFlagsRule.disableFlags(Flags.FLAG_SKIP_OLD_AND_DISABLED_COMPAT_LOGGING); + ChangeReporter reporter = new ChangeReporter(ChangeReporter.SOURCE_UNKNOWN_SOURCE); + int myUid = 1022; + long myChangeId = 500L; + int myEnabledState = ChangeReporter.STATE_ENABLED; + int myDisabledState = ChangeReporter.STATE_DISABLED; + + // Report will be logged even if the change is not the latest sdk but the flag is off. + assertTrue(reporter.shouldWriteToDebug(myUid, myChangeId, myEnabledState, false)); + + reporter.resetReportedChanges(myUid); + + // Report will be logged if the change is enabled and the latest sdk but the flag is off. + assertTrue(reporter.shouldWriteToDebug(myUid, myChangeId, myEnabledState, true)); + + reporter.resetReportedChanges(myUid); + + // Report will be logged if the change is disabled and the latest sdk but the flag is + // off. + assertTrue(reporter.shouldWriteToDebug(myUid, myChangeId, myDisabledState, true)); + + reporter.resetReportedChanges(myUid); + + // Report will be logged if the change is disabled and not the latest sdk but the flag is + // off. + assertTrue(reporter.shouldWriteToDebug(myUid, myChangeId, myDisabledState, false)); + } } diff --git a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateInfoTest.java b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateInfoTest.java index 76772b7a528b..08977265667c 100644 --- a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateInfoTest.java +++ b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateInfoTest.java @@ -16,6 +16,12 @@ package android.hardware.devicestate; +import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY; +import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY; +import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED; +import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN; +import static android.hardware.devicestate.DeviceState.PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; @@ -33,6 +39,7 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.List; +import java.util.Set; /** * Unit tests for {@link DeviceStateInfo}. @@ -44,11 +51,25 @@ import java.util.List; public final class DeviceStateInfoTest { private static final DeviceState DEVICE_STATE_0 = new DeviceState( - new DeviceState.Configuration.Builder(0, "STATE_0").build()); + new DeviceState.Configuration.Builder(0, "STATE_0") + .setSystemProperties( + Set.of(PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS, + PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY)) + .setPhysicalProperties( + Set.of(PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED)) + .build()); private static final DeviceState DEVICE_STATE_1 = new DeviceState( - new DeviceState.Configuration.Builder(1, "STATE_1").build()); + new DeviceState.Configuration.Builder(1, "STATE_1") + .setSystemProperties( + Set.of(PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY)) + .setPhysicalProperties( + Set.of(PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN)) + .build()); private static final DeviceState DEVICE_STATE_2 = new DeviceState( - new DeviceState.Configuration.Builder(2, "STATE_2").build()); + new DeviceState.Configuration.Builder(2, "STATE_2") + .setSystemProperties( + Set.of(PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY)) + .build()); @Test public void create() { diff --git a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java index 68de21f76dc8..78d4324ebb1a 100644 --- a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java +++ b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java @@ -93,4 +93,22 @@ public final class DeviceStateTest { Assert.assertEquals(originalState, new DeviceState(stateConfiguration)); } + + @Test + public void writeToParcel_noPhysicalProperties() { + final DeviceState originalState = new DeviceState( + new DeviceState.Configuration.Builder(0, "TEST_STATE") + .setSystemProperties(Set.of(PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS, + PROPERTY_POLICY_AVAILABLE_FOR_APP_REQUEST)) + .build()); + + final Parcel parcel = Parcel.obtain(); + originalState.getConfiguration().writeToParcel(parcel, 0 /* flags */); + parcel.setDataPosition(0); + + final DeviceState.Configuration stateConfiguration = + DeviceState.Configuration.CREATOR.createFromParcel(parcel); + + Assert.assertEquals(originalState, new DeviceState(stateConfiguration)); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java index 539832e3cf3c..d44033c72302 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -523,8 +523,8 @@ class ActivityEmbeddingAnimationRunner { /** * Whether we should use jump cut for the change transition. * This normally happens when opening a new secondary with the existing primary using a - * different split layout. This can be complicated, like from horizontal to vertical split with - * new split pairs. + * different split layout (ratio or direction). This can be complicated, like from horizontal to + * vertical split with new split pairs. * Uses a jump cut animation to simplify. */ private boolean shouldUseJumpCutForChangeTransition(@NonNull TransitionInfo info) { @@ -553,8 +553,8 @@ class ActivityEmbeddingAnimationRunner { } // Check if the transition contains both opening and closing windows. - boolean hasOpeningWindow = false; - boolean hasClosingWindow = false; + final List<TransitionInfo.Change> openChanges = new ArrayList<>(); + final List<TransitionInfo.Change> closeChanges = new ArrayList<>(); for (TransitionInfo.Change change : info.getChanges()) { if (changingChanges.contains(change)) { continue; @@ -564,10 +564,30 @@ class ActivityEmbeddingAnimationRunner { // No-op if it will be covered by the changing parent window. continue; } - hasOpeningWindow |= TransitionUtil.isOpeningType(change.getMode()); - hasClosingWindow |= TransitionUtil.isClosingType(change.getMode()); + if (TransitionUtil.isOpeningType(change.getMode())) { + openChanges.add(change); + } else if (TransitionUtil.isClosingType(change.getMode())) { + closeChanges.add(change); + } + } + if (openChanges.isEmpty() || closeChanges.isEmpty()) { + // Only skip if the transition contains both open and close. + return false; + } + if (changingChanges.size() != 1 || openChanges.size() != 1 || closeChanges.size() != 1) { + // Skip when there are too many windows involved. + return true; + } + final TransitionInfo.Change changingChange = changingChanges.get(0); + final TransitionInfo.Change openChange = openChanges.get(0); + final TransitionInfo.Change closeChange = closeChanges.get(0); + if (changingChange.getStartAbsBounds().equals(openChange.getEndAbsBounds()) + && changingChange.getEndAbsBounds().equals(closeChange.getStartAbsBounds())) { + // Don't skip if the transition is a simple shifting without split direction or ratio + // change. For example, A|B -> B|C. + return false; } - return hasOpeningWindow && hasClosingWindow; + return true; } /** Updates the changes to end states in {@code startTransaction} for jump cut animation. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 8b2ec0a35685..8d489e106ae1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -846,8 +846,10 @@ public abstract class WMShellBaseModule { static ShellController provideShellController(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, + DisplayInsetsController displayInsetsController, @ShellMainThread ShellExecutor mainExecutor) { - return new ShellController(context, shellInit, shellCommandHandler, mainExecutor); + return new ShellController(context, shellInit, shellCommandHandler, + displayInsetsController, mainExecutor); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index fb3c35b6a1e3..04f0f44d2876 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -57,6 +57,8 @@ import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.dagger.back.ShellBackAnimationModule; import com.android.wm.shell.dagger.pip.PipModule; +import com.android.wm.shell.desktopmode.DesktopModeEventLogger; +import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.desktopmode.DesktopTasksController; @@ -509,6 +511,7 @@ public abstract class WMShellModule { ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, DragToDesktopTransitionHandler dragToDesktopTransitionHandler, @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, + DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver, LaunchAdjacentController launchAdjacentController, RecentsTransitionHandler recentsTransitionHandler, MultiInstanceHelper multiInstanceHelper, @@ -518,7 +521,8 @@ public abstract class WMShellModule { displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, dragAndDropController, transitions, enterDesktopTransitionHandler, exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler, - dragToDesktopTransitionHandler, desktopModeTaskRepository, launchAdjacentController, + dragToDesktopTransitionHandler, desktopModeTaskRepository, + desktopModeLoggerTransitionObserver, launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, mainExecutor); } @@ -562,6 +566,22 @@ public abstract class WMShellModule { return new DesktopModeTaskRepository(); } + @WMSingleton + @Provides + static DesktopModeLoggerTransitionObserver provideDesktopModeLoggerTransitionObserver( + ShellInit shellInit, + Transitions transitions, + DesktopModeEventLogger desktopModeEventLogger) { + return new DesktopModeLoggerTransitionObserver( + shellInit, transitions, desktopModeEventLogger); + } + + @WMSingleton + @Provides + static DesktopModeEventLogger provideDesktopModeEventLogger() { + return new DesktopModeEventLogger(); + } + // // Drag and drop // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt new file mode 100644 index 000000000000..a10c7c093c60 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -0,0 +1,349 @@ +/* + * Copyright (C) 2024 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.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.app.ActivityTaskManager.INVALID_TASK_ID +import android.app.TaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.os.IBinder +import android.util.SparseArray +import android.view.SurfaceControl +import android.view.WindowManager +import android.window.TransitionInfo +import androidx.annotation.VisibleForTesting +import androidx.core.util.containsKey +import androidx.core.util.forEach +import androidx.core.util.isEmpty +import androidx.core.util.isNotEmpty +import androidx.core.util.plus +import androidx.core.util.putAll +import com.android.internal.logging.InstanceId +import com.android.internal.logging.InstanceIdSequence +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.TaskUpdate +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.shared.TransitionUtil +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.util.KtProtoLog + +/** + * A [Transitions.TransitionObserver] that observes transitions and the proposed changes to log + * appropriate desktop mode session log events. This observes transitions related to desktop mode + * and other transitions that originate both within and outside shell. + */ +class DesktopModeLoggerTransitionObserver( + shellInit: ShellInit, + private val transitions: Transitions, + private val desktopModeEventLogger: DesktopModeEventLogger +) : Transitions.TransitionObserver { + + private val idSequence: InstanceIdSequence by lazy { InstanceIdSequence(Int.MAX_VALUE) } + + init { + if (Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.isEnabled()) { + shellInit.addInitCallback(this::onInit, this) + } + } + + // A sparse array of visible freeform tasks and taskInfos + private val visibleFreeformTaskInfos: SparseArray<TaskInfo> = SparseArray() + + // Caching the taskInfos to handle canceled recents animations, if we identify that the recents + // animation was cancelled, we restore these tasks to calculate the post-Transition state + private val tasksSavedForRecents: SparseArray<TaskInfo> = SparseArray() + + // The instanceId for the current logging session + private var loggerInstanceId: InstanceId? = null + + private val isSessionActive: Boolean + get() = loggerInstanceId != null + + private fun setSessionInactive() { + loggerInstanceId = null + } + + fun onInit() { + transitions.registerObserver(this) + } + + override fun onTransitionReady( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction + ) { + // this was a new recents animation + if (info.isRecentsTransition() && tasksSavedForRecents.isEmpty()) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopModeLogger: Recents animation running, saving tasks for later" + ) + // TODO (b/326391303) - avoid logging session exit if we can identify a cancelled + // recents animation + + // when recents animation is running, all freeform tasks are sent TO_BACK temporarily + // if the user ends up at home, we need to update the visible freeform tasks + // if the user cancels the animation, the subsequent transition is NONE + // if the user opens a new task, the subsequent transition is OPEN with flag + tasksSavedForRecents.putAll(visibleFreeformTaskInfos) + } + + // figure out what the new state of freeform tasks would be post transition + var postTransitionVisibleFreeformTasks = getPostTransitionVisibleFreeformTaskInfos(info) + + // A canceled recents animation is followed by a TRANSIT_NONE transition with no flags, if + // that's the case, we might have accidentally logged a session exit and would need to + // revaluate again. Add all the tasks back. + // This will start a new desktop mode session. + if ( + info.type == WindowManager.TRANSIT_NONE && + info.flags == 0 && + tasksSavedForRecents.isNotEmpty() + ) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopModeLogger: Canceled recents animation, restoring tasks" + ) + // restore saved tasks in the updated set and clear for next use + postTransitionVisibleFreeformTasks += tasksSavedForRecents + tasksSavedForRecents.clear() + } + + // identify if we need to log any changes and update the state of visible freeform tasks + identifyLogEventAndUpdateState( + transitionInfo = info, + preTransitionVisibleFreeformTasks = visibleFreeformTaskInfos, + postTransitionVisibleFreeformTasks = postTransitionVisibleFreeformTasks + ) + } + + override fun onTransitionStarting(transition: IBinder) {} + + override fun onTransitionMerged(merged: IBinder, playing: IBinder) {} + + override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {} + + private fun getPostTransitionVisibleFreeformTaskInfos( + info: TransitionInfo + ): SparseArray<TaskInfo> { + // device is sleeping, so no task will be visible anymore + if (info.type == WindowManager.TRANSIT_SLEEP) { + return SparseArray() + } + + // filter changes involving freeform tasks or tasks that were cached in previous state + val changesToFreeformWindows = + info.changes + .filter { it.taskInfo != null && it.requireTaskInfo().taskId != INVALID_TASK_ID } + .filter { + it.requireTaskInfo().isFreeformWindow() || + visibleFreeformTaskInfos.containsKey(it.requireTaskInfo().taskId) + } + + val postTransitionFreeformTasks: SparseArray<TaskInfo> = SparseArray() + // start off by adding all existing tasks + postTransitionFreeformTasks.putAll(visibleFreeformTaskInfos) + + // the combined set of taskInfos we are interested in this transition change + for (change in changesToFreeformWindows) { + val taskInfo = change.requireTaskInfo() + + // check if this task existed as freeform window in previous cached state and it's now + // changing window modes + if ( + visibleFreeformTaskInfos.containsKey(taskInfo.taskId) && + visibleFreeformTaskInfos.get(taskInfo.taskId).isFreeformWindow() && + !taskInfo.isFreeformWindow() + ) { + postTransitionFreeformTasks.remove(taskInfo.taskId) + // no need to evaluate new visibility of this task, since it's no longer a freeform + // window + continue + } + + // check if the task is visible after this change, otherwise remove it + if (isTaskVisibleAfterChange(change)) { + postTransitionFreeformTasks.put(taskInfo.taskId, taskInfo) + } else { + postTransitionFreeformTasks.remove(taskInfo.taskId) + } + } + + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopModeLogger: taskInfo map after processing changes %s", + postTransitionFreeformTasks.size() + ) + + return postTransitionFreeformTasks + } + + /** + * Look at the [TransitionInfo.Change] and figure out if this task will be visible after this + * change is processed + */ + private fun isTaskVisibleAfterChange(change: TransitionInfo.Change): Boolean = + when { + TransitionUtil.isOpeningType(change.mode) -> true + TransitionUtil.isClosingType(change.mode) -> false + // change mode TRANSIT_CHANGE is only for visible to visible transitions + change.mode == WindowManager.TRANSIT_CHANGE -> true + else -> false + } + + /** + * Log the appropriate log event based on the new state of TasksInfos and previously cached + * state and update it + */ + private fun identifyLogEventAndUpdateState( + transitionInfo: TransitionInfo, + preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>, + postTransitionVisibleFreeformTasks: SparseArray<TaskInfo> + ) { + if ( + postTransitionVisibleFreeformTasks.isEmpty() && + preTransitionVisibleFreeformTasks.isNotEmpty() && + isSessionActive + ) { + // Sessions is finishing, log task updates followed by an exit event + identifyAndLogTaskUpdates( + loggerInstanceId!!.id, + preTransitionVisibleFreeformTasks, + postTransitionVisibleFreeformTasks + ) + + desktopModeEventLogger.logSessionExit( + loggerInstanceId!!.id, + getExitReason(transitionInfo) + ) + + setSessionInactive() + } else if ( + postTransitionVisibleFreeformTasks.isNotEmpty() && + preTransitionVisibleFreeformTasks.isEmpty() && + !isSessionActive + ) { + // Session is starting, log enter event followed by task updates + loggerInstanceId = idSequence.newInstanceId() + desktopModeEventLogger.logSessionEnter( + loggerInstanceId!!.id, + getEnterReason(transitionInfo) + ) + + identifyAndLogTaskUpdates( + loggerInstanceId!!.id, + preTransitionVisibleFreeformTasks, + postTransitionVisibleFreeformTasks + ) + } else if (isSessionActive) { + // Session is neither starting, nor finishing, log task updates if there are any + identifyAndLogTaskUpdates( + loggerInstanceId!!.id, + preTransitionVisibleFreeformTasks, + postTransitionVisibleFreeformTasks + ) + } + + // update the state to the new version + visibleFreeformTaskInfos.clear() + visibleFreeformTaskInfos.putAll(postTransitionVisibleFreeformTasks) + } + + // TODO(b/326231724) - Add logging around taskInfoChanges Updates + /** Compare the old and new state of taskInfos and identify and log the changes */ + private fun identifyAndLogTaskUpdates( + sessionId: Int, + preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>, + postTransitionVisibleFreeformTasks: SparseArray<TaskInfo> + ) { + // find new tasks that were added + postTransitionVisibleFreeformTasks.forEach { taskId, taskInfo -> + if (!preTransitionVisibleFreeformTasks.containsKey(taskId)) { + desktopModeEventLogger.logTaskAdded(sessionId, buildTaskUpdateForTask(taskInfo)) + } + } + + // find old tasks that were removed + preTransitionVisibleFreeformTasks.forEach { taskId, taskInfo -> + if (!postTransitionVisibleFreeformTasks.containsKey(taskId)) { + desktopModeEventLogger.logTaskRemoved(sessionId, buildTaskUpdateForTask(taskInfo)) + } + } + } + + // TODO(b/326231724: figure out how to get taskWidth and taskHeight from TaskInfo + private fun buildTaskUpdateForTask(taskInfo: TaskInfo): TaskUpdate { + val taskUpdate = TaskUpdate(taskInfo.taskId, taskInfo.userId) + // add task x, y if available + taskInfo.positionInParent?.let { taskUpdate.copy(taskX = it.x, taskY = it.y) } + + return taskUpdate + } + + /** Get [EnterReason] for this session enter */ + private fun getEnterReason(transitionInfo: TransitionInfo): EnterReason { + // TODO(b/326231756) - Add support for missing enter reasons + return when (transitionInfo.type) { + WindowManager.TRANSIT_WAKE -> EnterReason.SCREEN_ON + Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP -> EnterReason.APP_HANDLE_DRAG + Transitions.TRANSIT_MOVE_TO_DESKTOP -> EnterReason.APP_HANDLE_MENU_BUTTON + WindowManager.TRANSIT_OPEN -> EnterReason.APP_FREEFORM_INTENT + else -> EnterReason.UNKNOWN_ENTER + } + } + + /** Get [ExitReason] for this session exit */ + private fun getExitReason(transitionInfo: TransitionInfo): ExitReason { + // TODO(b/326231756) - Add support for missing exit reasons + return when { + transitionInfo.type == WindowManager.TRANSIT_SLEEP -> ExitReason.SCREEN_OFF + transitionInfo.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED + transitionInfo.type == Transitions.TRANSIT_EXIT_DESKTOP_MODE -> ExitReason.DRAG_TO_EXIT + transitionInfo.isRecentsTransition() -> ExitReason.RETURN_HOME_OR_OVERVIEW + else -> ExitReason.UNKNOWN_EXIT + } + } + + /** Adds tasks to the saved copy of freeform taskId, taskInfo. Only used for testing. */ + @VisibleForTesting + fun addTaskInfosToCachedMap(taskInfo: TaskInfo) { + visibleFreeformTaskInfos.set(taskInfo.taskId, taskInfo) + } + + @VisibleForTesting fun getLoggerSessionId(): Int? = loggerInstanceId?.id + + @VisibleForTesting + fun setLoggerSessionId(id: Int) { + loggerInstanceId = InstanceId.fakeInstanceId(id) + } + + private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo { + return this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change") + } + + private fun TaskInfo.isFreeformWindow(): Boolean { + return this.windowingMode == WINDOWING_MODE_FREEFORM + } + + private fun TransitionInfo.isRecentsTransition(): Boolean { + return this.type == WindowManager.TRANSIT_TO_FRONT && + this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 95237c38f309..dd8c1a0f2e02 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -102,6 +102,7 @@ class DesktopTasksController( ToggleResizeDesktopTaskTransitionHandler, private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler, private val desktopModeTaskRepository: DesktopModeTaskRepository, + private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver, private val launchAdjacentController: LaunchAdjacentController, private val recentsTransitionHandler: RecentsTransitionHandler, private val multiInstanceHelper: MultiInstanceHelper, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 9dd4c193a006..ec907fd9bd12 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -29,6 +29,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -2776,7 +2777,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, + " with " + taskInfo.taskId + " before startAnimation()."); record.addRecord(stage, true, taskInfo.taskId); } - } else if (isClosingType(change.getMode())) { + } else if (change.getMode() == TRANSIT_CLOSE) { if (stage.containsTask(taskInfo.taskId)) { record.addRecord(stage, false, taskInfo.taskId); Log.w(TAG, "Expected onTaskVanished on " + stage + " to have been called" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/DisplayImeChangeListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/DisplayImeChangeListener.java new file mode 100644 index 000000000000..a94f80241d4f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/DisplayImeChangeListener.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 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.wm.shell.sysui; + +import android.graphics.Rect; + +/** + * Callbacks for when the Display IME changes. + */ +public interface DisplayImeChangeListener { + /** + * Called when the ime bounds change. + */ + default void onImeBoundsChanged(int displayId, Rect bounds) {} + + /** + * Called when the IME visibility change. + */ + default void onImeVisibilityChanged(int displayId, boolean isShowing) {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java index a7843e218a8a..2f6edc226c45 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java @@ -30,21 +30,28 @@ import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.UserInfo; import android.content.res.Configuration; +import android.graphics.Rect; import android.os.Bundle; import android.util.ArrayMap; +import android.view.InsetsSource; +import android.view.InsetsState; import android.view.SurfaceControlRegistry; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.annotations.ExternalThread; import java.io.PrintWriter; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; import java.util.function.Supplier; /** @@ -57,6 +64,7 @@ public class ShellController { private final ShellInit mShellInit; private final ShellCommandHandler mShellCommandHandler; private final ShellExecutor mMainExecutor; + private final DisplayInsetsController mDisplayInsetsController; private final ShellInterfaceImpl mImpl = new ShellInterfaceImpl(); private final CopyOnWriteArrayList<ConfigurationChangeListener> mConfigChangeListeners = @@ -65,6 +73,8 @@ public class ShellController { new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList<UserChangeListener> mUserChangeListeners = new CopyOnWriteArrayList<>(); + private final ConcurrentHashMap<DisplayImeChangeListener, Executor> mDisplayImeChangeListeners = + new ConcurrentHashMap<>(); private ArrayMap<String, Supplier<ExternalInterfaceBinder>> mExternalInterfaceSuppliers = new ArrayMap<>(); @@ -73,20 +83,53 @@ public class ShellController { private Configuration mLastConfiguration; + private OnInsetsChangedListener mInsetsChangeListener = new OnInsetsChangedListener() { + private InsetsState mInsetsState = new InsetsState(); + + @Override + public void insetsChanged(InsetsState insetsState) { + if (mInsetsState == insetsState) { + return; + } + + InsetsSource oldSource = mInsetsState.peekSource(InsetsSource.ID_IME); + boolean wasVisible = (oldSource != null && oldSource.isVisible()); + Rect oldFrame = wasVisible ? oldSource.getFrame() : null; + + InsetsSource newSource = insetsState.peekSource(InsetsSource.ID_IME); + boolean isVisible = (newSource != null && newSource.isVisible()); + Rect newFrame = isVisible ? newSource.getFrame() : null; + + if (wasVisible != isVisible) { + onImeVisibilityChanged(isVisible); + } + + if (newFrame != null && !newFrame.equals(oldFrame)) { + onImeBoundsChanged(newFrame); + } + + mInsetsState = insetsState; + } + }; + public ShellController(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, + DisplayInsetsController displayInsetsController, ShellExecutor mainExecutor) { mContext = context; mShellInit = shellInit; mShellCommandHandler = shellCommandHandler; + mDisplayInsetsController = displayInsetsController; mMainExecutor = mainExecutor; shellInit.addInitCallback(this::onInit, this); } private void onInit() { mShellCommandHandler.addDumpCallback(this::dump, this); + mDisplayInsetsController.addInsetsChangedListener( + mContext.getDisplayId(), mInsetsChangeListener); } /** @@ -259,6 +302,25 @@ public class ShellController { } } + @VisibleForTesting + void onImeBoundsChanged(Rect bounds) { + ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Display Ime bounds changed"); + mDisplayImeChangeListeners.forEach( + (DisplayImeChangeListener listener, Executor executor) -> + executor.execute(() -> listener.onImeBoundsChanged( + mContext.getDisplayId(), bounds))); + } + + @VisibleForTesting + void onImeVisibilityChanged(boolean isShowing) { + ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Display Ime visibility changed: isShowing=%b", + isShowing); + mDisplayImeChangeListeners.forEach( + (DisplayImeChangeListener listener, Executor executor) -> + executor.execute(() -> listener.onImeVisibilityChanged( + mContext.getDisplayId(), isShowing))); + } + private void handleInit() { SurfaceControlRegistry.createProcessInstance(mContext); mShellInit.init(); @@ -329,6 +391,19 @@ public class ShellController { } @Override + public void addDisplayImeChangeListener(DisplayImeChangeListener listener, + Executor executor) { + ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Adding new DisplayImeChangeListener"); + mDisplayImeChangeListeners.put(listener, executor); + } + + @Override + public void removeDisplayImeChangeListener(DisplayImeChangeListener listener) { + ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Removing DisplayImeChangeListener"); + mDisplayImeChangeListeners.remove(listener); + } + + @Override public boolean handleCommand(String[] args, PrintWriter pw) { try { boolean[] result = new boolean[1]; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java index bc5dd11ef54e..bd1c64a0d182 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java @@ -25,6 +25,7 @@ import androidx.annotation.NonNull; import java.io.PrintWriter; import java.util.List; +import java.util.concurrent.Executor; /** * General interface for notifying the Shell of common SysUI events like configuration or keyguard @@ -65,6 +66,18 @@ public interface ShellInterface { default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {} /** + * Registers a DisplayImeChangeListener to monitor for changes on Ime + * position and visibility. + */ + default void addDisplayImeChangeListener(DisplayImeChangeListener listener, + Executor executor) {} + + /** + * Removes a registered DisplayImeChangeListener. + */ + default void removeDisplayImeChangeListener(DisplayImeChangeListener listener) {} + + /** * Handles a shell command. */ default boolean handleCommand(final String[] args, PrintWriter pw) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt index ae39fbcb4eed..4a4c5e860bb2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt @@ -37,6 +37,7 @@ import com.android.wm.shell.WindowManagerShellWrapper import com.android.wm.shell.bubbles.bar.BubbleBarLayerView import com.android.wm.shell.bubbles.properties.BubbleProperties import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayInsetsController import com.android.wm.shell.common.FloatingContentCoordinator import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue @@ -94,7 +95,8 @@ class BubbleViewInfoTest : ShellTestCase() { val windowManager = context.getSystemService(WindowManager::class.java) val shellInit = ShellInit(mainExecutor) val shellCommandHandler = ShellCommandHandler() - val shellController = ShellController(context, shellInit, shellCommandHandler, mainExecutor) + val shellController = ShellController(context, shellInit, shellCommandHandler, + mock<DisplayInsetsController>(), mainExecutor) bubblePositioner = BubblePositioner(context, windowManager) val bubbleData = BubbleData( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt new file mode 100644 index 000000000000..65117f7e9eea --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2024 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.wm.shell.desktopmode + +import android.app.ActivityManager +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.os.IBinder +import android.testing.AndroidTestingRunner +import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_CHANGE +import android.view.WindowManager.TRANSIT_CLOSE +import android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS +import android.view.WindowManager.TRANSIT_NONE +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_SLEEP +import android.view.WindowManager.TRANSIT_TO_BACK +import android.view.WindowManager.TRANSIT_TO_FRONT +import android.view.WindowManager.TRANSIT_WAKE +import android.window.IWindowContainerToken +import android.window.TransitionInfo +import android.window.TransitionInfo.Change +import android.window.WindowContainerToken +import androidx.test.filters.SmallTest +import com.android.modules.utils.testing.ExtendedMockitoRule +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.TransitionInfoBuilder +import com.android.wm.shell.transition.Transitions +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.never +import org.mockito.kotlin.same +import org.mockito.kotlin.times + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopModeLoggerTransitionObserverTest { + + @JvmField + @Rule + val extendedMockitoRule = ExtendedMockitoRule.Builder(this) + .mockStatic(DesktopModeEventLogger::class.java) + .mockStatic(DesktopModeStatus::class.java).build()!! + + @Mock + lateinit var testExecutor: ShellExecutor + @Mock + private lateinit var mockShellInit: ShellInit + @Mock + private lateinit var transitions: Transitions + + private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver + private lateinit var shellInit: ShellInit + private lateinit var desktopModeEventLogger: DesktopModeEventLogger + + @Before + fun setup() { + Mockito.`when`(DesktopModeStatus.isEnabled()).thenReturn(true) + shellInit = Mockito.spy(ShellInit(testExecutor)) + desktopModeEventLogger = mock(DesktopModeEventLogger::class.java) + + transitionObserver = DesktopModeLoggerTransitionObserver( + mockShellInit, transitions, desktopModeEventLogger) + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + val initRunnableCaptor = ArgumentCaptor.forClass( + Runnable::class.java) + verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(), + same(transitionObserver)) + initRunnableCaptor.value.run() + } else { + transitionObserver.onInit() + } + } + + @Test + fun testRegistersObserverAtInit() { + verify(transitions) + .registerObserver(same( + transitionObserver)) + } + + @Test + fun taskCreated_notFreeformWindow_doesNotLogSessionEnterOrTaskAdded() { + val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, never()).logSessionEnter(any(), any()) + verify(desktopModeEventLogger, never()).logTaskAdded(any(), any()) + } + + @Test + fun taskCreated_FreeformWindowOpen_logSessionEnterAndTaskAdded() { + val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), + eq(EnterReason.APP_FREEFORM_INTENT)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + } + + @Test + fun taskChanged_taskMovedToDesktopByDrag_logSessionEnterAndTaskAdded() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + // task change is finalised when drag ends + val transitionInfo = TransitionInfoBuilder( + Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), + eq(EnterReason.APP_HANDLE_DRAG)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + } + + @Test + fun taskChanged_taskMovedToDesktopByButtonTap_logSessionEnterAndTaskAdded() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_MOVE_TO_DESKTOP, 0) + .addChange(change).build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), + eq(EnterReason.APP_HANDLE_MENU_BUTTON)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + } + + @Test + fun taskChanged_existingFreeformTaskMadeVisible_logSessionEnterAndTaskAdded() { + val taskInfo = createTaskInfo(1, WINDOWING_MODE_FREEFORM) + taskInfo.isVisibleRequested = true + val change = createChange(TRANSIT_CHANGE, taskInfo) + val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_MOVE_TO_DESKTOP, 0) + .addChange(change).build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), + eq(EnterReason.APP_HANDLE_MENU_BUTTON)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + } + + @Test + fun taskToFront_screenWake_logSessionStartedAndTaskAdded() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0) + .addChange(change).build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), + eq(EnterReason.SCREEN_ON)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + } + + @Test + fun freeformTaskVisible_screenTurnOff_logSessionExitAndTaskRemoved_sessionIdNull() { + val sessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), + eq(ExitReason.SCREEN_OFF)) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + } + + @Test + fun freeformTaskVisible_exitDesktopUsingDrag_logSessionExitAndTaskRemoved_sessionIdNull() { + val sessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // window mode changing from FREEFORM to FULLSCREEN + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_EXIT_DESKTOP_MODE) + .addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), + eq(ExitReason.DRAG_TO_EXIT)) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + } + + @Test + fun freeformTaskVisible_exitDesktopBySwipeUp_logSessionExitAndTaskRemoved_sessionIdNull() { + val sessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // recents transition + val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), + eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + } + + @Test + fun freeformTaskVisible_taskFinished_logSessionExitAndTaskRemoved_sessionIdNull() { + val sessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // task closing + val change = createChange(TRANSIT_CLOSE, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), + eq(ExitReason.TASK_FINISHED)) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + } + + @Test + fun sessionExitByRecents_cancelledAnimation_sessionRestored() { + val sessionId = 1 + // add a freeform task to an existing session + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // recents transition sent freeform window to back + val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo1 = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change) + .build() + callOnTransitionReady(transitionInfo1) + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), + eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + + val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build() + callOnTransitionReady(transitionInfo2) + + verify(desktopModeEventLogger, times(1)).logSessionEnter(any(), any()) + verify(desktopModeEventLogger, times(1)).logTaskAdded(any(), any()) + } + + @Test + fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() { + val sessionId = 1 + // add an existing freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // new freeform task added + val change = createChange(TRANSIT_OPEN, createTaskInfo(2, WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + verify(desktopModeEventLogger, never()).logSessionEnter(any(), any()) + } + + @Test + fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() { + val sessionId = 1 + // add two existing freeform tasks + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(2, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // new freeform task added + val change = createChange(TRANSIT_CLOSE, createTaskInfo(2, WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, never()).logSessionExit(any(), any()) + } + + /** + * Simulate calling the onTransitionReady() method + */ + private fun callOnTransitionReady(transitionInfo: TransitionInfo) { + val transition = mock(IBinder::class.java) + val startT = mock( + SurfaceControl.Transaction::class.java) + val finishT = mock( + SurfaceControl.Transaction::class.java) + + transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT) + } + + companion object { + fun createTaskInfo(taskId: Int, windowMode: Int): ActivityManager.RunningTaskInfo { + val taskInfo = ActivityManager.RunningTaskInfo() + taskInfo.taskId = taskId + taskInfo.configuration.windowConfiguration.windowingMode = windowMode + + return taskInfo + } + + fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change { + val change = Change( + WindowContainerToken(mock( + IWindowContainerToken::class.java)), + mock(SurfaceControl::class.java)) + change.mode = mode + change.taskInfo = taskInfo + return change + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 0136751d8c9a..5df9dd38a75d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -87,6 +87,7 @@ import org.mockito.Mockito.any import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.verify +import org.mockito.kotlin.times import org.mockito.Mockito.`when` as whenever import org.mockito.quality.Strictness @@ -113,6 +114,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler @Mock lateinit var dragAndDropController: DragAndDropController @Mock lateinit var multiInstanceHelper: MultiInstanceHelper + @Mock lateinit var desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver private lateinit var mockitoSession: StaticMockitoSession private lateinit var controller: DesktopTasksController @@ -163,6 +165,7 @@ class DesktopTasksControllerTest : ShellTestCase() { mToggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, desktopModeTaskRepository, + desktopModeLoggerTransitionObserver, launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index 3384509f1da9..d38fc6cb6418 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -129,7 +129,7 @@ public class PipControllerTest extends ShellTestCase { }).when(mMockExecutor).execute(any()); mShellInit = spy(new ShellInit(mMockExecutor)); mShellController = spy(new ShellController(mContext, mShellInit, mMockShellCommandHandler, - mMockExecutor)); + mMockDisplayInsetsController, mMockExecutor)); mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler, mShellController, mMockDisplayController, mMockPipAnimationController, mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java index 10e9e11e9004..41a4e8d503c9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -58,6 +58,7 @@ import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; @@ -96,6 +97,8 @@ public class RecentTasksControllerTest extends ShellTestCase { private DesktopModeTaskRepository mDesktopModeTaskRepository; @Mock private ActivityTaskManager mActivityTaskManager; + @Mock + private DisplayInsetsController mDisplayInsetsController; private ShellTaskOrganizer mShellTaskOrganizer; private RecentTasksController mRecentTasksController; @@ -110,7 +113,7 @@ public class RecentTasksControllerTest extends ShellTestCase { when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); mShellInit = spy(new ShellInit(mMainExecutor)); mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler, - mMainExecutor)); + mDisplayInsetsController, mMainExecutor)); mRecentTasksControllerReal = new RecentTasksController(mContext, mShellInit, mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager, Optional.of(mDesktopModeTaskRepository), mMainExecutor); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index 315d97ed333b..3c387f0d7c34 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -123,7 +123,7 @@ public class SplitScreenControllerTests extends ShellTestCase { assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow(mContext)); MockitoAnnotations.initMocks(this); mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler, - mMainExecutor)); + mDisplayInsetsController, mMainExecutor)); mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit, mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue, mRootTDAOrganizer, mDisplayController, mDisplayImeController, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java index 012c40811811..ff76a2f13527 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java @@ -40,6 +40,7 @@ import com.android.internal.util.function.TriConsumer; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -65,6 +66,7 @@ public class StartingWindowControllerTests extends ShellTestCase { private @Mock Context mContext; private @Mock DisplayManager mDisplayManager; + private @Mock DisplayInsetsController mDisplayInsetsController; private @Mock ShellCommandHandler mShellCommandHandler; private @Mock ShellTaskOrganizer mTaskOrganizer; private @Mock ShellExecutor mMainExecutor; @@ -83,7 +85,7 @@ public class StartingWindowControllerTests extends ShellTestCase { doReturn(super.mContext.getResources()).when(mContext).getResources(); mShellInit = spy(new ShellInit(mMainExecutor)); mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler, - mMainExecutor)); + mDisplayInsetsController, mMainExecutor)); mController = new StartingWindowController(mContext, mShellInit, mShellController, mTaskOrganizer, mMainExecutor, mTypeAlgorithm, mIconProvider, mTransactionPool); mShellInit.init(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java index 7c520c34b29d..6292018ba35d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.mock; import android.content.Context; import android.content.pm.UserInfo; import android.content.res.Configuration; +import android.graphics.Rect; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; @@ -35,8 +36,8 @@ import androidx.test.platform.app.InstrumentationRegistry; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ExternalInterfaceBinder; -import com.android.wm.shell.common.ShellExecutor; import org.junit.After; import org.junit.Before; @@ -63,12 +64,15 @@ public class ShellControllerTest extends ShellTestCase { private ShellCommandHandler mShellCommandHandler; @Mock private Context mTestUserContext; + @Mock + private DisplayInsetsController mDisplayInsetsController; private TestShellExecutor mExecutor; private ShellController mController; private TestConfigurationChangeListener mConfigChangeListener; private TestKeyguardChangeListener mKeyguardChangeListener; private TestUserChangeListener mUserChangeListener; + private TestDisplayImeChangeListener mDisplayImeChangeListener; @Before @@ -77,8 +81,10 @@ public class ShellControllerTest extends ShellTestCase { mKeyguardChangeListener = new TestKeyguardChangeListener(); mConfigChangeListener = new TestConfigurationChangeListener(); mUserChangeListener = new TestUserChangeListener(); + mDisplayImeChangeListener = new TestDisplayImeChangeListener(); mExecutor = new TestShellExecutor(); - mController = new ShellController(mContext, mShellInit, mShellCommandHandler, mExecutor); + mController = new ShellController(mContext, mShellInit, mShellCommandHandler, + mDisplayInsetsController, mExecutor); mController.onConfigurationChanged(getConfigurationCopy()); } @@ -130,6 +136,45 @@ public class ShellControllerTest extends ShellTestCase { } @Test + public void testAddDisplayImeChangeListener_ensureCallback() { + mController.asShell().addDisplayImeChangeListener( + mDisplayImeChangeListener, mExecutor); + + final Rect bounds = new Rect(10, 20, 30, 40); + mController.onImeBoundsChanged(bounds); + mController.onImeVisibilityChanged(true); + mExecutor.flushAll(); + + assertTrue(mDisplayImeChangeListener.boundsChanged == 1); + assertTrue(bounds.equals(mDisplayImeChangeListener.lastBounds)); + assertTrue(mDisplayImeChangeListener.visibilityChanged == 1); + assertTrue(mDisplayImeChangeListener.lastVisibility); + } + + @Test + public void testDoubleAddDisplayImeChangeListener_ensureSingleCallback() { + mController.asShell().addDisplayImeChangeListener( + mDisplayImeChangeListener, mExecutor); + mController.asShell().addDisplayImeChangeListener( + mDisplayImeChangeListener, mExecutor); + + mController.onImeVisibilityChanged(true); + mExecutor.flushAll(); + assertTrue(mDisplayImeChangeListener.visibilityChanged == 1); + } + + @Test + public void testAddRemoveDisplayImeChangeListener_ensureNoCallback() { + mController.asShell().addDisplayImeChangeListener( + mDisplayImeChangeListener, mExecutor); + mController.asShell().removeDisplayImeChangeListener(mDisplayImeChangeListener); + + mController.onImeVisibilityChanged(true); + mExecutor.flushAll(); + assertTrue(mDisplayImeChangeListener.visibilityChanged == 0); + } + + @Test public void testAddUserChangeListener_ensureCallback() { mController.addUserChangeListener(mUserChangeListener); @@ -457,4 +502,23 @@ public class ShellControllerTest extends ShellTestCase { lastUserProfiles = profiles; } } + + private static class TestDisplayImeChangeListener implements DisplayImeChangeListener { + public int boundsChanged = 0; + public Rect lastBounds; + public int visibilityChanged = 0; + public boolean lastVisibility = false; + + @Override + public void onImeBoundsChanged(int displayId, Rect bounds) { + boundsChanged++; + lastBounds = bounds; + } + + @Override + public void onImeVisibilityChanged(int displayId, boolean isShowing) { + visibilityChanged++; + lastVisibility = isShowing; + } + } } diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig index 156be389fe84..f33bcb7f9643 100644 --- a/location/java/android/location/flags/location.aconfig +++ b/location/java/android/location/flags/location.aconfig @@ -11,7 +11,7 @@ flag { name: "location_bypass" namespace: "location" description: "Enable location bypass appops behavior" - bug: "301150056" + bug: "329151785" } flag { diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 6cf9c6fa7616..bf3942559b8a 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -109,5 +109,5 @@ flag { name: "enable_null_session_in_media_browser_service" namespace: "media_solutions" description: "Enables apps owning a MediaBrowserService to disconnect all connected browsers." - bug: "263520343" + bug: "185136506" } diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java index d20490860448..dd2a534676d8 100644 --- a/media/java/android/media/tv/ad/TvAdView.java +++ b/media/java/android/media/tv/ad/TvAdView.java @@ -393,16 +393,12 @@ public class TvAdView extends ViewGroup { } /** - * Sets a listener to be invoked when an input event is not handled - * by the TV AD service. + * Sets a listener to be invoked when an input event is not handled by the TV AD service. * * @param listener The callback to be invoked when the unhandled input event is received. */ - public void setOnUnhandledInputEventListener( - @NonNull @CallbackExecutor Executor executor, - @NonNull OnUnhandledInputEventListener listener) { + public void setOnUnhandledInputEventListener(@NonNull OnUnhandledInputEventListener listener) { mOnUnhandledInputEventListener = listener; - // TODO: handle CallbackExecutor } /** @@ -441,6 +437,9 @@ public class TvAdView extends ViewGroup { /** * Prepares the AD service of corresponding {@link TvAdService}. * + * <p>This should be called before calling {@link #startAdService()}. Otherwise, + * {@link #startAdService()} is a no-op. + * * @param serviceId the AD service ID, which can be found in TvAdServiceInfo#getId(). */ public void prepareAdService(@NonNull String serviceId, @NonNull String type) { @@ -455,6 +454,9 @@ public class TvAdView extends ViewGroup { /** * Starts the AD service. + * + * <p>This should be called after calling {@link #prepareAdService(String, String)}. Otherwise, + * it's a no-op. */ public void startAdService() { if (DEBUG) { @@ -467,6 +469,8 @@ public class TvAdView extends ViewGroup { /** * Stops the AD service. + * + * <p>It's a no-op if the service is not started. */ public void stopAdService() { if (DEBUG) { diff --git a/nfc/Android.bp b/nfc/Android.bp index 7dd16ba6c18e..7698e2b2d054 100644 --- a/nfc/Android.bp +++ b/nfc/Android.bp @@ -76,6 +76,9 @@ java_sdk_library { "//apex_available:platform", "com.android.nfcservices", ], + aconfig_declarations: [ + "android.nfc.flags-aconfig", + ], } filegroup { diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml index 6019aa8560e1..42d0cc403372 100644 --- a/packages/CompanionDeviceManager/res/values/strings.xml +++ b/packages/CompanionDeviceManager/res/values/strings.xml @@ -52,12 +52,21 @@ <!-- Confirmation for associating an application with a companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] --> <string name="title_app_streaming">Allow <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to access this information from your phone</string> + <!-- Confirmation for associating an application with a companion device of APP_STREAMING profile (type) with mirroring enabled [CHAR LIMIT=NONE] --> + <string name="title_app_streaming_with_mirroring">Allow <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to stream your phone\u2019s apps?</string> + + <!-- Summary for associating an application with a companion device of APP_STREAMING profile [CHAR LIMIT=NONE] --> + <string name="summary_app_streaming">%1$s will have access to anything that’s visible or played on the phone, including audio, photos, passwords, and messages.<br/><br/>%1$s will be able to stream apps until you remove access to this permission.</string> + <!-- Title of the helper dialog for APP_STREAMING profile [CHAR LIMIT=30]. --> <string name="helper_title_app_streaming">Cross-device services</string> <!-- Description of the helper dialog for APP_STREAMING profile. [CHAR LIMIT=NONE] --> <string name="helper_summary_app_streaming"><xliff:g id="app_name" example="GMS">%1$s</xliff:g> is requesting permission on behalf of your <xliff:g id="display_name" example="Chromebook">%2$s</xliff:g> to stream apps between your devices</string> + <!-- Description of the helper dialog for APP_STREAMING profile with mirroring enabled. [CHAR LIMIT=NONE] --> + <string name="helper_summary_app_streaming_with_mirroring"><xliff:g id="app_name" example="GMS">%1$s</xliff:g> is requesting permission on behalf of your <xliff:g id="display_name" example="Chromebook">%2$s</xliff:g> to display and stream apps between your devices</string> + <!-- ================= DEVICE_PROFILE_AUTOMOTIVE_PROJECTION ================= --> <!-- Confirmation for associating an application with a companion device of AUTOMOTIVE_PROJECTION profile (type) [CHAR LIMIT=NONE] --> @@ -85,6 +94,12 @@ <!-- Confirmation for associating an application with a companion device of NEARBY_DEVICE_STREAMING profile (type) [CHAR LIMIT=NONE] --> <string name="title_nearby_device_streaming">Allow <strong><xliff:g id="device_name" example="NearbyStreamer">%1$s</xliff:g></strong> to take this action?</string> + <!-- Confirmation for associating an application with a companion device of NEARBY_DEVICE_STREAMING profile (type) with mirroring enabled [CHAR LIMIT=NONE] --> + <string name="title_nearby_device_streaming_with_mirroring">Allow <strong><xliff:g id="device_name" example="NearbyStreamer">%1$s</xliff:g></strong> to stream your phone\u2019s apps and system features?</string> + + <!-- Summary for associating an application with a companion device of NEARBY_DEVICE_STREAMING profile [CHAR LIMIT=NONE] --> + <string name="summary_nearby_device_streaming">%1$s will have access to anything that’s visible or played on your phone, including audio, photos, payment info, passwords, and messages.<br/><br/>%1$s will be able to stream apps and system features until you remove access to this permission.</string> + <!-- Description of the helper dialog for NEARBY_DEVICE_STREAMING profile. [CHAR LIMIT=NONE] --> <string name="helper_summary_nearby_device_streaming"><xliff:g id="app_name" example="NearbyStreamerApp">%1$s</xliff:g> is requesting permission on behalf of your <xliff:g id="device_name" example="NearbyDevice">%2$s</xliff:g> to stream apps and other system features to nearby devices</string> diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java index 4c1f6313c3a0..1231b639ead6 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java @@ -179,7 +179,7 @@ public class CompanionDeviceActivity extends FragmentActivity implements // onActivityResult() after the association is created. private @Nullable DeviceFilterPair<?> mSelectedDevice; - private LinearLayoutManager mPermissionsLayoutManager = new LinearLayoutManager(this); + private final LinearLayoutManager mPermissionsLayoutManager = new LinearLayoutManager(this); @Override public void onCreate(Bundle savedInstanceState) { @@ -484,10 +484,18 @@ public class CompanionDeviceActivity extends FragmentActivity implements } title = getHtmlFromResources(this, PROFILE_TITLES.get(deviceProfile), deviceName); + + if (PROFILE_SUMMARIES.containsKey(deviceProfile)) { + final int summaryResourceId = PROFILE_SUMMARIES.get(deviceProfile); + final Spanned summary = getHtmlFromResources(this, summaryResourceId, + deviceName); + mSummary.setText(summary); + } else { + mSummary.setVisibility(View.GONE); + } + setupPermissionList(deviceProfile); - // Summary is not needed for selfManaged dialog. - mSummary.setVisibility(View.GONE); mTitle.setText(title); mVendorHeaderName.setText(vendorName); mVendorHeader.setVisibility(View.VISIBLE); @@ -692,6 +700,11 @@ public class CompanionDeviceActivity extends FragmentActivity implements private void setupPermissionList(String deviceProfile) { final List<Integer> permissionTypes = new ArrayList<>( PROFILE_PERMISSIONS.get(deviceProfile)); + if (permissionTypes.isEmpty()) { + // Nothing to do if there are no permission types. + return; + } + mPermissionListAdapter.setPermissionType(permissionTypes); mPermissionListRecyclerView.setAdapter(mPermissionListAdapter); mPermissionListRecyclerView.setLayoutManager(mPermissionsLayoutManager); diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java index 23a11d618085..dc68bccc8f0a 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java @@ -27,11 +27,13 @@ import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import static java.util.Collections.unmodifiableMap; import static java.util.Collections.unmodifiableSet; +import android.companion.virtual.flags.Flags; import android.os.Build; import android.util.ArrayMap; import android.util.ArraySet; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -122,10 +124,19 @@ final class CompanionDeviceResources { static final Map<String, Integer> PROFILE_TITLES; static { final Map<String, Integer> map = new ArrayMap<>(); - map.put(DEVICE_PROFILE_APP_STREAMING, R.string.title_app_streaming); + if (Flags.interactiveScreenMirror()) { + map.put(DEVICE_PROFILE_APP_STREAMING, R.string.title_app_streaming_with_mirroring); + } else { + map.put(DEVICE_PROFILE_APP_STREAMING, R.string.title_app_streaming); + } map.put(DEVICE_PROFILE_AUTOMOTIVE_PROJECTION, R.string.title_automotive_projection); map.put(DEVICE_PROFILE_COMPUTER, R.string.title_computer); - map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, R.string.title_nearby_device_streaming); + if (Flags.interactiveScreenMirror()) { + map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, + R.string.title_nearby_device_streaming_with_mirroring); + } else { + map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, R.string.title_nearby_device_streaming); + } map.put(DEVICE_PROFILE_WATCH, R.string.confirmation_title); map.put(DEVICE_PROFILE_GLASSES, R.string.confirmation_title_glasses); map.put(null, R.string.confirmation_title); @@ -138,6 +149,11 @@ final class CompanionDeviceResources { final Map<String, Integer> map = new ArrayMap<>(); map.put(DEVICE_PROFILE_WATCH, R.string.summary_watch); map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses); + if (Flags.interactiveScreenMirror()) { + map.put(DEVICE_PROFILE_APP_STREAMING, R.string.summary_app_streaming); + map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, + R.string.summary_nearby_device_streaming); + } map.put(null, R.string.summary_generic); PROFILE_SUMMARIES = unmodifiableMap(map); @@ -146,11 +162,16 @@ final class CompanionDeviceResources { static final Map<String, List<Integer>> PROFILE_PERMISSIONS; static { final Map<String, List<Integer>> map = new ArrayMap<>(); - map.put(DEVICE_PROFILE_APP_STREAMING, Arrays.asList(PERMISSION_APP_STREAMING)); map.put(DEVICE_PROFILE_COMPUTER, Arrays.asList( PERMISSION_NOTIFICATION_LISTENER_ACCESS, PERMISSION_STORAGE)); - map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, - Arrays.asList(PERMISSION_NEARBY_DEVICE_STREAMING)); + if (Flags.interactiveScreenMirror()) { + map.put(DEVICE_PROFILE_APP_STREAMING, Collections.emptyList()); + map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, Collections.emptyList()); + } else { + map.put(DEVICE_PROFILE_APP_STREAMING, Arrays.asList(PERMISSION_APP_STREAMING)); + map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, + Arrays.asList(PERMISSION_NEARBY_DEVICE_STREAMING)); + } if (Build.VERSION.SDK_INT > UPSIDE_DOWN_CAKE) { map.put(DEVICE_PROFILE_WATCH, Arrays.asList(PERMISSION_NOTIFICATIONS, PERMISSION_PHONE, PERMISSION_CALL_LOGS, PERMISSION_SMS, PERMISSION_CONTACTS, PERMISSION_CALENDAR, diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java index 8f32dbb86d04..fe0e021b363c 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java @@ -26,6 +26,7 @@ import static com.android.companiondevicemanager.Utils.getHtmlFromResources; import android.annotation.Nullable; import android.companion.AssociationRequest; +import android.companion.virtual.flags.Flags; import android.content.DialogInterface; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; @@ -129,7 +130,9 @@ public class CompanionVendorHelperDialogFragment extends DialogFragment { case DEVICE_PROFILE_APP_STREAMING: title = getHtmlFromResources(getContext(), R.string.helper_title_app_streaming); summary = getHtmlFromResources( - getContext(), R.string.helper_summary_app_streaming, title, displayName); + getContext(), Flags.interactiveScreenMirror() + ? R.string.helper_summary_app_streaming_with_mirroring + : R.string.helper_summary_app_streaming, title, displayName); break; case DEVICE_PROFILE_COMPUTER: diff --git a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java index 5d71b7d98fdc..37b5d408a508 100644 --- a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java +++ b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java @@ -38,15 +38,15 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.AtomicFile; -import android.util.BackgroundThread; -import android.util.LongArrayQueue; import android.util.Slog; import android.util.Xml; +import android.utils.BackgroundThread; +import android.utils.LongArrayQueue; +import android.utils.XmlUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; -import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; diff --git a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java index 9217e7012e7e..0fcec268fe9c 100644 --- a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java +++ b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java @@ -29,7 +29,6 @@ import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; import android.os.Build; import android.os.Environment; -import android.os.FileUtils; import android.os.PowerManager; import android.os.RecoverySystem; import android.os.SystemClock; @@ -42,10 +41,11 @@ import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import android.util.Slog; +import android.utils.ArrayUtils; +import android.utils.FileUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.ArrayUtils; import com.android.server.PackageWatchdog.FailureReasons; import com.android.server.PackageWatchdog.PackageHealthObserver; import com.android.server.PackageWatchdog.PackageHealthObserverImpact; diff --git a/packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java b/packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java new file mode 100644 index 000000000000..fa4d6afc03d3 --- /dev/null +++ b/packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2024 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 android.utils; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.io.File; +import java.util.List; +import java.util.Objects; + +/** + * Copied over from frameworks/base/core/java/com/android/internal/util/ArrayUtils.java + * + * @hide + */ +public class ArrayUtils { + private ArrayUtils() { /* cannot be instantiated */ } + public static final File[] EMPTY_FILE = new File[0]; + + + /** + * Return first index of {@code value} in {@code array}, or {@code -1} if + * not found. + */ + public static <T> int indexOf(@Nullable T[] array, T value) { + if (array == null) return -1; + for (int i = 0; i < array.length; i++) { + if (Objects.equals(array[i], value)) return i; + } + return -1; + } + + /** @hide */ + public static @NonNull File[] defeatNullable(@Nullable File[] val) { + return (val != null) ? val : EMPTY_FILE; + } + + /** + * Checks if given array is null or has zero elements. + */ + public static boolean isEmpty(@Nullable int[] array) { + return array == null || array.length == 0; + } + + /** + * True if the byte array is null or has length 0. + */ + public static boolean isEmpty(@Nullable byte[] array) { + return array == null || array.length == 0; + } + + /** + * Converts from List of bytes to byte array + * @param list + * @return byte[] + */ + public static byte[] toPrimitive(List<byte[]> list) { + if (list.size() == 0) { + return new byte[0]; + } + int byteLen = list.get(0).length; + byte[] array = new byte[list.size() * byteLen]; + for (int i = 0; i < list.size(); i++) { + for (int j = 0; j < list.get(i).length; j++) { + array[i * byteLen + j] = list.get(i)[j]; + } + } + return array; + } + + /** + * Adds value to given array if not already present, providing set-like + * behavior. + */ + public static @NonNull int[] appendInt(@Nullable int[] cur, int val) { + return appendInt(cur, val, false); + } + + /** + * Adds value to given array. + */ + public static @NonNull int[] appendInt(@Nullable int[] cur, int val, + boolean allowDuplicates) { + if (cur == null) { + return new int[] { val }; + } + final int n = cur.length; + if (!allowDuplicates) { + for (int i = 0; i < n; i++) { + if (cur[i] == val) { + return cur; + } + } + } + int[] ret = new int[n + 1]; + System.arraycopy(cur, 0, ret, 0, n); + ret[n] = val; + return ret; + } +} diff --git a/packages/CrashRecovery/services/java/com/android/util/BackgroundThread.java b/packages/CrashRecovery/services/java/com/android/utils/BackgroundThread.java index a6ae68f62f10..afcf6895fd0d 100644 --- a/packages/CrashRecovery/services/java/com/android/util/BackgroundThread.java +++ b/packages/CrashRecovery/services/java/com/android/utils/BackgroundThread.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.util; +package android.utils; import android.annotation.NonNull; import android.os.Handler; diff --git a/packages/CrashRecovery/services/java/com/android/utils/FileUtils.java b/packages/CrashRecovery/services/java/com/android/utils/FileUtils.java new file mode 100644 index 000000000000..e4923bfc4ecb --- /dev/null +++ b/packages/CrashRecovery/services/java/com/android/utils/FileUtils.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2024 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 android.utils; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Bits and pieces copied from hidden API of android.os.FileUtils. + * + * @hide + */ +public class FileUtils { + /** + * Read a text file into a String, optionally limiting the length. + * + * @param file to read (will not seek, so things like /proc files are OK) + * @param max length (positive for head, negative of tail, 0 for no limit) + * @param ellipsis to add of the file was truncated (can be null) + * @return the contents of the file, possibly truncated + * @throws IOException if something goes wrong reading the file + * @hide + */ + public static @Nullable String readTextFile(@Nullable File file, @Nullable int max, + @Nullable String ellipsis) throws IOException { + InputStream input = new FileInputStream(file); + // wrapping a BufferedInputStream around it because when reading /proc with unbuffered + // input stream, bytes read not equal to buffer size is not necessarily the correct + // indication for EOF; but it is true for BufferedInputStream due to its implementation. + BufferedInputStream bis = new BufferedInputStream(input); + try { + long size = file.length(); + if (max > 0 || (size > 0 && max == 0)) { // "head" mode: read the first N bytes + if (size > 0 && (max == 0 || size < max)) max = (int) size; + byte[] data = new byte[max + 1]; + int length = bis.read(data); + if (length <= 0) return ""; + if (length <= max) return new String(data, 0, length); + if (ellipsis == null) return new String(data, 0, max); + return new String(data, 0, max) + ellipsis; + } else if (max < 0) { // "tail" mode: keep the last N + int len; + boolean rolled = false; + byte[] last = null; + byte[] data = null; + do { + if (last != null) rolled = true; + byte[] tmp = last; + last = data; + data = tmp; + if (data == null) data = new byte[-max]; + len = bis.read(data); + } while (len == data.length); + + if (last == null && len <= 0) return ""; + if (last == null) return new String(data, 0, len); + if (len > 0) { + rolled = true; + System.arraycopy(last, len, last, 0, last.length - len); + System.arraycopy(data, 0, last, last.length - len, len); + } + if (ellipsis == null || !rolled) return new String(last); + return ellipsis + new String(last); + } else { // "cat" mode: size unknown, read it all in streaming fashion + ByteArrayOutputStream contents = new ByteArrayOutputStream(); + int len; + byte[] data = new byte[1024]; + do { + len = bis.read(data); + if (len > 0) contents.write(data, 0, len); + } while (len == data.length); + return contents.toString(); + } + } finally { + bis.close(); + input.close(); + } + } + + /** + * Perform an fsync on the given FileOutputStream. The stream at this + * point must be flushed but not yet closed. + * + * @hide + */ + public static boolean sync(FileOutputStream stream) { + try { + if (stream != null) { + stream.getFD().sync(); + } + return true; + } catch (IOException e) { + } + return false; + } + + /** + * List the files in the directory or return empty file. + * + * @hide + */ + public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) { + return (dir != null) ? ArrayUtils.defeatNullable(dir.listFiles()) + : ArrayUtils.EMPTY_FILE; + } +} diff --git a/packages/CrashRecovery/services/java/com/android/util/HandlerExecutor.java b/packages/CrashRecovery/services/java/com/android/utils/HandlerExecutor.java index 948ebcca0263..fdb15e2333d5 100644 --- a/packages/CrashRecovery/services/java/com/android/util/HandlerExecutor.java +++ b/packages/CrashRecovery/services/java/com/android/utils/HandlerExecutor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.util; +package android.utils; import android.annotation.NonNull; import android.os.Handler; diff --git a/packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java b/packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java new file mode 100644 index 000000000000..5cdc2536129a --- /dev/null +++ b/packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2024 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 android.utils; + +import libcore.util.EmptyArray; + +import java.util.NoSuchElementException; + +/** + * Copied from frameworks/base/core/java/android/util/LongArrayQueue.java + * + * @hide + */ +public class LongArrayQueue { + + private long[] mValues; + private int mSize; + private int mHead; + private int mTail; + + private long[] newUnpaddedLongArray(int num) { + return new long[num]; + } + /** + * Initializes a queue with the given starting capacity. + * + * @param initialCapacity the capacity. + */ + public LongArrayQueue(int initialCapacity) { + if (initialCapacity == 0) { + mValues = EmptyArray.LONG; + } else { + mValues = newUnpaddedLongArray(initialCapacity); + } + mSize = 0; + mHead = mTail = 0; + } + + /** + * Initializes a queue with default starting capacity. + */ + public LongArrayQueue() { + this(16); + } + + /** @hide */ + public static int growSize(int currentSize) { + return currentSize <= 4 ? 8 : currentSize * 2; + } + + private void grow() { + if (mSize < mValues.length) { + throw new IllegalStateException("Queue not full yet!"); + } + final int newSize = growSize(mSize); + final long[] newArray = newUnpaddedLongArray(newSize); + final int r = mValues.length - mHead; // Number of elements on and to the right of head. + System.arraycopy(mValues, mHead, newArray, 0, r); + System.arraycopy(mValues, 0, newArray, r, mHead); + mValues = newArray; + mHead = 0; + mTail = mSize; + } + + /** + * Returns the number of elements in the queue. + */ + public int size() { + return mSize; + } + + /** + * Removes all elements from this queue. + */ + public void clear() { + mSize = 0; + mHead = mTail = 0; + } + + /** + * Adds a value to the tail of the queue. + * + * @param value the value to be added. + */ + public void addLast(long value) { + if (mSize == mValues.length) { + grow(); + } + mValues[mTail] = value; + mTail = (mTail + 1) % mValues.length; + mSize++; + } + + /** + * Removes an element from the head of the queue. + * + * @return the element at the head of the queue. + * @throws NoSuchElementException if the queue is empty. + */ + public long removeFirst() { + if (mSize == 0) { + throw new NoSuchElementException("Queue is empty!"); + } + final long ret = mValues[mHead]; + mHead = (mHead + 1) % mValues.length; + mSize--; + return ret; + } + + /** + * Returns the element at the given position from the head of the queue, where 0 represents the + * head of the queue. + * + * @param position the position from the head of the queue. + * @return the element found at the given position. + * @throws IndexOutOfBoundsException if {@code position} < {@code 0} or + * {@code position} >= {@link #size()} + */ + public long get(int position) { + if (position < 0 || position >= mSize) { + throw new IndexOutOfBoundsException("Index " + position + + " not valid for a queue of size " + mSize); + } + final int index = (mHead + position) % mValues.length; + return mValues[index]; + } + + /** + * Returns the element at the head of the queue, without removing it. + * + * @return the element at the head of the queue. + * @throws NoSuchElementException if the queue is empty + */ + public long peekFirst() { + if (mSize == 0) { + throw new NoSuchElementException("Queue is empty!"); + } + return mValues[mHead]; + } + + /** + * Returns the element at the tail of the queue. + * + * @return the element at the tail of the queue. + * @throws NoSuchElementException if the queue is empty. + */ + public long peekLast() { + if (mSize == 0) { + throw new NoSuchElementException("Queue is empty!"); + } + final int index = (mTail == 0) ? mValues.length - 1 : mTail - 1; + return mValues[index]; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + if (mSize <= 0) { + return "{}"; + } + + final StringBuilder buffer = new StringBuilder(mSize * 64); + buffer.append('{'); + buffer.append(get(0)); + for (int i = 1; i < mSize; i++) { + buffer.append(", "); + buffer.append(get(i)); + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java b/packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java new file mode 100644 index 000000000000..dbbef61f6777 --- /dev/null +++ b/packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2024 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 android.utils; + +import android.annotation.NonNull; +import android.system.ErrnoException; +import android.system.Os; + +import com.android.modules.utils.TypedXmlPullParser; + +import libcore.util.XmlObjectFactory; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Copied over partly from frameworks/base/core/java/com/android/internal/util/XmlUtils.java + * + * @hide + */ +public class XmlUtils { + + private static final String STRING_ARRAY_SEPARATOR = ":"; + + /** @hide */ + public static final void beginDocument(XmlPullParser parser, String firstElementName) + throws XmlPullParserException, IOException { + int type; + while ((type = parser.next()) != parser.START_TAG + && type != parser.END_DOCUMENT) { + // Do nothing + } + + if (type != parser.START_TAG) { + throw new XmlPullParserException("No start tag found"); + } + + if (!parser.getName().equals(firstElementName)) { + throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() + + ", expected " + firstElementName); + } + } + + /** @hide */ + public static boolean nextElementWithin(XmlPullParser parser, int outerDepth) + throws IOException, XmlPullParserException { + for (;;) { + int type = parser.next(); + if (type == XmlPullParser.END_DOCUMENT + || (type == XmlPullParser.END_TAG && parser.getDepth() == outerDepth)) { + return false; + } + if (type == XmlPullParser.START_TAG + && parser.getDepth() == outerDepth + 1) { + return true; + } + } + } + + private static XmlPullParser newPullParser() { + try { + XmlPullParser parser = XmlObjectFactory.newXmlPullParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + return parser; + } catch (XmlPullParserException e) { + throw new AssertionError(); + } + } + + /** @hide */ + public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in) + throws IOException { + final byte[] magic = new byte[4]; + if (in instanceof FileInputStream) { + try { + Os.pread(((FileInputStream) in).getFD(), magic, 0, magic.length, 0); + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } else { + if (!in.markSupported()) { + in = new BufferedInputStream(in); + } + in.mark(8); + in.read(magic); + in.reset(); + } + + final TypedXmlPullParser xml; + xml = (TypedXmlPullParser) newPullParser(); + try { + xml.setInput(in, "UTF_8"); + } catch (XmlPullParserException e) { + throw new IOException(e); + } + return xml; + } +} diff --git a/packages/CredentialManager/wear/res/values/strings.xml b/packages/CredentialManager/wear/res/values/strings.xml index 9480e64aa134..ee8bb786c3f8 100644 --- a/packages/CredentialManager/wear/res/values/strings.xml +++ b/packages/CredentialManager/wear/res/values/strings.xml @@ -21,9 +21,6 @@ <!-- Title of a screen prompting if the user would like to use their saved passkey. [CHAR LIMIT=80] --> <string name="use_passkey_title">Use passkey?</string> - <!-- Title of a screen prompting if the user would like to use their saved passkey. -[CHAR LIMIT=80] --> - <string name="use_sign_in_with_provider_title">Use your sign in for %1$s</string> <!-- Title of a screen prompting if the user would like to sign in with provider [CHAR LIMIT=80] --> <string name="use_password_title">Use password?</string> @@ -35,6 +32,8 @@ <string name="dialog_sign_in_options_button">Sign-in Options</string> <!-- Title for multiple credentials folded screen. [CHAR LIMIT=NONE] --> <string name="sign_in_options_title">Sign-in Options</string> + <!-- Provider settings list title. [CHAR LIMIT=NONE] --> + <string name="provider_list_title">Manage sign-ins</string> <!-- Title for multiple credentials screen. [CHAR LIMIT=NONE] --> <string name="choose_sign_in_title">Choose a sign in</string> <!-- Title for multiple credentials screen with only passkeys. [CHAR LIMIT=NONE] --> diff --git a/packages/CredentialManager/wear/robotests/Android.bp b/packages/CredentialManager/wear/robotests/Android.bp new file mode 100644 index 000000000000..c0a1822a771f --- /dev/null +++ b/packages/CredentialManager/wear/robotests/Android.bp @@ -0,0 +1,28 @@ +package { + // See: http://go/android-license-faq + default_applicable_licenses: ["frameworks_base_license"], +} + +android_robolectric_test { + name: "CredentialSelectorTests", + srcs: [ + "src/**/*.kt", + ], + // Include test libraries. + instrumentation_for: "ClockworkCredentialManager", + libs: [ + "androidx.test.runner", + "androidx.test.ext.junit", + "kotlinx_coroutines_android", + "kotlinx_coroutines", + "kotlinx-coroutines-core", + "kotlinx_coroutines_test", + "mockito-robolectric-prebuilt", + "mockito-kotlin2", + "CredentialManagerShared", + "ClockworkCredentialManager", + "framework_graphics_flags_java_lib", + ], + java_resource_dirs: ["config"], + upstream: true, +} diff --git a/packages/CredentialManager/wear/robotests/config/robolectric.properties b/packages/CredentialManager/wear/robotests/config/robolectric.properties new file mode 100644 index 000000000000..140e42b65c9a --- /dev/null +++ b/packages/CredentialManager/wear/robotests/config/robolectric.properties @@ -0,0 +1,16 @@ +# Copyright (C) 2024 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. +# +sdk=NEWEST_SDK + diff --git a/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorUiStateGetMapperTest.kt b/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorUiStateGetMapperTest.kt new file mode 100644 index 000000000000..3422d3dc4d94 --- /dev/null +++ b/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorUiStateGetMapperTest.kt @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.credentialmanager + +import java.time.Instant +import android.graphics.drawable.Drawable +import com.android.credentialmanager.model.get.CredentialEntryInfo +import com.android.credentialmanager.model.get.ActionEntryInfo +import com.android.credentialmanager.model.get.AuthenticationEntryInfo +import com.android.credentialmanager.model.Request +import androidx.test.filters.SmallTest +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.mockito.kotlin.mock +import org.junit.runner.RunWith +import com.android.credentialmanager.model.CredentialType +import com.google.common.truth.Truth.assertThat +import com.android.credentialmanager.ui.mappers.toGet +import com.android.credentialmanager.model.get.ProviderInfo +import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry.PerUserNameEntries + +/** Unit tests for [CredentialSelectorUiStateGetMapper]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class CredentialSelectorUiStateGetMapperTest { + + private val mDrawable = mock<Drawable>() + + private val actionEntryInfo = + ActionEntryInfo( + providerId = "", + entryKey = "", + entrySubkey = "", + pendingIntent = null, + fillInIntent = null, + title = "title", + icon = mDrawable, + subTitle = "subtitle", + ) + + private val authenticationEntryInfo = + AuthenticationEntryInfo( + providerId = "", + entryKey = "", + entrySubkey = "", + pendingIntent = null, + fillInIntent = null, + title = "title", + providerDisplayName = "", + icon = mDrawable, + isUnlockedAndEmpty = true, + isLastUnlocked = true + ) + + val passkeyCredentialEntryInfo = + createCredentialEntryInfo(credentialType = CredentialType.PASSKEY, userName = "userName") + + val unknownCredentialEntryInfo = + createCredentialEntryInfo(credentialType = CredentialType.UNKNOWN, userName = "userName2") + + val passwordCredentialEntryInfo = + createCredentialEntryInfo(credentialType = CredentialType.PASSWORD, userName = "userName") + + val recentlyUsedPasskeyCredential = + createCredentialEntryInfo(credentialType = + CredentialType.PASSKEY, lastUsedTimeMillis = 2L, userName = "userName") + + val recentlyUsedPasswordCredential = + createCredentialEntryInfo(credentialType = + CredentialType.PASSWORD, lastUsedTimeMillis = 2L, userName = "userName") + + val credentialList1 = listOf( + passkeyCredentialEntryInfo, + passwordCredentialEntryInfo + ) + + val credentialList2 = listOf( + passkeyCredentialEntryInfo, + passwordCredentialEntryInfo, + recentlyUsedPasskeyCredential, + unknownCredentialEntryInfo, + recentlyUsedPasswordCredential + ) + + @Test + fun `On primary screen, just one account returns SingleEntry`() { + val getCredentialUiState = Request.Get( + token = null, + resultReceiver = null, + finalResponseReceiver = null, + providerInfos = listOf(createProviderInfo(credentialList1))).toGet(isPrimary = true) + + assertThat(getCredentialUiState).isEqualTo( + CredentialSelectorUiState.Get.SingleEntry(passkeyCredentialEntryInfo) + ) // prefer passkey over password for selected credential + } + + @Test + fun `On primary screen, multiple accounts returns SingleEntryPerAccount`() { + val getCredentialUiState = Request.Get( + token = null, + resultReceiver = null, + finalResponseReceiver = null, + providerInfos = listOf(createProviderInfo(listOf(passkeyCredentialEntryInfo, + unknownCredentialEntryInfo)))).toGet(isPrimary = true) + + assertThat(getCredentialUiState).isEqualTo( + CredentialSelectorUiState.Get.SingleEntryPerAccount( + sortedEntries = listOf( + passkeyCredentialEntryInfo, // userName + unknownCredentialEntryInfo // userName2 + ), + authenticationEntryList = listOf(authenticationEntryInfo) + )) // prefer passkey from account 1, then unknown from account 2 + } + + @Test + fun `On secondary screen, a MultipleEntry is returned`() { + val getCredentialUiState = Request.Get( + token = null, + resultReceiver = null, + finalResponseReceiver = null, + providerInfos = listOf(createProviderInfo(credentialList1))).toGet(isPrimary = false) + + assertThat(getCredentialUiState).isEqualTo( + CredentialSelectorUiState.Get.MultipleEntry( + listOf(PerUserNameEntries("userName", listOf( + passkeyCredentialEntryInfo, + passwordCredentialEntryInfo)) + ), + listOf(actionEntryInfo), + listOf(authenticationEntryInfo) + )) + } + + @Test + fun `Returned multiple entry is sorted by credentialType and lastUsedTimeMillis`() { + val getCredentialUiState = Request.Get( + token = null, + resultReceiver = null, + finalResponseReceiver = null, + providerInfos = listOf(createProviderInfo(credentialList1), + createProviderInfo(credentialList2))).toGet(isPrimary = false) + + assertThat(getCredentialUiState).isEqualTo( + CredentialSelectorUiState.Get.MultipleEntry( + listOf( + PerUserNameEntries("userName", + listOf( + recentlyUsedPasskeyCredential, // from provider 2 + passkeyCredentialEntryInfo, // from provider 1 or 2 + passkeyCredentialEntryInfo, // from provider 1 or 2 + recentlyUsedPasswordCredential, // from provider 2 + passwordCredentialEntryInfo, // from provider 1 or 2 + passwordCredentialEntryInfo, // from provider 1 or 2 + )), + PerUserNameEntries("userName2", listOf(unknownCredentialEntryInfo)), + ), + listOf(actionEntryInfo, actionEntryInfo), + listOf(authenticationEntryInfo, authenticationEntryInfo) + ) + ) + } + + fun createCredentialEntryInfo( + userName: String, + credentialType: CredentialType = CredentialType.PASSKEY, + lastUsedTimeMillis: Long = 0L + ): CredentialEntryInfo = + CredentialEntryInfo( + providerId = "", + entryKey = "", + entrySubkey = "", + pendingIntent = null, + fillInIntent = null, + credentialType = credentialType, + rawCredentialType = "", + credentialTypeDisplayName = "", + providerDisplayName = "", + userName = userName, + displayName = "", + icon = mDrawable, + shouldTintIcon = false, + lastUsedTimeMillis = Instant.ofEpochMilli(lastUsedTimeMillis), + isAutoSelectable = true, + entryGroupId = "", + isDefaultIconPreferredAsSingleProvider = false, + affiliatedDomain = "", + ) + + fun createProviderInfo(credentials: List<CredentialEntryInfo> = listOf()): ProviderInfo = + ProviderInfo( + id = "providerInfo", + icon = mDrawable, + displayName = "displayName", + credentialEntryList = credentials, + authenticationEntryList = listOf(authenticationEntryInfo), + remoteEntry = null, + actionEntryList = listOf(actionEntryInfo) + ) +} diff --git a/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorViewModelTest.kt b/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorViewModelTest.kt new file mode 100644 index 000000000000..b79f34c54f51 --- /dev/null +++ b/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorViewModelTest.kt @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.credentialmanager + +import org.mockito.kotlin.whenever +import com.android.credentialmanager.model.EntryInfo +import com.android.credentialmanager.model.Request +import androidx.test.filters.SmallTest +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.Before +import java.util.Collections.emptyList +import org.junit.runner.RunWith +import android.content.Intent +import com.android.credentialmanager.client.CredentialManagerClient +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import android.credentials.selection.BaseDialogResult +import com.google.common.truth.Truth.assertThat +import org.mockito.kotlin.doReturn +import kotlinx.coroutines.Job +import org.junit.After +import org.robolectric.shadows.ShadowLooper +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.ExperimentalCoroutinesApi + +/** Unit tests for [CredentialSelectorViewModel]. */ +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class CredentialSelectorViewModelTest { + private val testScope = TestScope(UnconfinedTestDispatcher()) + + private val stateFlow: MutableStateFlow<Request?> = MutableStateFlow(Request.Create(null)) + private val credentialManagerClient = mock<CredentialManagerClient>{ + on { requests } doReturn stateFlow + } + private val mViewModel = CredentialSelectorViewModel(credentialManagerClient) + private lateinit var job: Job + + val testEntryInfo = + EntryInfo( + providerId = "", + entryKey = "", + entrySubkey = "", + pendingIntent = null, + fillInIntent = null, + shouldTerminateUiUponSuccessfulProviderResult = true) + + @Before + fun setUp() { + job = checkNotNull(mViewModel).uiState.launchIn(testScope) + } + + @After + fun teardown() { + job.cancel() + } + + @Test + fun `Setting state to idle when receiving null request`() { + stateFlow.value = null + ShadowLooper.idleMainLooper() + + assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Idle) + } + + @Test + fun `Setting state to cancel when receiving Cancel request`() { + stateFlow.value = Request.Cancel(appName = "appName", token = null) + ShadowLooper.idleMainLooper() + + assertThat(mViewModel.uiState.value) + .isEqualTo(CredentialSelectorUiState.Cancel("appName")) + } + + @Test + fun `Setting state to create when receiving Create request`() { + stateFlow.value = Request.Create(token = null) + ShadowLooper.idleMainLooper() + + assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Create) + } + + @Test + fun `Closing app when receiving Close request`() { + stateFlow.value = Request.Close(token = null) + ShadowLooper.idleMainLooper() + + assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close) + } + + @Test + fun `Updates request`() { + val intent = Intent() + + mViewModel.updateRequest(intent) + + verify(credentialManagerClient).updateRequest(intent) + } + + @Test + fun `Back on a single entry screen closes app`() { + mViewModel.openSecondaryScreen() + stateFlow.value = Request.Get( + token = null, + resultReceiver = null, + finalResponseReceiver = null, + providerInfos = emptyList()) + + mViewModel.back() + ShadowLooper.idleMainLooper() + + assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close) + } + + @Test + fun `Back on a multiple entry screen gets us back to a primary screen`() { + mViewModel.openSecondaryScreen() + stateFlow.value = Request.Get( + token = null, + resultReceiver = null, + finalResponseReceiver = null, + providerInfos = emptyList()) + + mViewModel.back() + ShadowLooper.idleMainLooper() + + assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close) + } + + @Test + fun `Back on create request state closes app`() { + stateFlow.value = Request.Create(token = null) + + mViewModel.back() + ShadowLooper.idleMainLooper() + + assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close) + } + + @Test + fun `Back on close request state closes app`() { + stateFlow.value = Request.Close(token = null) + + mViewModel.back() + ShadowLooper.idleMainLooper() + + assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close) + } + + @Test + fun `Back on cancel request state closes app`() { + stateFlow.value = Request.Cancel(appName = "", token = null) + + mViewModel.back() + ShadowLooper.idleMainLooper() + + assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close) + } + + @Test + fun `Back on idle request state closes app`() { + stateFlow.value = null + + mViewModel.back() + ShadowLooper.idleMainLooper() + + assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close) + } + + @Test + fun `Cancel closes the app`() { + mViewModel.cancel() + ShadowLooper.idleMainLooper() + + verify(credentialManagerClient).sendError(BaseDialogResult.RESULT_CODE_DIALOG_USER_CANCELED) + assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close) + } + + @Test + fun `Send entry selection result closes app and calls client method`() { + whenever(credentialManagerClient.sendEntrySelectionResult( + entryInfo = testEntryInfo, + resultCode = null, + resultData = null, + isAutoSelected = false + )).thenReturn(true) + + mViewModel.sendSelectionResult( + entryInfo = testEntryInfo, + resultCode = null, + resultData = null, + isAutoSelected = false) + ShadowLooper.idleMainLooper() + + verify(credentialManagerClient).sendEntrySelectionResult( + testEntryInfo, null, null, false + ) + assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close) + } + + @Test + fun `Send entry selection result does not close app on false return`() { + whenever(credentialManagerClient.sendEntrySelectionResult( + entryInfo = testEntryInfo, + resultCode = null, + resultData = null, + isAutoSelected = false + )).thenReturn(false) + stateFlow.value = Request.Create(null) + + mViewModel.sendSelectionResult(entryInfo = testEntryInfo, resultCode = null, + resultData = null, isAutoSelected = false) + ShadowLooper.idleMainLooper() + + verify(credentialManagerClient).sendEntrySelectionResult( + entryInfo = testEntryInfo, + resultCode = null, + resultData = null, + isAutoSelected = false + ) + assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Create) + } +} diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index 66be7ba5e079..9d9776301518 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -59,8 +59,10 @@ class CredentialSelectorViewModel @Inject constructor( isPrimaryScreen, shouldClose ) { request, isPrimary, shouldClose -> + Log.d(TAG, "Request updated: " + request?.toString() + + " isClose: " + shouldClose.toString() + + " isPrimaryScreen: " + isPrimary.toString()) if (shouldClose) { - Log.d(TAG, "Request finished, closing ") return@combine Close } @@ -139,7 +141,10 @@ sealed class CredentialSelectorUiState { data object Idle : CredentialSelectorUiState() sealed class Get : CredentialSelectorUiState() { data class SingleEntry(val entry: CredentialEntryInfo) : Get() - data class SingleEntryPerAccount(val sortedEntries: List<CredentialEntryInfo>) : Get() + data class SingleEntryPerAccount( + val sortedEntries: List<CredentialEntryInfo>, + val authenticationEntryList: List<AuthenticationEntryInfo>, + ) : Get() data class MultipleEntry( val accounts: List<PerUserNameEntries>, val actionEntryList: List<ActionEntryInfo>, diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt index 405de1d3acc6..bf4c988679b9 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt @@ -29,6 +29,7 @@ import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState import com.android.credentialmanager.CredentialSelectorUiState +import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntryPerAccount import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntry import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry import com.android.credentialmanager.CredentialSelectorViewModel @@ -45,6 +46,8 @@ import com.google.android.horologist.compose.navscaffold.scrollable import com.android.credentialmanager.model.CredentialType import com.android.credentialmanager.model.EntryInfo import com.android.credentialmanager.ui.screens.multiple.MultiCredentialsFoldScreen +import com.android.credentialmanager.ui.screens.multiple.MultiCredentialsFlattenScreen + @OptIn(ExperimentalHorologistApi::class) @Composable @@ -78,59 +81,70 @@ fun WearApp( scrollable(Screen.SinglePasskeyScreen.route) { SinglePasskeyScreen( - credentialSelectorUiState = viewModel.uiState.value as SingleEntry, + entry = (remember { uiState } as SingleEntry).entry, columnState = it.columnState, + flowEngine = flowEngine, ) } scrollable(Screen.SignInWithProviderScreen.route) { SignInWithProviderScreen( - credentialSelectorUiState = viewModel.uiState.value as SingleEntry, + entry = (remember { uiState } as SingleEntry).entry, columnState = it.columnState, + flowEngine = flowEngine, ) } scrollable(Screen.MultipleCredentialsScreenFold.route) { MultiCredentialsFoldScreen( - credentialSelectorUiState = viewModel.uiState.value as MultipleEntry, - screenIcon = null, + credentialSelectorUiState = (remember { uiState } as SingleEntryPerAccount), columnState = it.columnState, + flowEngine = flowEngine, ) } - } - BackHandler(true) { - viewModel.back() - } - Log.d(TAG, "uiState change, state: $uiState") - when (val state = uiState) { - CredentialSelectorUiState.Idle -> { - if (navController.currentDestination?.route != Screen.Loading.route) { - navController.navigateToLoading() - } - } - is CredentialSelectorUiState.Get -> { - handleGetNavigation( - navController = navController, - state = state, - onCloseApp = onCloseApp, - selectEntry = selectEntry + + scrollable(Screen.MultipleCredentialsScreenFlatten.route) { + MultiCredentialsFlattenScreen( + credentialSelectorUiState = (remember { uiState } as MultipleEntry), + columnState = it.columnState, + flowEngine = flowEngine, ) } - - CredentialSelectorUiState.Create -> { - // TODO: b/301206624 - Implement create flow - onCloseApp() + } + BackHandler(true) { + viewModel.back() } + Log.d(TAG, "uiState change, state: $uiState") + when (val state = uiState) { + CredentialSelectorUiState.Idle -> { + if (navController.currentDestination?.route != Screen.Loading.route) { + navController.navigateToLoading() + } + } - is CredentialSelectorUiState.Cancel -> { - onCloseApp() - } + is CredentialSelectorUiState.Get -> { + handleGetNavigation( + navController = navController, + state = state, + onCloseApp = onCloseApp, + selectEntry = selectEntry + ) + } + + CredentialSelectorUiState.Create -> { + // TODO: b/301206624 - Implement create flow + onCloseApp() + } - CredentialSelectorUiState.Close -> { - onCloseApp() + is CredentialSelectorUiState.Cancel -> { + onCloseApp() + } + + CredentialSelectorUiState.Close -> { + onCloseApp() + } } } -} private fun handleGetNavigation( navController: NavController, @@ -157,13 +171,12 @@ private fun handleGetNavigation( } } - is MultipleEntry -> { - navController.navigateToMultipleCredentialsFoldScreen() - } + is SingleEntryPerAccount -> { + navController.navigateToMultipleCredentialsFoldScreen() + } - else -> { - // TODO: b/301206470 - Implement other get flows - onCloseApp() + is MultipleEntry -> { + navController.navigateToMultipleCredentialsFlattenScreen() + } } } -} diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt index 03b0931f3eb4..7a936b603ec1 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt @@ -32,11 +32,14 @@ fun Request.Get.toGet(isPrimary: Boolean): CredentialSelectorUiState.Get { return if (isPrimary) { if (accounts.size == 1) { CredentialSelectorUiState.Get.SingleEntry( - accounts[0].value.minWith(comparator) + entry = accounts[0].value.minWith(comparator) ) } else { CredentialSelectorUiState.Get.SingleEntryPerAccount( - accounts.map { it.value.minWith(comparator) }.sortedWith(comparator) + sortedEntries = accounts.map { + it.value.minWith(comparator) + }.sortedWith(comparator), + authenticationEntryList = providerInfos.flatMap { it.authenticationEntryList } ) } } else { diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt index 11188b436a05..d54103cd66e8 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt @@ -15,112 +15,53 @@ */ package com.android.credentialmanager.ui.screens.multiple -import android.graphics.drawable.Drawable -import com.android.credentialmanager.ui.screens.UiState -import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController import androidx.wear.compose.material.MaterialTheme import androidx.wear.compose.material.Text import com.android.credentialmanager.ui.components.SignInHeader import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry +import com.android.credentialmanager.FlowEngine import com.android.credentialmanager.R -import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract -import com.android.credentialmanager.model.get.ActionEntryInfo import com.android.credentialmanager.model.get.CredentialEntryInfo import com.android.credentialmanager.ui.components.CredentialsScreenChip import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.compose.layout.ScalingLazyColumn import com.google.android.horologist.compose.layout.ScalingLazyColumnState - /** * Screen that shows multiple credentials to select from, grouped by accounts * * @param credentialSelectorUiState The app bar view model. - * @param screenIcon The view model corresponding to the home page. * @param columnState ScalingLazyColumn configuration to be be applied * @param modifier styling for composable - * @param viewModel ViewModel that updates ui state for this screen - * @param navController handles navigation events from this screen + * @param flowEngine [FlowEngine] that updates ui state for this screen */ @OptIn(ExperimentalHorologistApi::class) @Composable fun MultiCredentialsFlattenScreen( credentialSelectorUiState: MultipleEntry, - screenIcon: Drawable?, - columnState: ScalingLazyColumnState, - modifier: Modifier = Modifier, - viewModel: MultiCredentialsFlattenViewModel = hiltViewModel(), - navController: NavHostController = rememberNavController(), -) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - - when (val state = uiState) { - UiState.CredentialScreen -> { - MultiCredentialsFlattenScreen( - state = credentialSelectorUiState, - columnState = columnState, - screenIcon = screenIcon, - onActionEntryClicked = viewModel::onActionEntryClicked, - onCredentialClicked = viewModel::onCredentialClicked, - modifier = modifier, - ) - } - - is UiState.CredentialSelected -> { - val launcher = rememberLauncherForActivityResult( - StartBalIntentSenderForResultContract() - ) { - viewModel.onInfoRetrieved(it.resultCode, null) - } - - SideEffect { - state.intentSenderRequest?.let { - launcher.launch(it) - } - } - } - - UiState.Cancel -> { - navController.popBackStack() - } - } -} - -@OptIn(ExperimentalHorologistApi::class) -@Composable -fun MultiCredentialsFlattenScreen( - state: MultipleEntry, columnState: ScalingLazyColumnState, - screenIcon: Drawable?, - onActionEntryClicked: (entryInfo: ActionEntryInfo) -> Unit, - onCredentialClicked: (entryInfo: CredentialEntryInfo) -> Unit, - modifier: Modifier, + flowEngine: FlowEngine, ) { + val selectEntry = flowEngine.getEntrySelector() ScalingLazyColumn( columnState = columnState, - modifier = modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) { item { // make this credential specific if all credentials are same SignInHeader( - icon = screenIcon, + icon = null, title = stringResource(R.string.sign_in_options_title), ) } - state.accounts.forEach { userNameEntries -> + credentialSelectorUiState.accounts.forEach { userNameEntries -> item { Text( text = userNameEntries.userName, @@ -135,17 +76,16 @@ fun MultiCredentialsFlattenScreen( item { CredentialsScreenChip( label = credential.userName, - onClick = { onCredentialClicked(credential) }, - secondaryLabel = credential.userName, + onClick = { selectEntry(credential, false) }, + secondaryLabel = credential.credentialTypeDisplayName, icon = credential.icon, - modifier = modifier, ) } } } item { Text( - text = "Manage Sign-ins", + text = stringResource(R.string.provider_list_title), modifier = Modifier .padding(top = 6.dp) .padding(horizontal = 10.dp), @@ -153,14 +93,13 @@ fun MultiCredentialsFlattenScreen( ) } - state.actionEntryList.forEach { + credentialSelectorUiState.actionEntryList.forEach {actionEntry -> item { CredentialsScreenChip( - label = it.title, - onClick = { onActionEntryClicked(it) }, + label = actionEntry.title, + onClick = { selectEntry(actionEntry, false) }, secondaryLabel = null, - icon = it.icon, - modifier = modifier, + icon = actionEntry.icon, ) } } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenViewModel.kt deleted file mode 100644 index ee5f3f4799d6..000000000000 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenViewModel.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.credentialmanager.ui.screens.multiple - -import android.content.Intent -import android.credentials.selection.ProviderPendingIntentResponse -import android.credentials.selection.UserSelectionDialogResult -import androidx.lifecycle.ViewModel -import com.android.credentialmanager.client.CredentialManagerClient -import com.android.credentialmanager.ktx.getIntentSenderRequest -import com.android.credentialmanager.model.Request -import com.android.credentialmanager.model.get.ActionEntryInfo -import com.android.credentialmanager.model.get.CredentialEntryInfo -import com.android.credentialmanager.ui.screens.UiState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject - -/** ViewModel for [MultiCredentialsFlattenScreen].*/ -@HiltViewModel -class MultiCredentialsFlattenViewModel @Inject constructor( - private val credentialManagerClient: CredentialManagerClient, -) : ViewModel() { - - private lateinit var requestGet: Request.Get - private lateinit var entryInfo: CredentialEntryInfo - - private val _uiState = - MutableStateFlow<UiState>(UiState.CredentialScreen) - val uiState: StateFlow<UiState> = _uiState - - fun onCredentialClicked(entryInfo: CredentialEntryInfo) { - this.entryInfo = entryInfo - _uiState.value = UiState.CredentialSelected( - intentSenderRequest = entryInfo.getIntentSenderRequest() - ) - } - - fun onCancelClicked() { - _uiState.value = UiState.Cancel - } - - fun onInfoRetrieved( - resultCode: Int? = null, - resultData: Intent? = null, - ) { - val userSelectionDialogResult = UserSelectionDialogResult( - requestGet.token, - entryInfo.providerId, - entryInfo.entryKey, - entryInfo.entrySubkey, - if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null - ) - credentialManagerClient.sendResult(userSelectionDialogResult) - } - - fun onActionEntryClicked(actionEntryInfo: ActionEntryInfo) { - // TODO(b/322797032)to be filled out - } -}
\ No newline at end of file diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt index 5515c8696b87..6f32c9906a1d 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt @@ -16,24 +16,15 @@ package com.android.credentialmanager.ui.screens.multiple -import com.android.credentialmanager.ui.screens.UiState -import android.graphics.drawable.Drawable -import androidx.activity.compose.rememberLauncherForActivityResult import com.android.credentialmanager.R import androidx.compose.ui.res.stringResource import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController import com.android.credentialmanager.CredentialSelectorUiState -import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract +import com.android.credentialmanager.FlowEngine import com.android.credentialmanager.model.get.CredentialEntryInfo import com.android.credentialmanager.ui.components.DismissChip import com.android.credentialmanager.ui.components.CredentialsScreenChip @@ -49,74 +40,22 @@ import com.android.credentialmanager.model.CredentialType * Screen that shows multiple credentials to select from. * * @param credentialSelectorUiState The app bar view model. - * @param screenIcon The view model corresponding to the home page. * @param columnState ScalingLazyColumn configuration to be be applied - * @param modifier styling for composable - * @param viewModel ViewModel that updates ui state for this screen - * @param navController handles navigation events from this screen */ @OptIn(ExperimentalHorologistApi::class) @Composable fun MultiCredentialsFoldScreen( - credentialSelectorUiState: CredentialSelectorUiState.Get.MultipleEntry, - screenIcon: Drawable?, + credentialSelectorUiState: CredentialSelectorUiState.Get.SingleEntryPerAccount, columnState: ScalingLazyColumnState, - modifier: Modifier = Modifier, - viewModel: MultiCredentialsFoldViewModel = hiltViewModel(), - navController: NavHostController = rememberNavController(), -) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - - when (val state = uiState) { - UiState.CredentialScreen -> { - MultiCredentialsFoldScreen( - state = credentialSelectorUiState, - onSignInOptionsClicked = viewModel::onSignInOptionsClicked, - onCredentialClicked = viewModel::onCredentialClicked, - onCancelClicked = viewModel::onCancelClicked, - screenIcon = screenIcon, - columnState = columnState, - modifier = modifier - ) - } - - is UiState.CredentialSelected -> { - val launcher = rememberLauncherForActivityResult( - StartBalIntentSenderForResultContract() - ) { - viewModel.onInfoRetrieved(it.resultCode, null) - } - - SideEffect { - state.intentSenderRequest?.let { - launcher.launch(it) - } - } - } - - UiState.Cancel -> { - navController.popBackStack() - } - } -} - -@OptIn(ExperimentalHorologistApi::class) -@Composable -fun MultiCredentialsFoldScreen( - state: CredentialSelectorUiState.Get.MultipleEntry, - onSignInOptionsClicked: () -> Unit, - onCredentialClicked: (entryInfo: CredentialEntryInfo) -> Unit, - onCancelClicked: () -> Unit, - screenIcon: Drawable?, - columnState: ScalingLazyColumnState, - modifier: Modifier, + flowEngine: FlowEngine, ) { + val selectEntry = flowEngine.getEntrySelector() ScalingLazyColumn( columnState = columnState, - modifier = modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) { // flatten all credentials into one - val credentials = state.accounts.flatMap { it.sortedCredentialEntryList } + val credentials = credentialSelectorUiState.sortedEntries item { var title = stringResource(R.string.choose_sign_in_title) if (credentials.all{ it.credentialType == CredentialType.PASSKEY }) { @@ -126,7 +65,7 @@ fun MultiCredentialsFoldScreen( } SignInHeader( - icon = screenIcon, + icon = null, title = title, modifier = Modifier .padding(top = 6.dp), @@ -137,22 +76,24 @@ fun MultiCredentialsFoldScreen( item { CredentialsScreenChip( label = credential.userName, - onClick = { onCredentialClicked(credential) }, + onClick = { selectEntry(credential, false) }, secondaryLabel = credential.credentialTypeDisplayName, icon = credential.icon, ) } } - state.authenticationEntryList.forEach { authenticationEntryInfo -> + credentialSelectorUiState.authenticationEntryList.forEach { authenticationEntryInfo -> item { LockedProviderChip(authenticationEntryInfo) { - // TODO(b/322797032) invoke LockedProviderScreen here using flow engine - } + selectEntry(authenticationEntryInfo, false) } } } - - item { SignInOptionsChip(onSignInOptionsClicked)} - item { DismissChip(onCancelClicked) } + item { + SignInOptionsChip { flowEngine.openSecondaryScreen() } + } + item { + DismissChip { flowEngine.cancel() } + } } } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldViewModel.kt deleted file mode 100644 index 627a63de0934..000000000000 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldViewModel.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.credentialmanager.ui.screens.multiple - -import android.content.Intent -import android.credentials.selection.ProviderPendingIntentResponse -import android.credentials.selection.UserSelectionDialogResult -import androidx.lifecycle.ViewModel -import com.android.credentialmanager.client.CredentialManagerClient -import com.android.credentialmanager.ktx.getIntentSenderRequest -import com.android.credentialmanager.model.Request -import com.android.credentialmanager.model.get.CredentialEntryInfo -import com.android.credentialmanager.ui.screens.UiState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject - -/** ViewModel for [MultiCredentialsFoldScreen].*/ -@HiltViewModel -class MultiCredentialsFoldViewModel @Inject constructor( - private val credentialManagerClient: CredentialManagerClient, -) : ViewModel() { - - private lateinit var requestGet: Request.Get - private lateinit var entryInfo: CredentialEntryInfo - - private val _uiState = - MutableStateFlow<UiState>(UiState.CredentialScreen) - val uiState: StateFlow<UiState> = _uiState - - fun onCredentialClicked(entryInfo: CredentialEntryInfo) { - this.entryInfo = entryInfo - _uiState.value = UiState.CredentialSelected( - intentSenderRequest = entryInfo.getIntentSenderRequest() - ) - } - - fun onSignInOptionsClicked() { - // TODO(b/322797032) Implement navigation route for single credential screen to multiple - // credentials - } - - fun onCancelClicked() { - _uiState.value = UiState.Cancel - } - - fun onInfoRetrieved( - resultCode: Int? = null, - resultData: Intent? = null, - ) { - val userSelectionDialogResult = UserSelectionDialogResult( - requestGet.token, - entryInfo.providerId, - entryInfo.entryKey, - entryInfo.entrySubkey, - if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null - ) - credentialManagerClient.sendResult(userSelectionDialogResult) - } -} diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt index b2595a1e500c..e79176bdebcf 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt @@ -19,122 +19,62 @@ package com.android.credentialmanager.ui.screens.single.passkey import androidx.compose.foundation.layout.Column -import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController -import com.android.credentialmanager.CredentialSelectorUiState +import com.android.credentialmanager.FlowEngine import com.android.credentialmanager.model.get.CredentialEntryInfo import com.android.credentialmanager.R -import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract import com.android.credentialmanager.ui.components.AccountRow import com.android.credentialmanager.ui.components.ContinueChip import com.android.credentialmanager.ui.components.DismissChip import com.android.credentialmanager.ui.components.SignInHeader import com.android.credentialmanager.ui.components.SignInOptionsChip import com.android.credentialmanager.ui.screens.single.SingleAccountScreen -import com.android.credentialmanager.ui.screens.UiState import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.compose.layout.ScalingLazyColumnState /** * Screen that shows sign in with provider credential. * - * @param credentialSelectorUiState The app bar view model. + * @param entry The password entry * @param columnState ScalingLazyColumn configuration to be be applied to SingleAccountScreen * @param modifier styling for composable - * @param viewModel ViewModel that updates ui state for this screen - * @param navController handles navigation events from this screen + * @param flowEngine [FlowEngine] that updates ui state for this screen */ @OptIn(ExperimentalHorologistApi::class) @Composable fun SinglePasskeyScreen( - credentialSelectorUiState: CredentialSelectorUiState.Get.SingleEntry, - columnState: ScalingLazyColumnState, - modifier: Modifier = Modifier, - viewModel: SinglePasskeyScreenViewModel = hiltViewModel(), - navController: NavHostController = rememberNavController(), -) { - viewModel.initialize(credentialSelectorUiState.entry) - - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - - when (val state = uiState) { - UiState.CredentialScreen -> { - SinglePasskeyScreen( - credentialSelectorUiState.entry, - columnState, - modifier, - viewModel - ) - } - - is UiState.CredentialSelected -> { - val launcher = rememberLauncherForActivityResult( - StartBalIntentSenderForResultContract() - ) { - viewModel.onPasskeyInfoRetrieved(it.resultCode, null) - } - - SideEffect { - state.intentSenderRequest?.let { - launcher.launch(it) - } - } - } - - UiState.Cancel -> { - // TODO(b/322797032) add valid navigation path here for going back - navController.popBackStack() - } - } -} - -@OptIn(ExperimentalHorologistApi::class) -@Composable -fun SinglePasskeyScreen( entry: CredentialEntryInfo, columnState: ScalingLazyColumnState, modifier: Modifier = Modifier, - viewModel: SinglePasskeyScreenViewModel, + flowEngine: FlowEngine, ) { SingleAccountScreen( headerContent = { SignInHeader( icon = entry.icon, - title = stringResource(R.string.use_passkey_title), + title = stringResource(R.string.use_password_title), ) }, accountContent = { - if (entry.displayName != null) { - AccountRow( + AccountRow( primaryText = checkNotNull(entry.displayName), secondaryText = entry.userName, modifier = Modifier.padding(top = 10.dp), ) - } else { - AccountRow( - primaryText = entry.userName, - modifier = Modifier.padding(top = 10.dp), - ) - } }, columnState = columnState, modifier = modifier.padding(horizontal = 10.dp) ) { item { + val selectEntry = flowEngine.getEntrySelector() Column { - ContinueChip(viewModel::onContinueClick) - SignInOptionsChip(viewModel::onSignInOptionsClick) - DismissChip(viewModel::onDismissClick) + ContinueChip { selectEntry(entry, false) } + SignInOptionsChip{ flowEngine.openSecondaryScreen() } + DismissChip { flowEngine.cancel() } } } } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt deleted file mode 100644 index 37ffaca3cf45..000000000000 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.credentialmanager.ui.screens.single.passkey - -import android.content.Intent -import android.credentials.selection.UserSelectionDialogResult -import android.credentials.selection.ProviderPendingIntentResponse -import androidx.annotation.MainThread -import androidx.lifecycle.ViewModel -import com.android.credentialmanager.ktx.getIntentSenderRequest -import com.android.credentialmanager.model.Request -import com.android.credentialmanager.client.CredentialManagerClient -import com.android.credentialmanager.model.get.CredentialEntryInfo -import dagger.hilt.android.lifecycle.HiltViewModel -import com.android.credentialmanager.ui.screens.UiState -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject - -@HiltViewModel -class SinglePasskeyScreenViewModel @Inject constructor( - private val credentialManagerClient: CredentialManagerClient, -) : ViewModel() { - - private val _uiState = - MutableStateFlow<UiState>(UiState.CredentialScreen) - val uiState: StateFlow<UiState> = _uiState - - private lateinit var requestGet: Request.Get - private lateinit var entryInfo: CredentialEntryInfo - - - @MainThread - fun initialize(entry: CredentialEntryInfo) { - this.entryInfo = entry - } - - fun onDismissClick() { - _uiState.value = UiState.Cancel - } - - fun onContinueClick() { - _uiState.value = UiState.CredentialSelected( - intentSenderRequest = entryInfo.getIntentSenderRequest() - ) - } - - fun onSignInOptionsClick() { - } - - fun onPasskeyInfoRetrieved( - resultCode: Int? = null, - resultData: Intent? = null, - ) { - val userSelectionDialogResult = UserSelectionDialogResult( - requestGet.token, - entryInfo.providerId, - entryInfo.entryKey, - entryInfo.entrySubkey, - if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null - ) - credentialManagerClient.sendResult(userSelectionDialogResult) - } -} - diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt index b0ece0d0453a..3a86feb4203b 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt @@ -16,100 +16,43 @@ package com.android.credentialmanager.ui.screens.single.signInWithProvider -import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController -import com.android.credentialmanager.CredentialSelectorUiState +import com.android.credentialmanager.FlowEngine import com.android.credentialmanager.model.get.CredentialEntryInfo -import com.android.credentialmanager.R -import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract import com.android.credentialmanager.ui.components.AccountRow import com.android.credentialmanager.ui.components.ContinueChip import com.android.credentialmanager.ui.components.DismissChip import com.android.credentialmanager.ui.components.SignInHeader import com.android.credentialmanager.ui.components.SignInOptionsChip import com.android.credentialmanager.ui.screens.single.SingleAccountScreen -import com.android.credentialmanager.ui.screens.UiState import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.compose.layout.ScalingLazyColumnState /** * Screen that shows sign in with provider credential. * - * @param credentialSelectorUiState The app bar view model. + * @param entry The password entry. * @param columnState ScalingLazyColumn configuration to be be applied to SingleAccountScreen * @param modifier styling for composable - * @param viewModel ViewModel that updates ui state for this screen - * @param navController handles navigation events from this screen + * @param flowEngine [FlowEngine] that updates ui state for this screen */ @OptIn(ExperimentalHorologistApi::class) @Composable fun SignInWithProviderScreen( - credentialSelectorUiState: CredentialSelectorUiState.Get.SingleEntry, - columnState: ScalingLazyColumnState, - modifier: Modifier = Modifier, - viewModel: SignInWithProviderViewModel = hiltViewModel(), - navController: NavHostController = rememberNavController(), -) { - viewModel.initialize(credentialSelectorUiState.entry) - - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - - when (uiState) { - UiState.CredentialScreen -> { - SignInWithProviderScreen( - credentialSelectorUiState.entry, - columnState, - modifier, - viewModel - ) - } - - is UiState.CredentialSelected -> { - val launcher = rememberLauncherForActivityResult( - StartBalIntentSenderForResultContract() - ) { - viewModel.onInfoRetrieved(it.resultCode, null) - } - - SideEffect { - (uiState as UiState.CredentialSelected).intentSenderRequest?.let { - launcher.launch(it) - } - } - } - - UiState.Cancel -> { - // TODO(b/322797032) add valid navigation path here for going back - navController.popBackStack() - } - } -} - -@OptIn(ExperimentalHorologistApi::class) -@Composable -fun SignInWithProviderScreen( entry: CredentialEntryInfo, columnState: ScalingLazyColumnState, modifier: Modifier = Modifier, - viewModel: SignInWithProviderViewModel, + flowEngine: FlowEngine, ) { SingleAccountScreen( headerContent = { SignInHeader( icon = entry.icon, - title = stringResource(R.string.use_sign_in_with_provider_title, - entry.providerDisplayName), + title = entry.providerDisplayName, ) }, accountContent = { @@ -130,12 +73,13 @@ fun SignInWithProviderScreen( columnState = columnState, modifier = modifier.padding(horizontal = 10.dp) ) { - item { - Column { - ContinueChip(viewModel::onContinueClick) - SignInOptionsChip(viewModel::onSignInOptionsClick) - DismissChip(viewModel::onDismissClick) - } - } + item { + val selectEntry = flowEngine.getEntrySelector() + Column { + ContinueChip { selectEntry(entry, false) } + SignInOptionsChip{ flowEngine.openSecondaryScreen() } + DismissChip { flowEngine.cancel() } + } + } } }
\ No newline at end of file diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderViewModel.kt deleted file mode 100644 index 7ba45e5012e8..000000000000 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderViewModel.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.credentialmanager.ui.screens.single.signInWithProvider - -import android.content.Intent -import android.credentials.selection.ProviderPendingIntentResponse -import android.credentials.selection.UserSelectionDialogResult -import androidx.annotation.MainThread -import androidx.lifecycle.ViewModel -import com.android.credentialmanager.ktx.getIntentSenderRequest -import com.android.credentialmanager.model.Request -import com.android.credentialmanager.client.CredentialManagerClient -import com.android.credentialmanager.model.get.CredentialEntryInfo -import dagger.hilt.android.lifecycle.HiltViewModel -import com.android.credentialmanager.ui.screens.UiState -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject - -/** ViewModel for [SignInWithProviderScreen].*/ -@HiltViewModel -class SignInWithProviderViewModel @Inject constructor( - private val credentialManagerClient: CredentialManagerClient, -) : ViewModel() { - - private val _uiState = - MutableStateFlow<UiState>(UiState.CredentialScreen) - val uiState: StateFlow<UiState> = _uiState - - private lateinit var requestGet: Request.Get - private lateinit var entryInfo: CredentialEntryInfo - - @MainThread - fun initialize(entry: CredentialEntryInfo) { - this.entryInfo = entry - } - - fun onDismissClick() { - _uiState.value = UiState.Cancel - } - - fun onContinueClick() { - _uiState.value = UiState.CredentialSelected( - intentSenderRequest = entryInfo.getIntentSenderRequest() - ) - } - - fun onSignInOptionsClick() { - // TODO(b/322797032) Implement navigation route for single credential screen to multiple - // credentials - } - - fun onInfoRetrieved( - resultCode: Int? = null, - resultData: Intent? = null, - ) { - val userSelectionDialogResult = UserSelectionDialogResult( - requestGet.token, - entryInfo.providerId, - entryInfo.entryKey, - entryInfo.entrySubkey, - if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null - ) - credentialManagerClient.sendResult(userSelectionDialogResult) - } -} - diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt index 0e3949365646..cfdcaff4d34c 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt @@ -26,23 +26,10 @@ import java.util.concurrent.ConcurrentHashMap /** Manager of [BackupRestoreStorage]. */ class BackupRestoreStorageManager private constructor(private val application: Application) { - private val storages = ConcurrentHashMap<String, BackupRestoreStorage>() + private val storageWrappers = ConcurrentHashMap<String, StorageWrapper>() private val executor = MoreExecutors.directExecutor() - private val observer = Observer { reason -> notifyBackupManager(null, reason) } - - private val keyedObserver = - KeyedObserver<Any?> { key, reason -> notifyBackupManager(key, reason) } - - private fun notifyBackupManager(key: Any?, reason: Int) { - // prefer not triggering backup immediately after restore - if (reason == ChangeReason.RESTORE) return - // TODO: log storage name - Log.d(LOG_TAG, "Notify BackupManager data changed for change: key=$key") - BackupManager.dataChanged(application.packageName) - } - /** * Adds all the registered [BackupRestoreStorage] as the helpers of given [BackupAgentHelper]. * @@ -52,7 +39,8 @@ class BackupRestoreStorageManager private constructor(private val application: A */ fun addBackupAgentHelpers(backupAgentHelper: BackupAgentHelper) { val fileStorages = mutableListOf<BackupRestoreFileStorage>() - for ((keyPrefix, storage) in storages) { + for ((keyPrefix, storageWrapper) in storageWrappers) { + val storage = storageWrapper.storage if (storage is BackupRestoreFileStorage) { fileStorages.add(storage) } else { @@ -70,15 +58,8 @@ class BackupRestoreStorageManager private constructor(private val application: A * The observers of the storages will be notified. */ fun onRestoreFinished() { - for (storage in storages.values) { - storage.notifyRestoreFinished() - } - } - - private fun BackupRestoreStorage.notifyRestoreFinished() { - when (this) { - is KeyedObservable<*> -> notifyChange(ChangeReason.RESTORE) - is Observable -> notifyChange(ChangeReason.RESTORE) + for (storageWrapper in storageWrappers.values) { + storageWrapper.notifyRestoreFinished() } } @@ -99,50 +80,82 @@ class BackupRestoreStorageManager private constructor(private val application: A fun add(storage: BackupRestoreStorage) { if (storage is BackupRestoreFileStorage) storage.checkFilePaths() val name = storage.name - val oldStorage = storages.put(name, storage) + val oldStorage = storageWrappers.put(name, StorageWrapper(storage))?.storage if (oldStorage != null) { throw IllegalStateException( "Storage name '$name' conflicts:\n\told: $oldStorage\n\tnew: $storage" ) } - storage.addObserver() - } - - private fun BackupRestoreStorage.addObserver() { - when (this) { - is KeyedObservable<*> -> addObserver(keyedObserver, executor) - is Observable -> addObserver(observer, executor) - else -> - throw IllegalArgumentException( - "$this does not implement either KeyedObservable or Observable" - ) - } } /** Removes all the storages. */ fun removeAll() { - for ((name, _) in storages) remove(name) + for ((name, _) in storageWrappers) remove(name) } /** Removes storage with given name. */ fun remove(name: String): BackupRestoreStorage? { - val storage = storages.remove(name) - storage?.removeObserver() - return storage - } - - private fun BackupRestoreStorage.removeObserver() { - when (this) { - is KeyedObservable<*> -> removeObserver(keyedObserver) - is Observable -> removeObserver(observer) - } + val storageWrapper = storageWrappers.remove(name) + storageWrapper?.removeObserver() + return storageWrapper?.storage } /** Returns storage with given name. */ - fun get(name: String): BackupRestoreStorage? = storages[name] + fun get(name: String): BackupRestoreStorage? = storageWrappers[name]?.storage /** Returns storage with given name, exception is raised if not found. */ - fun getOrThrow(name: String): BackupRestoreStorage = storages[name]!! + fun getOrThrow(name: String): BackupRestoreStorage = storageWrappers[name]!!.storage + + private inner class StorageWrapper(val storage: BackupRestoreStorage) : + Observer, KeyedObserver<Any?> { + init { + when (storage) { + is KeyedObservable<*> -> storage.addObserver(this, executor) + is Observable -> storage.addObserver(this, executor) + else -> + throw IllegalArgumentException( + "$this does not implement either KeyedObservable or Observable" + ) + } + } + + override fun onChanged(reason: Int) = onKeyChanged(null, reason) + + override fun onKeyChanged(key: Any?, reason: Int) { + notifyBackupManager(key, reason) + } + + private fun notifyBackupManager(key: Any?, reason: Int) { + val name = storage.name + // prefer not triggering backup immediately after restore + if (reason == ChangeReason.RESTORE) { + Log.d( + LOG_TAG, + "Notify BackupManager dataChanged ignored for restore: storage=$name key=$key" + ) + return + } + Log.d( + LOG_TAG, + "Notify BackupManager dataChanged: storage=$name key=$key reason=$reason" + ) + BackupManager.dataChanged(application.packageName) + } + + fun removeObserver() { + when (storage) { + is KeyedObservable<*> -> storage.removeObserver(this) + is Observable -> storage.removeObserver(this) + } + } + + fun notifyRestoreFinished() { + when (storage) { + is KeyedObservable<*> -> storage.notifyChange(ChangeReason.RESTORE) + is Observable -> storage.notifyChange(ChangeReason.RESTORE) + } + } + } companion object { @Volatile private var instance: BackupRestoreStorageManager? = null diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/MobileMappings.java b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileMappings.java index 840c9364de32..b7108c98c3fe 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/MobileMappings.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileMappings.java @@ -236,7 +236,8 @@ public class MobileMappings { // Handle specific carrier config values for the default data SIM int defaultDataSubId = SubscriptionManager.from(context) .getDefaultDataSubscriptionId(); - PersistableBundle b = configMgr.getConfigForSubId(defaultDataSubId); + PersistableBundle b = configMgr == null ? null + : configMgr.getConfigForSubId(defaultDataSubId); if (b != null) { config.alwaysShowDataRatIcon = b.getBoolean( CarrierConfigManager.KEY_ALWAYS_SHOW_DATA_RAT_ICON_BOOL); diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java index 53daef1c4112..69c7410818dd 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java @@ -242,6 +242,7 @@ public class CreateUserDialogController { .setMessage(messageResId) .setNegativeButtonText(R.string.cancel) .setPositiveButtonText(R.string.next); + mCustomDialogHelper.requestFocusOnTitle(); break; case GRANT_ADMIN_DIALOG: mEditUserInfoView.setVisibility(View.GONE); @@ -254,6 +255,7 @@ public class CreateUserDialogController { .setMessage(R.string.user_grant_admin_message) .setNegativeButtonText(R.string.back) .setPositiveButtonText(R.string.next); + mCustomDialogHelper.requestFocusOnTitle(); if (mIsAdmin == null) { mCustomDialogHelper.setButtonEnabled(false); } @@ -265,6 +267,7 @@ public class CreateUserDialogController { .setTitle(R.string.user_info_settings_title) .setNegativeButtonText(R.string.back) .setPositiveButtonText(R.string.done); + mCustomDialogHelper.requestFocusOnTitle(); mEditUserInfoView.setVisibility(View.VISIBLE); mGrantAdminView.setVisibility(View.GONE); break; @@ -273,7 +276,6 @@ public class CreateUserDialogController { && mEditUserPhotoController.getNewUserPhotoDrawable() != null) ? mEditUserPhotoController.getNewUserPhotoDrawable() : mSavedDrawable; - String newName = mUserNameView.getText().toString().trim(); String defaultName = mActivity.getString(R.string.user_new_user_name); mUserName = !newName.isEmpty() ? newName : defaultName; diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java b/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java index 5201b3ddc606..4cf3bc23b9a0 100644 --- a/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java +++ b/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java @@ -23,6 +23,7 @@ import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; @@ -282,4 +283,13 @@ public class CustomDialogHelper { } return this; } + + /** + * Requests focus on dialog title when used. Used to let talkback know that the dialog content + * is updated and needs to be read from the beginning. + */ + public void requestFocusOnTitle() { + mDialogTitle.requestFocus(); + mDialogTitle.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt index 56b0bf74574f..1586b8f46ff8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt @@ -23,8 +23,8 @@ import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.AudioStreamModel import com.android.settingslib.volume.shared.model.RingerMode import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map /** Provides audio stream state and an ability to change it */ @@ -43,6 +43,9 @@ class AudioVolumeInteractor( streamModel.copy(volume = processVolume(streamModel, ringerMode, isZenMuted)) } + val ringerMode: StateFlow<RingerMode> + get() = audioRepository.ringerMode + suspend fun setVolume(audioStream: AudioStream, volume: Int) = audioRepository.setVolume(audioStream, volume) @@ -52,9 +55,14 @@ class AudioVolumeInteractor( /** Checks if the volume can be changed via the UI. */ fun canChangeVolume(audioStream: AudioStream): Flow<Boolean> { return if (audioStream.value == AudioManager.STREAM_NOTIFICATION) { - getAudioStream(AudioStream(AudioManager.STREAM_RING)).map { !it.isMuted } + combine( + notificationsSoundPolicyInteractor.isZenMuted(audioStream), + getAudioStream(AudioStream(AudioManager.STREAM_RING)).map { it.isMuted }, + ) { isZenMuted, isRingMuted -> + !isZenMuted && !isRingMuted + } } else { - flowOf(true) + notificationsSoundPolicyInteractor.isZenMuted(audioStream).map { !it } } } diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index da06830357e8..85bdb295cbb2 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -617,3 +617,14 @@ flag { description: "enables new focus outline for qs tiles when focused on with physical keyboard" bug: "312899524" } + +flag { + name: "edgeback_gesture_handler_get_running_tasks_background" + namespace: "systemui" + description: "Decide whether to get the running tasks from activity manager in EdgebackGestureHandler" + " class on the background thread." + bug: "325041960" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt index 5d5f12e8e567..3f57f88a13d3 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt @@ -337,6 +337,7 @@ constructor( if (ghostedView is LaunchableView) { // Restore the ghosted view visibility. ghostedView.setShouldBlockVisibilityChanges(false) + ghostedView.onActivityLaunchAnimationEnd() } else { // Make the ghosted view visible. We ensure that the view is considered VISIBLE by // accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17 diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt index ed8e70568b48..da6ccaa2dd2c 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt @@ -38,6 +38,9 @@ interface LaunchableView { * @param block whether we should block/postpone all calls to `setVisibility`. */ fun setShouldBlockVisibilityChanges(block: Boolean) + + /** Perform an action when the activity launch animation ends */ + fun onActivityLaunchAnimationEnd() {} } /** A delegate that can be used by views to make the implementation of [LaunchableView] easier. */ diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt index ef15c8461b95..9a996496d41c 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt @@ -21,22 +21,25 @@ package com.android.compose import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.SliderState import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -44,17 +47,28 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import com.android.compose.modifiers.padding +import androidx.compose.ui.util.fastFirst +import androidx.compose.ui.util.fastFirstOrNull import com.android.compose.theme.LocalAndroidColorScheme /** @@ -62,15 +76,16 @@ import com.android.compose.theme.LocalAndroidColorScheme * * @param onValueChangeFinished is called when the slider settles on a [value]. This callback * shouldn't be used to react to value changes. Use [onValueChange] instead - * @param interactionSource - the [MutableInteractionSource] representing the stream of Interactions + * @param interactionSource the [MutableInteractionSource] representing the stream of Interactions * for this slider. You can create and pass in your own remembered instance to observe * Interactions and customize the appearance / behavior of this slider in different states. - * @param colors - slider color scheme. - * @param draggingCornersRadius - radius of the slider indicator when the user drags it - * @param icon - icon at the start of the slider. Icon is limited to a square space at the start of - * the slider - * @param label - control shown next to the icon. + * @param colors determine slider color scheme. + * @param draggingCornersRadius is the radius of the slider indicator when the user drags it + * @param icon at the start of the slider. Icon is limited to a square space at the start of the + * slider + * @param label is shown next to the icon. */ +@OptIn(ExperimentalMaterial3Api::class) @Composable fun PlatformSlider( value: Float, @@ -86,7 +101,7 @@ fun PlatformSlider( label: (@Composable (isDragging: Boolean) -> Unit)? = null, ) { val sliderHeight: Dp = 64.dp - val iconWidth: Dp = sliderHeight + val thumbSize: Dp = sliderHeight var isDragging by remember { mutableStateOf(false) } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> @@ -101,16 +116,6 @@ fun PlatformSlider( } } } - val paddingStart by - animateDpAsState( - targetValue = - if ((!isDragging && value == valueRange.start) || icon == null) { - 16.dp - } else { - 0.dp - }, - label = "LabelIconSpacingAnimation" - ) Box(modifier = modifier.height(sliderHeight)) { Slider( @@ -126,130 +131,275 @@ fun PlatformSlider( sliderState = it, enabled = enabled, colors = colors, - iconWidth = iconWidth, draggingCornersRadius = draggingCornersRadius, sliderHeight = sliderHeight, + thumbSize = thumbSize, isDragging = isDragging, - modifier = Modifier, + label = label, + icon = icon, + modifier = Modifier.fillMaxSize(), ) }, - thumb = { Spacer(Modifier.width(iconWidth).height(sliderHeight)) }, + thumb = { Spacer(Modifier.size(thumbSize)) }, ) - if (icon != null || label != null) { - Row(modifier = Modifier.fillMaxSize()) { - icon?.let { iconComposable -> - Box( - modifier = Modifier.fillMaxHeight().aspectRatio(1f), - contentAlignment = Alignment.Center, - ) { - iconComposable(isDragging) - } - } - - label?.let { labelComposable -> - Box( - modifier = - Modifier.fillMaxHeight() - .weight(1f) - .padding( - start = { paddingStart.roundToPx() }, - end = { sliderHeight.roundToPx() / 2 }, - ), - contentAlignment = Alignment.CenterStart, - ) { - labelComposable(isDragging) - } - } - } - } + Spacer( + Modifier.padding(8.dp) + .size(4.dp) + .align(Alignment.CenterEnd) + .background(color = colors.indicatorColor, shape = CircleShape) + ) } } +private enum class TrackComponent(val zIndex: Float) { + Background(0f), + Icon(1f), + Label(1f), +} + @Composable private fun Track( sliderState: SliderState, enabled: Boolean, colors: PlatformSliderColors, - iconWidth: Dp, draggingCornersRadius: Dp, sliderHeight: Dp, + thumbSize: Dp, isDragging: Boolean, + icon: (@Composable (isDragging: Boolean) -> Unit)?, + label: (@Composable (isDragging: Boolean) -> Unit)?, modifier: Modifier = Modifier, ) { val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl - val iconWidthPx: Float - val halfIconWidthPx: Float - val targetIndicatorRadiusPx: Float - val halfSliderHeightPx: Float - with(LocalDensity.current) { - halfSliderHeightPx = sliderHeight.toPx() / 2 - iconWidthPx = iconWidth.toPx() - halfIconWidthPx = iconWidthPx / 2 - targetIndicatorRadiusPx = - if (isDragging) draggingCornersRadius.toPx() else halfSliderHeightPx - } + var drawingState: DrawingState by remember { mutableStateOf(DrawingState()) } + Layout( + modifier = modifier, + content = { + TrackBackground( + modifier = Modifier.layoutId(TrackComponent.Background), + drawingState = drawingState, + enabled = enabled, + colors = colors, + draggingCornersRadiusActive = draggingCornersRadius, + draggingCornersRadiusIdle = sliderHeight / 2, + isDragging = isDragging, + ) + if (icon != null) { + Box( + modifier = Modifier.layoutId(TrackComponent.Icon).clip(CircleShape), + contentAlignment = Alignment.Center, + ) { + CompositionLocalProvider( + LocalContentColor provides + if (enabled) colors.iconColor else colors.disabledIconColor + ) { + icon(isDragging) + } + } + } + if (label != null) { + val offsetX by + animateFloatAsState( + targetValue = + if (enabled) { + if (drawingState.isLabelOnTopOfIndicator) { + drawingState.iconWidth.coerceAtLeast( + LocalDensity.current.run { 16.dp.toPx() } + ) + } else { + val indicatorWidth = + drawingState.indicatorRight - drawingState.indicatorLeft + indicatorWidth + LocalDensity.current.run { 16.dp.toPx() } + } + } else { + drawingState.iconWidth + }, + label = "LabelIconSpacingAnimation" + ) + Box( + modifier = + Modifier.layoutId(TrackComponent.Label).offset { + IntOffset(offsetX.toInt(), 0) + }, + contentAlignment = Alignment.CenterStart, + ) { + CompositionLocalProvider( + LocalContentColor provides + colors.getLabelColor( + isEnabled = enabled, + isLabelOnTopOfTheIndicator = drawingState.isLabelOnTopOfIndicator, + ) + ) { + label(isDragging) + } + } + } + }, + measurePolicy = + TrackMeasurePolicy( + sliderState = sliderState, + thumbSize = LocalDensity.current.run { thumbSize.roundToPx() }, + isRtl = isRtl, + onDrawingStateMeasured = { drawingState = it } + ) + ) +} - val indicatorRadiusPx: Float by - animateFloatAsState( - targetValue = targetIndicatorRadiusPx, +@Composable +private fun TrackBackground( + drawingState: DrawingState, + enabled: Boolean, + colors: PlatformSliderColors, + draggingCornersRadiusActive: Dp, + draggingCornersRadiusIdle: Dp, + isDragging: Boolean, + modifier: Modifier = Modifier, +) { + val indicatorRadiusDp: Dp by + animateDpAsState( + targetValue = + if (isDragging) draggingCornersRadiusActive else draggingCornersRadiusIdle, label = "PlatformSliderCornersAnimation", ) val trackColor = colors.getTrackColor(enabled) val indicatorColor = colors.getIndicatorColor(enabled) - val trackCornerRadius = CornerRadius(halfSliderHeightPx, halfSliderHeightPx) - val indicatorCornerRadius = CornerRadius(indicatorRadiusPx, indicatorRadiusPx) Canvas(modifier.fillMaxSize()) { + val trackCornerRadius = CornerRadius(size.height / 2, size.height / 2) val trackPath = Path() trackPath.addRoundRect( RoundRect( - left = -halfIconWidthPx, + left = 0f, top = 0f, - right = size.width + halfIconWidthPx, - bottom = size.height, + right = drawingState.totalWidth, + bottom = drawingState.totalHeight, cornerRadius = trackCornerRadius, ) ) drawPath(path = trackPath, color = trackColor) + val indicatorCornerRadius = CornerRadius(indicatorRadiusDp.toPx(), indicatorRadiusDp.toPx()) clipPath(trackPath) { val indicatorPath = Path() - if (isRtl) { - indicatorPath.addRoundRect( - RoundRect( - left = - size.width - - size.width * sliderState.coercedNormalizedValue - - halfIconWidthPx, - top = 0f, - right = size.width + iconWidthPx, - bottom = size.height, - topLeftCornerRadius = indicatorCornerRadius, - topRightCornerRadius = trackCornerRadius, - bottomRightCornerRadius = trackCornerRadius, - bottomLeftCornerRadius = indicatorCornerRadius, + indicatorPath.addRoundRect( + RoundRect( + left = drawingState.indicatorLeft, + top = drawingState.indicatorTop, + right = drawingState.indicatorRight, + bottom = drawingState.indicatorBottom, + topLeftCornerRadius = trackCornerRadius, + topRightCornerRadius = indicatorCornerRadius, + bottomRightCornerRadius = indicatorCornerRadius, + bottomLeftCornerRadius = trackCornerRadius, + ) + ) + drawPath(path = indicatorPath, color = indicatorColor) + } + } +} + +/** Measures track components sizes and calls [onDrawingStateMeasured] when it's done. */ +private class TrackMeasurePolicy( + private val sliderState: SliderState, + private val thumbSize: Int, + private val isRtl: Boolean, + private val onDrawingStateMeasured: (DrawingState) -> Unit, +) : MeasurePolicy { + + override fun MeasureScope.measure( + measurables: List<Measurable>, + constraints: Constraints + ): MeasureResult { + // Slider adds a paddings to the Track to make spase for thumb + val desiredWidth = constraints.maxWidth + thumbSize + val desiredHeight = constraints.maxHeight + val backgroundPlaceable: Placeable = + measurables + .fastFirst { it.layoutId == TrackComponent.Background } + .measure(Constraints(desiredWidth, desiredWidth, desiredHeight, desiredHeight)) + + val iconPlaceable: Placeable? = + measurables + .fastFirstOrNull { it.layoutId == TrackComponent.Icon } + ?.measure( + Constraints( + minWidth = desiredHeight, + maxWidth = desiredHeight, + minHeight = desiredHeight, + maxHeight = desiredHeight, ) ) - } else { - indicatorPath.addRoundRect( - RoundRect( - left = -halfIconWidthPx, - top = 0f, - right = size.width * sliderState.coercedNormalizedValue + halfIconWidthPx, - bottom = size.height, - topLeftCornerRadius = trackCornerRadius, - topRightCornerRadius = indicatorCornerRadius, - bottomRightCornerRadius = indicatorCornerRadius, - bottomLeftCornerRadius = trackCornerRadius, + + val iconSize = iconPlaceable?.width ?: 0 + val labelMaxWidth = (desiredWidth - iconSize) / 2 + val labelPlaceable: Placeable? = + measurables + .fastFirstOrNull { it.layoutId == TrackComponent.Label } + ?.measure( + Constraints( + minWidth = 0, + maxWidth = labelMaxWidth, + minHeight = desiredHeight, + maxHeight = desiredHeight, ) ) + + val drawingState = + if (isRtl) { + DrawingState( + isRtl = true, + totalWidth = desiredWidth.toFloat(), + totalHeight = desiredHeight.toFloat(), + indicatorLeft = + (desiredWidth - iconSize) * (1 - sliderState.coercedNormalizedValue), + indicatorTop = 0f, + indicatorRight = desiredWidth.toFloat(), + indicatorBottom = desiredHeight.toFloat(), + iconWidth = iconSize.toFloat(), + labelWidth = labelPlaceable?.width?.toFloat() ?: 0f, + ) + } else { + DrawingState( + isRtl = false, + totalWidth = desiredWidth.toFloat(), + totalHeight = desiredHeight.toFloat(), + indicatorLeft = 0f, + indicatorTop = 0f, + indicatorRight = + iconSize + (desiredWidth - iconSize) * sliderState.coercedNormalizedValue, + indicatorBottom = desiredHeight.toFloat(), + iconWidth = iconSize.toFloat(), + labelWidth = labelPlaceable?.width?.toFloat() ?: 0f, + ) } - drawPath(path = indicatorPath, color = indicatorColor) + + onDrawingStateMeasured(drawingState) + + return layout(desiredWidth, desiredHeight) { + backgroundPlaceable.placeRelative(0, 0, TrackComponent.Background.zIndex) + + iconPlaceable?.placeRelative(0, 0, TrackComponent.Icon.zIndex) + labelPlaceable?.placeRelative(0, 0, TrackComponent.Label.zIndex) } } } +private data class DrawingState( + val isRtl: Boolean = false, + val totalWidth: Float = 0f, + val totalHeight: Float = 0f, + val indicatorLeft: Float = 0f, + val indicatorTop: Float = 0f, + val indicatorRight: Float = 0f, + val indicatorBottom: Float = 0f, + val iconWidth: Float = 0f, + val labelWidth: Float = 0f, +) + +private val DrawingState.isLabelOnTopOfIndicator: Boolean + get() = labelWidth < indicatorRight - indicatorLeft - iconWidth + /** [SliderState.value] normalized using [SliderState.valueRange]. The result belongs to [0, 1] */ private val SliderState.coercedNormalizedValue: Float get() { @@ -268,17 +418,19 @@ private val SliderState.coercedNormalizedValue: Float * @param trackColor fills the track of the slider. This is a "background" of the slider * @param indicatorColor fills the slider from the start to the value * @param iconColor is the default icon color - * @param labelColor is the default icon color + * @param labelColorOnIndicator is the label color for when it's shown on top of the indicator + * @param labelColorOnTrack is the label color for when it's shown on top of the track * @param disabledTrackColor is the [trackColor] when the PlatformSlider#enabled == false * @param disabledIndicatorColor is the [indicatorColor] when the PlatformSlider#enabled == false * @param disabledIconColor is the [iconColor] when the PlatformSlider#enabled == false - * @param disabledLabelColor is the [labelColor] when the PlatformSlider#enabled == false + * @param disabledLabelColor is the label color when the PlatformSlider#enabled == false */ data class PlatformSliderColors( val trackColor: Color, val indicatorColor: Color, val iconColor: Color, - val labelColor: Color, + val labelColorOnIndicator: Color, + val labelColorOnTrack: Color, val disabledTrackColor: Color, val disabledIndicatorColor: Color, val disabledIconColor: Color, @@ -300,10 +452,11 @@ object PlatformSliderDefaults { @Composable private fun lightThemePlatformSliderColors() = PlatformSliderColors( - trackColor = MaterialTheme.colorScheme.tertiaryContainer, - indicatorColor = LocalAndroidColorScheme.current.tertiaryFixedDim, - iconColor = MaterialTheme.colorScheme.onTertiaryContainer, - labelColor = MaterialTheme.colorScheme.onTertiaryContainer, + trackColor = LocalAndroidColorScheme.current.tertiaryFixedDim, + indicatorColor = MaterialTheme.colorScheme.tertiary, + iconColor = MaterialTheme.colorScheme.onTertiary, + labelColorOnIndicator = MaterialTheme.colorScheme.onTertiary, + labelColorOnTrack = LocalAndroidColorScheme.current.onTertiaryFixed, disabledTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest, disabledIndicatorColor = MaterialTheme.colorScheme.surfaceContainerHighest, disabledIconColor = MaterialTheme.colorScheme.outline, @@ -314,10 +467,11 @@ private fun lightThemePlatformSliderColors() = @Composable private fun darkThemePlatformSliderColors() = PlatformSliderColors( - trackColor = MaterialTheme.colorScheme.onTertiary, - indicatorColor = LocalAndroidColorScheme.current.onTertiaryFixedVariant, + trackColor = MaterialTheme.colorScheme.tertiary, + indicatorColor = MaterialTheme.colorScheme.tertiary, iconColor = MaterialTheme.colorScheme.onTertiaryContainer, - labelColor = MaterialTheme.colorScheme.onTertiaryContainer, + labelColorOnIndicator = MaterialTheme.colorScheme.onTertiary, + labelColorOnTrack = LocalAndroidColorScheme.current.onTertiaryFixed, disabledTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest, disabledIndicatorColor = MaterialTheme.colorScheme.surfaceContainerHighest, disabledIconColor = MaterialTheme.colorScheme.outline, @@ -329,3 +483,14 @@ private fun PlatformSliderColors.getTrackColor(isEnabled: Boolean): Color = private fun PlatformSliderColors.getIndicatorColor(isEnabled: Boolean): Color = if (isEnabled) indicatorColor else disabledIndicatorColor + +private fun PlatformSliderColors.getLabelColor( + isEnabled: Boolean, + isLabelOnTopOfTheIndicator: Boolean +): Color { + return if (isEnabled) { + if (isLabelOnTopOfTheIndicator) labelColorOnIndicator else labelColorOnTrack + } else { + disabledLabelColor + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index a3372e3e83da..d0c498475d0b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -24,7 +24,6 @@ import com.android.compose.animation.scene.transitions import com.android.compose.animation.scene.updateSceneTransitionLayoutState import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.communal.shared.model.CommunalScenes -import com.android.systemui.communal.ui.compose.extensions.allowGestures import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.res.R @@ -84,7 +83,7 @@ fun CommunalContainer( SceneTransitionLayout( state = sceneTransitionLayoutState, - modifier = modifier.fillMaxSize().allowGestures(allowed = touchesAllowed), + modifier = modifier.fillMaxSize(), swipeSourceDetector = FixedSizeEdgeDetector( dimensionResource(id = R.dimen.communal_gesture_initiation_width) @@ -93,9 +92,14 @@ fun CommunalContainer( scene( CommunalScenes.Blank, userActions = - mapOf( - Swipe(SwipeDirection.Left, fromSource = Edge.Right) to CommunalScenes.Communal - ) + if (touchesAllowed) { + mapOf( + Swipe(SwipeDirection.Left, fromSource = Edge.Right) to + CommunalScenes.Communal + ) + } else { + emptyMap() + } ) { // This scene shows nothing only allowing for transitions to the communal scene. Box(modifier = Modifier.fillMaxSize()) @@ -104,7 +108,13 @@ fun CommunalContainer( scene( CommunalScenes.Communal, userActions = - mapOf(Swipe(SwipeDirection.Right, fromSource = Edge.Left) to CommunalScenes.Blank), + if (touchesAllowed) { + mapOf( + Swipe(SwipeDirection.Right, fromSource = Edge.Left) to CommunalScenes.Blank + ) + } else { + emptyMap() + }, ) { CommunalScene(viewModel, dialogFactory, modifier = modifier) } 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 96520b21cc72..7acb4d5498db 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 @@ -19,61 +19,31 @@ package com.android.systemui.keyguard.ui.composable import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.android.compose.animation.scene.Edge -import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneScope -import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateSceneFloatAsState 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.qs.ui.composable.QuickSettings import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.ComposableScene import dagger.Lazy import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn /** The lock screen scene shows when the device is locked. */ @SysUISingleton class LockscreenScene @Inject constructor( - @Application private val applicationScope: CoroutineScope, viewModel: LockscreenSceneViewModel, private val lockscreenContent: Lazy<LockscreenContent>, ) : ComposableScene { override val key = Scenes.Lockscreen override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = - combine( - viewModel.upDestinationSceneKey, - viewModel.leftDestinationSceneKey, - viewModel.downFromTopEdgeDestinationSceneKey, - ) { upKey, leftKey, downFromTopEdgeKey -> - destinationScenes( - up = upKey, - left = leftKey, - downFromTopEdge = downFromTopEdgeKey, - ) - } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = - destinationScenes( - up = viewModel.upDestinationSceneKey.value, - left = viewModel.leftDestinationSceneKey.value, - downFromTopEdge = viewModel.downFromTopEdgeDestinationSceneKey.value, - ) - ) + viewModel.destinationScenes @Composable override fun SceneScope.Content( @@ -84,22 +54,6 @@ constructor( modifier = modifier, ) } - - private fun destinationScenes( - up: SceneKey?, - left: SceneKey?, - downFromTopEdge: SceneKey?, - ): Map<UserAction, UserActionResult> { - return buildMap { - up?.let { this[Swipe(SwipeDirection.Up)] = UserActionResult(up) } - left?.let { this[Swipe(SwipeDirection.Left)] = UserActionResult(left) } - downFromTopEdge?.let { - this[Swipe(fromSource = Edge.Top, direction = SwipeDirection.Down)] = - UserActionResult(downFromTopEdge) - } - this[Swipe(direction = SwipeDirection.Down)] = UserActionResult(Scenes.Shade) - } - } } @Composable diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt index 8ac84ff819eb..b1fbe35eccd8 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt @@ -17,6 +17,7 @@ package com.android.systemui.volume.panel.component.anc.ui.composable import android.content.Context +import android.view.ContextThemeWrapper import android.view.View import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.MaterialTheme @@ -73,15 +74,16 @@ constructor( AndroidView<SliceView>( modifier = Modifier.fillMaxWidth(), factory = { context: Context -> - SliceView(context).apply { - mode = SliceView.MODE_LARGE - isScrollable = false - importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO - setShowTitleItems(true) - addOnLayoutChangeListener( - OnWidthChangedLayoutListener(viewModel::changeSliceWidth) - ) - } + SliceView(ContextThemeWrapper(context, R.style.Widget_SliceView_VolumePanel)) + .apply { + mode = SliceView.MODE_LARGE + isScrollable = false + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + setShowTitleItems(true) + addOnLayoutChangeListener( + OnWidthChangedLayoutListener(viewModel::changeSliceWidth) + ) + } }, update = { sliceView: SliceView -> sliceView.slice = slice } ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt index 4d810dfce89d..81d2da0688f0 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt @@ -42,9 +42,6 @@ import androidx.compose.material3.IconButtonDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -61,12 +58,13 @@ private const val COLLAPSE_DURATION_MILLIS = 300 @Composable fun ColumnVolumeSliders( viewModels: List<SliderViewModel>, + isExpanded: Boolean, + onExpandedChanged: (Boolean) -> Unit, sliderColors: PlatformSliderColors, isExpandable: Boolean, modifier: Modifier = Modifier, ) { require(viewModels.isNotEmpty()) - var isExpanded: Boolean by remember(isExpandable) { mutableStateOf(!isExpandable) } val transition = updateTransition(isExpanded, label = "CollapsableSliders") Column(modifier = modifier) { Row( @@ -85,7 +83,7 @@ fun ColumnVolumeSliders( if (isExpandable) { ExpandButton( isExpanded = isExpanded, - onExpandedChanged = { isExpanded = it }, + onExpandedChanged = onExpandedChanged, sliderColors, Modifier, ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index 18a62dca3769..3e0aee54ea34 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -22,6 +22,7 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.size import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -30,6 +31,7 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.android.compose.PlatformSlider import com.android.compose.PlatformSliderColors import com.android.systemui.common.ui.compose.Icon @@ -54,7 +56,7 @@ fun VolumeSlider( if (isDragging) { Text(text = value.toInt().toString()) } else { - state.icon?.let { Icon(icon = it) } + state.icon?.let { Icon(modifier = Modifier.size(24.dp), icon = it) } } }, colors = sliderColors, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt index 1ca18deeaac2..fdf8ee872019 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt @@ -48,8 +48,11 @@ constructor( modifier = modifier.fillMaxWidth(), ) } else { + val isExpanded by viewModel.isExpanded.collectAsState() ColumnVolumeSliders( viewModels = sliderViewModels, + isExpanded = isExpanded, + onExpandedChanged = viewModel::onExpandedChanged, sliderColors = PlatformSliderDefaults.defaultPlatformSliderColors(), isExpandable = isPortrait, modifier = modifier.fillMaxWidth(), diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 6114499a2f5e..63ec54fbef9c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round +import com.android.compose.animation.scene.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified import com.android.compose.nestedscroll.PriorityNestedScrollConnection import kotlin.math.absoluteValue import kotlinx.coroutines.CoroutineScope @@ -353,10 +354,7 @@ private class DragControllerImpl( // If the swipe was not committed or if the swipe distance is not computed yet, don't do // anything. - if ( - swipeTransition._currentScene != toScene || - distance == SwipeTransition.DistanceUnspecified - ) { + if (swipeTransition._currentScene != toScene || distance == DistanceUnspecified) { return fromScene to 0f } @@ -418,7 +416,7 @@ private class DragControllerImpl( var targetScene: Scene var targetOffset: Float if ( - distance != SwipeTransition.DistanceUnspecified && + distance != DistanceUnspecified && shouldCommitSwipe( offset, distance, @@ -444,8 +442,8 @@ private class DragControllerImpl( if (targetScene == fromScene) { 0f } else { - check(distance != SwipeTransition.DistanceUnspecified) { - "distance is equal to ${SwipeTransition.DistanceUnspecified}" + check(distance != DistanceUnspecified) { + "distance is equal to $DistanceUnspecified" } distance } @@ -628,6 +626,12 @@ private class SwipeTransition( /** The spec to use when animating this transition to either [fromScene] or [toScene]. */ lateinit var swipeSpec: SpringSpec<Float> + override val overscrollScope: OverscrollScope = + object : OverscrollScope { + override val absoluteDistance: Float + get() = distance().absoluteValue + } + private var lastDistance = DistanceUnspecified /** Whether [TransitionState.Transition.finish] was called on this transition. */ @@ -753,10 +757,6 @@ private class SwipeTransition( /** The job in which [animatable] is animated. */ val job: Job, ) - - companion object { - const val DistanceUnspecified = 0f - } } private object DefaultSwipeDistance : UserActionDistance { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index 86124df295b4..e6f5d585e915 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -236,19 +236,28 @@ sealed interface TransitionState { interface HasOverscrollProperties { /** - * The position of the [TransitionState.Transition.toScene]. + * The position of the [Transition.toScene]. * * Used to understand the direction of the overscroll. */ val isUpOrLeft: Boolean /** - * The relative orientation between [TransitionState.Transition.fromScene] and - * [TransitionState.Transition.toScene]. + * The relative orientation between [Transition.fromScene] and [Transition.toScene]. * * Used to understand the orientation of the overscroll. */ val orientation: Orientation + + /** + * Scope which can be used in the Overscroll DSL to define a transformation based on the + * distance between [Transition.fromScene] and [Transition.toScene]. + */ + val overscrollScope: OverscrollScope + + companion object { + const val DistanceUnspecified = 0f + } } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt index 2dd41cd329a2..b46614397ff4 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt @@ -30,6 +30,7 @@ import com.android.compose.animation.scene.transformation.AnchoredTranslate import com.android.compose.animation.scene.transformation.DrawScale import com.android.compose.animation.scene.transformation.EdgeTranslate import com.android.compose.animation.scene.transformation.Fade +import com.android.compose.animation.scene.transformation.OverscrollTranslate import com.android.compose.animation.scene.transformation.PropertyTransformation import com.android.compose.animation.scene.transformation.RangedPropertyTransformation import com.android.compose.animation.scene.transformation.ScaleSize @@ -124,7 +125,7 @@ internal constructor( overscrollSpecs.fastForEach { spec -> if (spec.orientation == orientation && filter(spec)) { if (match != null) { - error("Found multiple transition specs for transition $scene") + error("Found multiple overscroll specs for overscroll $scene") } match = spec } @@ -297,6 +298,7 @@ internal class TransformationSpecImpl( ) { when (current) { is Translate, + is OverscrollTranslate, is EdgeTranslate, is AnchoredTranslate -> { throwIfNotNull(offset, element, name = "offset") diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index bc52a28279dc..2c109a337f65 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified /** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */ fun transitions(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions { @@ -88,8 +89,7 @@ interface SceneTransitionsBuilder { ): OverscrollSpec } -@TransitionDsl -interface OverscrollBuilder : PropertyTransformationBuilder { +interface BaseTransitionBuilder : PropertyTransformationBuilder { /** * The distance it takes for this transition to animate from 0% to 100% when it is driven by a * [UserAction]. @@ -120,7 +120,7 @@ interface OverscrollBuilder : PropertyTransformationBuilder { } @TransitionDsl -interface TransitionBuilder : OverscrollBuilder, PropertyTransformationBuilder { +interface TransitionBuilder : BaseTransitionBuilder { /** * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when * the transition is triggered (i.e. it is not gesture-based). @@ -176,6 +176,24 @@ interface TransitionBuilder : OverscrollBuilder, PropertyTransformationBuilder { fun reversed(builder: TransitionBuilder.() -> Unit) } +@TransitionDsl +interface OverscrollBuilder : BaseTransitionBuilder { + /** Translate the element(s) matching [matcher] by ([x], [y]) pixels. */ + fun translate( + matcher: ElementMatcher, + x: OverscrollScope.() -> Float = { 0f }, + y: OverscrollScope.() -> Float = { 0f }, + ) +} + +interface OverscrollScope { + /** + * Return the absolute distance between fromScene and toScene, if available, otherwise + * [DistanceUnspecified]. + */ + val absoluteDistance: Float +} + /** * An interface to decide where we should draw shared Elements or compose MovableElements. * diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index 65e8ea5cc341..1c9080fa085d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -31,6 +31,7 @@ import com.android.compose.animation.scene.transformation.AnchoredTranslate import com.android.compose.animation.scene.transformation.DrawScale import com.android.compose.animation.scene.transformation.EdgeTranslate import com.android.compose.animation.scene.transformation.Fade +import com.android.compose.animation.scene.transformation.OverscrollTranslate import com.android.compose.animation.scene.transformation.PropertyTransformation import com.android.compose.animation.scene.transformation.RangedPropertyTransformation import com.android.compose.animation.scene.transformation.ScaleSize @@ -114,7 +115,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { } } -internal open class OverscrollBuilderImpl : OverscrollBuilder { +internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder { val transformations = mutableListOf<Transformation>() private var range: TransformationRange? = null protected var reversed = false @@ -130,7 +131,7 @@ internal open class OverscrollBuilderImpl : OverscrollBuilder { range = null } - private fun transformation(transformation: PropertyTransformation<*>) { + protected fun transformation(transformation: PropertyTransformation<*>) { val transformation = if (range != null) { RangedPropertyTransformation(transformation, range!!) @@ -185,7 +186,7 @@ internal open class OverscrollBuilderImpl : OverscrollBuilder { } } -internal class TransitionBuilderImpl : OverscrollBuilderImpl(), TransitionBuilder { +internal class TransitionBuilderImpl : BaseTransitionBuilderImpl(), TransitionBuilder { override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow) override var swipeSpec: SpringSpec<Float>? = null override var distance: UserActionDistance? = null @@ -226,3 +227,13 @@ internal class TransitionBuilderImpl : OverscrollBuilderImpl(), TransitionBuilde fractionRange(start, end, builder) } } + +internal open class OverscrollBuilderImpl : BaseTransitionBuilderImpl(), OverscrollBuilder { + override fun translate( + matcher: ElementMatcher, + x: OverscrollScope.() -> Float, + y: OverscrollScope.() -> Float + ) { + transformation(OverscrollTranslate(matcher, x, y)) + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt index 04d5914bff69..849c9d71ec2f 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt @@ -21,11 +21,11 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementMatcher +import com.android.compose.animation.scene.OverscrollScope import com.android.compose.animation.scene.Scene import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.TransitionState -/** Translate an element by a fixed amount of density-independent pixels. */ internal class Translate( override val matcher: ElementMatcher, private val x: Dp = 0.dp, @@ -47,3 +47,28 @@ internal class Translate( } } } + +internal class OverscrollTranslate( + override val matcher: ElementMatcher, + val x: OverscrollScope.() -> Float = { 0f }, + val y: OverscrollScope.() -> Float = { 0f }, +) : PropertyTransformation<Offset> { + override fun transform( + layoutImpl: SceneTransitionLayoutImpl, + scene: Scene, + element: Element, + sceneState: Element.SceneState, + transition: TransitionState.Transition, + value: Offset, + ): Offset { + // As this object is created by OverscrollBuilderImpl and we retrieve the current + // OverscrollSpec only when the transition implements HasOverscrollProperties, we can assume + // that this method was invoked after performing this check. + val overscrollProperties = transition as TransitionState.HasOverscrollProperties + + return Offset( + x = value.x + overscrollProperties.overscrollScope.x(), + y = value.y + overscrollProperties.overscrollScope.y(), + ) + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 059a10e23207..26e01fefcb9b 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -539,24 +539,20 @@ class ElementTest { } } - @Test - fun elementTransitionDuringOverscroll() { + private fun setupOverscrollScenario( + layoutWidth: Dp, + layoutHeight: Dp, + sceneTransitions: SceneTransitionsBuilder.() -> Unit, + firstScroll: Float + ): MutableSceneTransitionLayoutStateImpl { // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is // detected as a drag event. var touchSlop = 0f - val overscrollTranslateY = 10.dp - val layoutWidth = 200.dp - val layoutHeight = 400.dp val state = MutableSceneTransitionLayoutState( initialScene = TestScenes.SceneA, - transitions = - transitions { - overscroll(TestScenes.SceneB, Orientation.Vertical) { - translate(TestElements.Foo, y = overscrollTranslateY) - } - } + transitions = transitions(sceneTransitions), ) as MutableSceneTransitionLayoutStateImpl @@ -585,9 +581,30 @@ class ElementTest { rule.onRoot().performTouchInput { val middleTop = Offset((layoutWidth / 2).toPx(), 0f) down(middleTop) - // Scroll 50% - moveBy(Offset(0f, touchSlop + layoutHeight.toPx() * 0.5f), delayMillis = 1_000) + val firstScrollHeight = layoutHeight.toPx() * firstScroll + moveBy(Offset(0f, touchSlop + firstScrollHeight), delayMillis = 1_000) } + return state + } + + @Test + fun elementTransitionDuringOverscroll() { + val layoutWidth = 200.dp + val layoutHeight = 400.dp + val overscrollTranslateY = 10.dp + + val state = + setupOverscrollScenario( + layoutWidth = layoutWidth, + layoutHeight = layoutHeight, + sceneTransitions = { + overscroll(TestScenes.SceneB, Orientation.Vertical) { + // On overscroll 100% -> Foo should translate by overscrollTranslateY + translate(TestElements.Foo, y = overscrollTranslateY) + } + }, + firstScroll = 0.5f, // Scroll 50% + ) val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true) fooElement.assertTopPositionInRootIsEqualTo(0.dp) @@ -691,4 +708,48 @@ class ElementTest { assertThat(state.currentOverscrollSpec).isNotNull() fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f) } + + @Test + fun elementTransitionWithDistanceDuringOverscroll() { + val layoutWidth = 200.dp + val layoutHeight = 400.dp + val state = + setupOverscrollScenario( + layoutWidth = layoutWidth, + layoutHeight = layoutHeight, + sceneTransitions = { + overscroll(TestScenes.SceneB, Orientation.Vertical) { + // On overscroll 100% -> Foo should translate by layoutHeight + translate(TestElements.Foo, y = { absoluteDistance }) + } + }, + firstScroll = 1f, // 100% scroll + ) + + val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true) + fooElement.assertTopPositionInRootIsEqualTo(0.dp) + + rule.onRoot().performTouchInput { + // Scroll another 50% + moveBy(Offset(0f, layoutHeight.toPx() * 0.5f), delayMillis = 1_000) + } + + val transition = state.currentTransition + assertThat(transition).isNotNull() + + // Scroll 150% (100% scroll + 50% overscroll) + assertThat(transition!!.progress).isEqualTo(1.5f) + assertThat(state.currentOverscrollSpec).isNotNull() + fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 0.5f) + + rule.onRoot().performTouchInput { + // Scroll another 100% + moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000) + } + + // Scroll 250% (100% scroll + 150% overscroll) + assertThat(transition.progress).isEqualTo(2.5f) + assertThat(state.currentOverscrollSpec).isNotNull() + fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 1.5f) + } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt index c9c3eccdedfc..825fe138c3c4 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt @@ -22,9 +22,9 @@ import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.gestures.Orientation import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.transformation.OverscrollTranslate import com.android.compose.animation.scene.transformation.Transformation import com.android.compose.animation.scene.transformation.TransformationRange -import com.android.compose.animation.scene.transformation.Translate import com.google.common.truth.Correspondence import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -228,12 +228,14 @@ class TransitionDslTest { @Test fun overscrollSpec() { val transitions = transitions { - overscroll(TestScenes.SceneA, Orientation.Vertical) { translate(TestElements.Bar) } + overscroll(TestScenes.SceneA, Orientation.Vertical) { + translate(TestElements.Bar, x = { 1f }, y = { 2f }) + } } val overscrollSpec = transitions.overscrollSpecs.single() val transformation = overscrollSpec.transformationSpec.transformations.single() - assertThat(transformation).isInstanceOf(Translate::class.java) + assertThat(transformation).isInstanceOf(OverscrollTranslate::class.java) } companion object { diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt index 153d2b8769b3..73a66c629024 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt @@ -38,6 +38,10 @@ fun transition( override val isUserInputOngoing: Boolean = isUserInputOngoing override val isUpOrLeft: Boolean = isUpOrLeft override val orientation: Orientation = orientation + override val overscrollScope: OverscrollScope = + object : OverscrollScope { + override val absoluteDistance = 0f + } override fun finish(): Job { error("finish() is not supported in test transitions") diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt index 09fdd11a99dd..bd1403a6aa26 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.testKosmos 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.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -201,6 +202,7 @@ class SimBouncerInteractorTest : SysuiTestCase() { @Test fun verifySimPin() = testScope.runTest { + val msg by collectLastValue(underTest.bouncerMessageChanged) bouncerSimRepository.setSubscriptionId(1) bouncerSimRepository.setSimPukLocked(false) whenever(telephonyManager.createForSubscriptionId(anyInt())) @@ -208,8 +210,7 @@ class SimBouncerInteractorTest : SysuiTestCase() { whenever(telephonyManager.supplyIccLockPin(anyString())) .thenReturn(PinResult(PinResult.PIN_RESULT_TYPE_SUCCESS, 1)) - val msg: String? = underTest.verifySim(listOf(0, 0, 0, 0)) - runCurrent() + verifySim(listOf(0, 0, 0, 0)) assertThat(msg).isNull() verify(keyguardUpdateMonitor).reportSimUnlocked(1) @@ -218,6 +219,7 @@ class SimBouncerInteractorTest : SysuiTestCase() { @Test fun verifySimPin_incorrect_oneRemainingAttempt() = testScope.runTest { + val msg by collectLastValue(underTest.bouncerMessageChanged) bouncerSimRepository.setSubscriptionId(1) bouncerSimRepository.setSimPukLocked(false) whenever(telephonyManager.createForSubscriptionId(anyInt())) @@ -229,9 +231,7 @@ class SimBouncerInteractorTest : SysuiTestCase() { 1, ) ) - - val msg: String? = underTest.verifySim(listOf(0, 0, 0, 0)) - runCurrent() + verifySim(listOf(0, 0, 0, 0)) assertThat(msg).isNull() val errorDialogMessage by collectLastValue(bouncerSimRepository.errorDialogMessage) @@ -245,6 +245,7 @@ class SimBouncerInteractorTest : SysuiTestCase() { @Test fun verifySimPin_incorrect_threeRemainingAttempts() = testScope.runTest { + val msg by collectLastValue(underTest.bouncerMessageChanged) bouncerSimRepository.setSubscriptionId(1) bouncerSimRepository.setSimPukLocked(false) whenever(telephonyManager.createForSubscriptionId(anyInt())) @@ -257,8 +258,7 @@ class SimBouncerInteractorTest : SysuiTestCase() { ) ) - val msg = underTest.verifySim(listOf(0, 0, 0, 0)) - runCurrent() + verifySim(listOf(0, 0, 0, 0)) assertThat(msg).isEqualTo("Enter SIM PIN. You have 3 remaining attempts.") } @@ -266,10 +266,11 @@ class SimBouncerInteractorTest : SysuiTestCase() { @Test fun verifySimPin_notCorrectLength_tooShort() = testScope.runTest { + val msg by collectLastValue(underTest.bouncerMessageChanged) bouncerSimRepository.setSubscriptionId(1) bouncerSimRepository.setSimPukLocked(false) - val msg = underTest.verifySim(listOf(0)) + verifySim(listOf(0)) assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_pin_hint)) } @@ -277,10 +278,12 @@ class SimBouncerInteractorTest : SysuiTestCase() { @Test fun verifySimPin_notCorrectLength_tooLong() = testScope.runTest { + val msg by collectLastValue(underTest.bouncerMessageChanged) + bouncerSimRepository.setSubscriptionId(1) bouncerSimRepository.setSimPukLocked(false) - val msg = underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0)) + verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0)) assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_pin_hint)) } @@ -288,6 +291,7 @@ class SimBouncerInteractorTest : SysuiTestCase() { @Test fun verifySimPuk() = testScope.runTest { + val msg by collectLastValue(underTest.bouncerMessageChanged) whenever(telephonyManager.createForSubscriptionId(anyInt())) .thenReturn(telephonyManager) whenever(telephonyManager.supplyIccLockPuk(anyString(), anyString())) @@ -295,13 +299,13 @@ class SimBouncerInteractorTest : SysuiTestCase() { bouncerSimRepository.setSubscriptionId(1) bouncerSimRepository.setSimPukLocked(true) - var msg = underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0)) + verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0)) assertThat(msg).isEqualTo(resources.getString(R.string.kg_puk_enter_pin_hint)) - msg = underTest.verifySim(listOf(0, 0, 0, 0)) + verifySim(listOf(0, 0, 0, 0)) assertThat(msg).isEqualTo(resources.getString(R.string.kg_enter_confirm_pin_hint)) - msg = underTest.verifySim(listOf(0, 0, 0, 0)) + verifySim(listOf(0, 0, 0, 0)) assertThat(msg).isNull() runCurrent() @@ -311,37 +315,49 @@ class SimBouncerInteractorTest : SysuiTestCase() { @Test fun verifySimPuk_inputTooShort() = testScope.runTest { + val msg by collectLastValue(underTest.bouncerMessageChanged) + bouncerSimRepository.setSubscriptionId(1) bouncerSimRepository.setSimPukLocked(true) - val msg = underTest.verifySim(listOf(0, 0, 0, 0)) + + verifySim(listOf(0, 0, 0, 0)) assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_puk_hint)) } @Test fun verifySimPuk_pinNotCorrectLength() = testScope.runTest { + val msg by collectLastValue(underTest.bouncerMessageChanged) bouncerSimRepository.setSubscriptionId(1) bouncerSimRepository.setSimPukLocked(true) - underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0)) + verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0)) + + verifySim(listOf(0, 0, 0)) - val msg = underTest.verifySim(listOf(0, 0, 0)) assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_pin_hint)) } @Test fun verifySimPuk_confirmedPinDoesNotMatch() = testScope.runTest { + val msg by collectLastValue(underTest.bouncerMessageChanged) + bouncerSimRepository.setSubscriptionId(1) bouncerSimRepository.setSimPukLocked(true) - underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0)) - underTest.verifySim(listOf(0, 0, 0, 0)) + verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0)) + verifySim(listOf(0, 0, 0, 0)) - val msg = underTest.verifySim(listOf(0, 0, 0, 1)) + verifySim(listOf(0, 0, 0, 1)) assertThat(msg).isEqualTo(resources.getString(R.string.kg_puk_enter_pin_hint)) } + private suspend fun TestScope.verifySim(pinDigits: List<Int>) { + runCurrent() + underTest.verifySim(pinDigits) + } + @Test fun onErrorDialogDismissed_clearsErrorDialogMessageInRepository() { bouncerSimRepository.setSimVerificationErrorMessage("abc") diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractorTest.kt index 2e9ee5ca2851..4a7757ba1820 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractorTest.kt @@ -21,6 +21,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -35,6 +36,7 @@ class DeviceEntryBiometricSettingsInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val biometricSettingsRepository = kosmos.biometricSettingsRepository private val underTest = kosmos.deviceEntryBiometricSettingsInteractor + private val testScope = kosmos.testScope @Test fun isCoex_true() = runTest { @@ -59,4 +61,25 @@ class DeviceEntryBiometricSettingsInteractorTest : SysuiTestCase() { biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) assertThat(isCoex).isFalse() } + + @Test + fun authenticationFlags_providesAuthFlagsFromRepository() = + testScope.runTest { + assertThat(underTest.authenticationFlags) + .isSameInstanceAs(biometricSettingsRepository.authenticationFlags) + } + + @Test + fun isFaceAuthEnrolledAndEnabled_providesValueFromRepository() = + testScope.runTest { + assertThat(underTest.isFaceAuthEnrolledAndEnabled) + .isSameInstanceAs(biometricSettingsRepository.isFaceAuthEnrolledAndEnabled) + } + + @Test + fun isFingerprintAuthEnrolledAndEnabled_providesValueFromRepository() = + testScope.runTest { + assertThat(underTest.isFingerprintAuthEnrolledAndEnabled) + .isSameInstanceAs(biometricSettingsRepository.isFingerprintEnrolledAndEnabled) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt index 4f44705b7e72..70ceb2a75d7c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.deviceentry.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.SceneKey +import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository @@ -27,8 +28,21 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.AdaptiveAuthRequest +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.BouncerLockedOut +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.PolicyLockdown +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.SecurityTimeout +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.TrustAgentDisabled +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.UnattendedUpdate +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.UserLockdown +import com.android.systemui.flags.fakeSystemPropertiesHelper +import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.fakeTrustRepository +import com.android.systemui.keyguard.shared.model.AuthenticationFlags import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags @@ -36,6 +50,7 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -230,8 +245,8 @@ class DeviceEntryInteractorTest : SysuiTestCase() { assertThat(canSwipeToEnter).isFalse() trustRepository.setCurrentUserTrusted(true) - runCurrent() faceAuthRepository.isAuthenticated.value = false + runCurrent() assertThat(canSwipeToEnter).isTrue() } @@ -383,6 +398,204 @@ class DeviceEntryInteractorTest : SysuiTestCase() { assertThat(isUnlocked).isTrue() } + @Test + fun deviceEntryRestrictionReason_whenFaceOrFingerprintOrTrust_alwaysNull() = + testScope.runTest { + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) + runCurrent() + + verifyRestrictionReasonsForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to null, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to null, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to null, + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to null, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to + null, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + null, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to null + ) + } + + @Test + fun deviceEntryRestrictionReason_whenFaceIsEnrolledAndEnabled_mapsToAuthFlagsState() = + testScope.runTest { + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) + kosmos.fakeSystemPropertiesHelper.set( + DeviceEntryInteractor.SYS_BOOT_REASON_PROP, + "not mainline reboot" + ) + runCurrent() + + verifyRestrictionReasonsForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to + DeviceNotUnlockedSinceReboot, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST to + AdaptiveAuthRequest, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to + BouncerLockedOut, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to + SecurityTimeout, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + UserLockdown, + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to + NonStrongBiometricsSecurityTimeout, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + UnattendedUpdate, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + PolicyLockdown, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to + null, + ) + } + + @Test + fun deviceEntryRestrictionReason_whenFingerprintIsEnrolledAndEnabled_mapsToAuthFlagsState() = + testScope.runTest { + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) + kosmos.fakeSystemPropertiesHelper.set( + DeviceEntryInteractor.SYS_BOOT_REASON_PROP, + "not mainline reboot" + ) + runCurrent() + + verifyRestrictionReasonsForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to + DeviceNotUnlockedSinceReboot, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST to + AdaptiveAuthRequest, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to + BouncerLockedOut, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to + SecurityTimeout, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + UserLockdown, + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to + NonStrongBiometricsSecurityTimeout, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + UnattendedUpdate, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + PolicyLockdown, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to + null, + ) + } + + @Test + fun deviceEntryRestrictionReason_whenTrustAgentIsEnabled_mapsToAuthFlagsState() = + testScope.runTest { + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(true) + kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false) + kosmos.fakeSystemPropertiesHelper.set( + DeviceEntryInteractor.SYS_BOOT_REASON_PROP, + "not mainline reboot" + ) + runCurrent() + + verifyRestrictionReasonsForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to + DeviceNotUnlockedSinceReboot, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST to + AdaptiveAuthRequest, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to + BouncerLockedOut, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to + SecurityTimeout, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + UserLockdown, + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to + NonStrongBiometricsSecurityTimeout, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + UnattendedUpdate, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + PolicyLockdown, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to + TrustAgentDisabled, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to + TrustAgentDisabled, + ) + } + + @Test + fun deviceEntryRestrictionReason_whenDeviceRebootedForMainlineUpdate_mapsToTheCorrectReason() = + testScope.runTest { + val deviceEntryRestrictionReason by + collectLastValue(underTest.deviceEntryRestrictionReason) + kosmos.fakeSystemPropertiesHelper.set( + DeviceEntryInteractor.SYS_BOOT_REASON_PROP, + DeviceEntryInteractor.REBOOT_MAINLINE_UPDATE + ) + kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags( + AuthenticationFlags( + userId = 1, + flag = LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT + ) + ) + runCurrent() + + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) + runCurrent() + + assertThat(deviceEntryRestrictionReason).isNull() + + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + runCurrent() + + assertThat(deviceEntryRestrictionReason) + .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate) + + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + runCurrent() + + assertThat(deviceEntryRestrictionReason) + .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate) + + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(true) + runCurrent() + + assertThat(deviceEntryRestrictionReason) + .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate) + } + + private fun TestScope.verifyRestrictionReasonsForAuthFlags( + vararg authFlagToDeviceEntryRestriction: Pair<Int, DeviceEntryRestrictionReason?> + ) { + val deviceEntryRestrictionReason by collectLastValue(underTest.deviceEntryRestrictionReason) + + authFlagToDeviceEntryRestriction.forEach { (flag, expectedReason) -> + kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags( + AuthenticationFlags(userId = 1, flag = flag) + ) + runCurrent() + + if (expectedReason == null) { + assertThat(deviceEntryRestrictionReason).isNull() + } else { + assertThat(deviceEntryRestrictionReason).isEqualTo(expectedReason) + } + } + } + private fun switchToScene(sceneKey: SceneKey) { sceneInteractor.changeScene(sceneKey, "reason") } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt new file mode 100644 index 000000000000..8f03717b42f2 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2024 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.haptics.qs + +import android.os.VibrationEffect +import android.testing.TestableLooper.RunWithLooper +import android.view.MotionEvent +import android.view.View +import androidx.test.core.view.MotionEventBuilder +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.AnimatorTestRule +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +@SmallTest +@RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) +@RunWithLooper(setAsMainLooper = true) +class QSLongPressEffectTest : SysuiTestCase() { + + @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule() + @Mock private lateinit var vibratorHelper: VibratorHelper + @Mock private lateinit var testView: View + @get:Rule val animatorTestRule = AnimatorTestRule(this) + private val kosmos = testKosmos() + + private val effectDuration = 400 + private val lowTickDuration = 12 + private val spinDuration = 133 + + private lateinit var longPressEffect: QSLongPressEffect + + @Before + fun setup() { + whenever( + vibratorHelper.getPrimitiveDurations( + VibrationEffect.Composition.PRIMITIVE_LOW_TICK, + VibrationEffect.Composition.PRIMITIVE_SPIN, + ) + ) + .thenReturn(intArrayOf(lowTickDuration, spinDuration)) + + longPressEffect = + QSLongPressEffect( + vibratorHelper, + effectDuration, + ) + } + + @Test + fun onActionDown_whileIdle_startsWait() = testWithScope { + // GIVEN an action down event occurs + val downEvent = buildMotionEvent(MotionEvent.ACTION_DOWN) + longPressEffect.onTouch(testView, downEvent) + + // THEN the effect moves to the TIMEOUT_WAIT state + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT) + } + + @Test + fun onActionCancel_whileWaiting_goesIdle() = testWhileWaiting { + // GIVEN an action cancel occurs + val cancelEvent = buildMotionEvent(MotionEvent.ACTION_CANCEL) + longPressEffect.onTouch(testView, cancelEvent) + + // THEN the effect goes back to idle and does not start + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE) + assertEffectDidNotStart() + } + + @Test + fun onActionUp_whileWaiting_performsClick() = testWhileWaiting { + // GIVEN an action is being collected + val action by collectLastValue(longPressEffect.actionType) + + // GIVEN an action up occurs + val upEvent = buildMotionEvent(MotionEvent.ACTION_UP) + longPressEffect.onTouch(testView, upEvent) + + // THEN the action to invoke is the click action and the effect does not start + assertThat(action).isEqualTo(QSLongPressEffect.ActionType.CLICK) + assertEffectDidNotStart() + } + + @Test + fun onWaitComplete_whileWaiting_beginsEffect() = testWhileWaiting { + // GIVEN the pressed timeout is complete + advanceTimeBy(QSLongPressEffect.PRESSED_TIMEOUT + 10L) + + // THEN the effect starts + assertEffectStarted() + } + + @Test + fun onActionUp_whileEffectHasBegun_reversesEffect() = testWhileRunning { + // GIVEN that the effect is at the middle of its completion (progress of 50%) + animatorTestRule.advanceTimeBy(effectDuration / 2L) + + // WHEN an action up occurs + val upEvent = buildMotionEvent(MotionEvent.ACTION_UP) + longPressEffect.onTouch(testView, upEvent) + + // THEN the effect gets reversed at 50% progress + assertEffectReverses(0.5f) + } + + @Test + fun onActionCancel_whileEffectHasBegun_reversesEffect() = testWhileRunning { + // GIVEN that the effect is at the middle of its completion (progress of 50%) + animatorTestRule.advanceTimeBy(effectDuration / 2L) + + // WHEN an action cancel occurs + val cancelEvent = buildMotionEvent(MotionEvent.ACTION_CANCEL) + longPressEffect.onTouch(testView, cancelEvent) + + // THEN the effect gets reversed at 50% progress + assertEffectReverses(0.5f) + } + + @Test + fun onAnimationComplete_effectEnds() = testWhileRunning { + // GIVEN that the animation completes + animatorTestRule.advanceTimeBy(effectDuration + 10L) + + // THEN the long-press effect completes + assertEffectCompleted() + } + + @Test + fun onActionDown_whileRunningBackwards_resets() = testWhileRunning { + // GIVEN that the effect is at the middle of its completion (progress of 50%) + animatorTestRule.advanceTimeBy(effectDuration / 2L) + + // GIVEN an action cancel occurs and the effect gets reversed + val cancelEvent = buildMotionEvent(MotionEvent.ACTION_CANCEL) + longPressEffect.onTouch(testView, cancelEvent) + + // GIVEN an action down occurs + val downEvent = buildMotionEvent(MotionEvent.ACTION_DOWN) + longPressEffect.onTouch(testView, downEvent) + + // THEN the effect resets + assertEffectResets() + } + + @Test + fun onAnimationComplete_whileRunningBackwards_goesToIdle() = testWhileRunning { + // GIVEN that the effect is at the middle of its completion (progress of 50%) + animatorTestRule.advanceTimeBy(effectDuration / 2L) + + // GIVEN an action cancel occurs and the effect gets reversed + val cancelEvent = buildMotionEvent(MotionEvent.ACTION_CANCEL) + longPressEffect.onTouch(testView, cancelEvent) + + // GIVEN that the animation completes after a sufficient amount of time + animatorTestRule.advanceTimeBy(effectDuration.toLong()) + + // THEN the state goes to [QSLongPressEffect.State.IDLE] + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE) + } + + private fun buildMotionEvent(action: Int): MotionEvent = + MotionEventBuilder.newBuilder().setAction(action).build() + + private fun testWithScope(test: suspend TestScope.() -> Unit) = + with(kosmos) { + testScope.runTest { + // GIVEN an effect with a testing scope + longPressEffect.scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)) + + // THEN run the test + test() + } + } + + private fun testWhileWaiting(test: suspend TestScope.() -> Unit) = + with(kosmos) { + testScope.runTest { + // GIVEN an effect with a testing scope + longPressEffect.scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)) + + // GIVEN the TIMEOUT_WAIT state is entered + val downEvent = + MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_DOWN).build() + longPressEffect.onTouch(testView, downEvent) + + // THEN run the test + test() + } + } + + private fun testWhileRunning(test: suspend TestScope.() -> Unit) = + with(kosmos) { + testScope.runTest { + // GIVEN an effect with a testing scope + longPressEffect.scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)) + + // GIVEN the down event that enters the TIMEOUT_WAIT state + val downEvent = + MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_DOWN).build() + longPressEffect.onTouch(testView, downEvent) + + // GIVEN that the timeout completes and the effect starts + advanceTimeBy(QSLongPressEffect.PRESSED_TIMEOUT + 10L) + + // THEN run the test + test() + } + } + + /** + * Asserts that the effect started by checking that: + * 1. The effect progress is 0f + * 2. Initial hint haptics are played + * 3. The internal state is [QSLongPressEffect.State.RUNNING_FORWARD] + */ + private fun TestScope.assertEffectStarted() { + val effectProgress by collectLastValue(longPressEffect.effectProgress) + val longPressHint = + LongPressHapticBuilder.createLongPressHint( + lowTickDuration, + spinDuration, + effectDuration, + ) + + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.RUNNING_FORWARD) + assertThat(effectProgress).isEqualTo(0f) + assertThat(longPressHint).isNotNull() + verify(vibratorHelper).vibrate(longPressHint!!) + } + + /** + * Asserts that the effect did not start by checking that: + * 1. No effect progress is emitted + * 2. No haptics are played + * 3. The internal state is not [QSLongPressEffect.State.RUNNING_BACKWARDS] or + * [QSLongPressEffect.State.RUNNING_FORWARD] + */ + private fun TestScope.assertEffectDidNotStart() { + val effectProgress by collectLastValue(longPressEffect.effectProgress) + + assertThat(longPressEffect.state).isNotEqualTo(QSLongPressEffect.State.RUNNING_FORWARD) + assertThat(longPressEffect.state).isNotEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS) + assertThat(effectProgress).isNull() + verify(vibratorHelper, never()).vibrate(any(/* type= */ VibrationEffect::class.java)) + } + + /** + * Asserts that the effect completes by checking that: + * 1. The progress is null + * 2. The final snap haptics are played + * 3. The internal state goes back to [QSLongPressEffect.State.IDLE] + * 4. The action to perform on the tile is the long-press action + */ + private fun TestScope.assertEffectCompleted() { + val action by collectLastValue(longPressEffect.actionType) + val effectProgress by collectLastValue(longPressEffect.effectProgress) + val snapEffect = LongPressHapticBuilder.createSnapEffect() + + assertThat(effectProgress).isNull() + assertThat(snapEffect).isNotNull() + verify(vibratorHelper).vibrate(snapEffect!!) + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE) + assertThat(action).isEqualTo(QSLongPressEffect.ActionType.LONG_PRESS) + } + + /** + * Assert that the effect gets reverted by checking that: + * 1. The internal state is [QSLongPressEffect.State.RUNNING_BACKWARDS] + * 2. The reverse haptics plays at the point where the animation was paused + */ + private fun assertEffectReverses(pausedProgress: Float) { + val reverseHaptics = + LongPressHapticBuilder.createReversedEffect( + pausedProgress, + lowTickDuration, + effectDuration, + ) + + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS) + assertThat(reverseHaptics).isNotNull() + verify(vibratorHelper).vibrate(reverseHaptics!!) + } + + /** + * Asserts that the effect resets by checking that: + * 1. The effect progress resets to 0 + * 2. The internal state goes back to [QSLongPressEffect.State.TIMEOUT_WAIT] + */ + private fun TestScope.assertEffectResets() { + val effectProgress by collectLastValue(longPressEffect.effectProgress) + assertThat(effectProgress).isEqualTo(0f) + + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt index 19950a5fb89d..2fd2ef1f3240 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt @@ -19,9 +19,12 @@ package com.android.systemui.keyguard.ui.viewmodel import android.platform.test.annotations.EnableFlags -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_COMMUNAL_HUB +import com.android.compose.animation.scene.Edge +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel @@ -31,86 +34,129 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.kosmos.testScope -import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.shade.domain.startable.shadeStartable +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.BeforeClass import org.junit.Test import org.junit.runner.RunWith +import platform.test.runner.parameterized.Parameter +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) +@RunWith(ParameterizedAndroidJunit4::class) class LockscreenSceneViewModelTest : SysuiTestCase() { + companion object { + @Parameters( + name = + "canSwipeToEnter={0}, downWithTwoPointers={1}, downFromEdge={2}," + + " isSingleShade={3}, isCommunalAvailable={4}" + ) + @JvmStatic + fun combinations() = buildList { + repeat(32) { combination -> + add( + arrayOf( + /* canSwipeToEnter= */ combination and 1 != 0, + /* downWithTwoPointers= */ combination and 2 != 0, + /* downFromEdge= */ combination and 4 != 0, + /* isSingleShade= */ combination and 8 != 0, + /* isCommunalAvailable= */ combination and 16 != 0, + ) + ) + } + } + + @JvmStatic + @BeforeClass + fun setUp() { + val combinationStrings = + combinations().map { array -> + check(array.size == 5) + "${array[4]},${array[3]},${array[2]},${array[1]},${array[0]}" + } + val uniqueCombinations = combinationStrings.toSet() + assertThat(combinationStrings).hasSize(uniqueCombinations.size) + } + + private fun expectedDownDestination( + downFromEdge: Boolean, + isSingleShade: Boolean, + ): SceneKey { + return if (downFromEdge && isSingleShade) Scenes.QuickSettings else Scenes.Shade + } + } + private val kosmos = testKosmos() private val testScope = kosmos.testScope private val sceneInteractor by lazy { kosmos.sceneInteractor } + @JvmField @Parameter(0) var canSwipeToEnter: Boolean = false + @JvmField @Parameter(1) var downWithTwoPointers: Boolean = false + @JvmField @Parameter(2) var downFromEdge: Boolean = false + @JvmField @Parameter(3) var isSingleShade: Boolean = true + @JvmField @Parameter(4) var isCommunalAvailable: Boolean = false + private val underTest by lazy { createLockscreenSceneViewModel() } @Test - fun upTransitionSceneKey_canSwipeToUnlock_gone() = + @EnableFlags(Flags.FLAG_COMMUNAL_HUB) + fun destinationScenes() = testScope.runTest { - val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.None - ) kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) - kosmos.fakeDeviceEntryRepository.setUnlocked(true) - sceneInteractor.changeScene(Scenes.Lockscreen, "reason") - - assertThat(upTransitionSceneKey).isEqualTo(Scenes.Gone) - } - - @Test - fun upTransitionSceneKey_cannotSwipeToUnlock_bouncer() = - testScope.runTest { - val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin + if (canSwipeToEnter) { + AuthenticationMethodModel.None + } else { + AuthenticationMethodModel.Pin + } ) - kosmos.fakeDeviceEntryRepository.setUnlocked(false) + kosmos.fakeDeviceEntryRepository.setUnlocked(canSwipeToEnter) sceneInteractor.changeScene(Scenes.Lockscreen, "reason") + kosmos.shadeRepository.setShadeMode( + if (isSingleShade) { + ShadeMode.Single + } else { + ShadeMode.Split + } + ) + kosmos.setCommunalAvailable(isCommunalAvailable) - assertThat(upTransitionSceneKey).isEqualTo(Scenes.Bouncer) - } - - @EnableFlags(FLAG_COMMUNAL_HUB) - @Test - fun leftTransitionSceneKey_communalIsAvailable_communal() = - testScope.runTest { - val leftDestinationSceneKey by collectLastValue(underTest.leftDestinationSceneKey) - assertThat(leftDestinationSceneKey).isNull() + val destinationScenes by collectLastValue(underTest.destinationScenes) - kosmos.setCommunalAvailable(true) - runCurrent() - assertThat(leftDestinationSceneKey).isEqualTo(Scenes.Communal) - } + assertThat( + destinationScenes + ?.get( + Swipe( + SwipeDirection.Down, + fromSource = Edge.Top.takeIf { downFromEdge }, + pointerCount = if (downWithTwoPointers) 2 else 1, + ) + ) + ?.toScene + ) + .isEqualTo( + expectedDownDestination( + downFromEdge = downFromEdge, + isSingleShade = isSingleShade, + ) + ) - @Test - fun downFromTopEdgeDestinationSceneKey_whenNotSplitShade_quickSettings() = - testScope.runTest { - overrideResource(R.bool.config_use_split_notification_shade, false) - kosmos.shadeStartable.start() - val sceneKey by collectLastValue(underTest.downFromTopEdgeDestinationSceneKey) - assertThat(sceneKey).isEqualTo(Scenes.QuickSettings) - } + assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene) + .isEqualTo(if (canSwipeToEnter) Scenes.Gone else Scenes.Bouncer) - @Test - fun downFromTopEdgeDestinationSceneKey_whenSplitShade_null() = - testScope.runTest { - overrideResource(R.bool.config_use_split_notification_shade, true) - kosmos.shadeStartable.start() - val sceneKey by collectLastValue(underTest.downFromTopEdgeDestinationSceneKey) - assertThat(sceneKey).isNull() + assertThat(destinationScenes?.get(Swipe(SwipeDirection.Left))?.toScene) + .isEqualTo(Scenes.Communal.takeIf { isCommunalAvailable }) } private fun createLockscreenSceneViewModel(): LockscreenSceneViewModel { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 42c33544416d..af9abcda73fe 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -26,7 +26,6 @@ import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.SwipeDirection import com.android.internal.R import com.android.internal.util.EmergencyAffordanceManager import com.android.internal.util.emergencyAffordanceManager @@ -317,8 +316,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { @Test fun swipeUpOnLockscreen_enterCorrectPin_unlocksDevice() = testScope.runTest { - val upDestinationSceneKey by - collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey) + val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes) + val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) emulateUserDrivenTransition( to = upDestinationSceneKey, @@ -337,8 +336,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { testScope.runTest { setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) - val upDestinationSceneKey by - collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey) + val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes) + val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone) emulateUserDrivenTransition( to = upDestinationSceneKey, @@ -356,7 +355,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { emulateUserDrivenTransition(to = Scenes.Shade) assertCurrentScene(Scenes.Shade) - val upDestinationSceneKey = destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene + val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Lockscreen) emulateUserDrivenTransition( to = upDestinationSceneKey, @@ -379,7 +378,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { emulateUserDrivenTransition(to = Scenes.Shade) assertCurrentScene(Scenes.Shade) - val upDestinationSceneKey = destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene + val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone) emulateUserDrivenTransition( to = upDestinationSceneKey, @@ -447,8 +446,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { fun swipeUpOnLockscreenWhileUnlocked_dismissesLockscreen() = testScope.runTest { unlockDevice() - val upDestinationSceneKey by - collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey) + val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes) + val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone) } @@ -469,8 +468,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { fun dismissingIme_whileOnPasswordBouncer_navigatesToLockscreen() = testScope.runTest { setAuthMethod(AuthenticationMethodModel.Password) - val upDestinationSceneKey by - collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey) + val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes) + val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) emulateUserDrivenTransition( to = upDestinationSceneKey, @@ -487,8 +486,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { fun bouncerActionButtonClick_opensEmergencyServicesDialer() = testScope.runTest { setAuthMethod(AuthenticationMethodModel.Password) - val upDestinationSceneKey by - collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey) + val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes) + val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) emulateUserDrivenTransition(to = upDestinationSceneKey) @@ -507,8 +506,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { testScope.runTest { setAuthMethod(AuthenticationMethodModel.Password) startPhoneCall() - val upDestinationSceneKey by - collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey) + val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes) + val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) emulateUserDrivenTransition(to = upDestinationSceneKey) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt index d3fa3603d722..cd79ed1a8965 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt @@ -54,7 +54,7 @@ class ShadeControllerSceneImplTest : SysuiTestCase() { private val kosmos = Kosmos() private val testScope = kosmos.testScope private val sceneInteractor = kosmos.sceneInteractor - private val deviceEntryInteractor = kosmos.deviceEntryInteractor + private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor } private lateinit var shadeInteractor: ShadeInteractor private lateinit var underTest: ShadeControllerSceneImpl diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt index a2f3ccb8c416..e06efe8c4021 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt @@ -113,6 +113,28 @@ class AudioVolumeInteractorTest : SysuiTestCase() { } @Test + fun zenMuted_cantChange() { + with(kosmos) { + testScope.runTest { + notificationsSoundPolicyRepository.updateNotificationPolicy() + notificationsSoundPolicyRepository.updateZenMode( + ZenMode(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS) + ) + + val canChangeVolume by + collectLastValue( + underTest.canChangeVolume(AudioStream(AudioManager.STREAM_NOTIFICATION)) + ) + + underTest.setMuted(AudioStream(AudioManager.STREAM_RING), true) + runCurrent() + + assertThat(canChangeVolume).isFalse() + } + } + } + + @Test fun streamIsMuted_getStream_volumeZero() { with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml index f51e1098f333..7341015e8690 100644 --- a/packages/SystemUI/res-keyguard/values/strings.xml +++ b/packages/SystemUI/res-keyguard/values/strings.xml @@ -304,6 +304,15 @@ <!-- An explanation text that the password needs to be entered since the user hasn't used strong authentication since quite some time. [CHAR LIMIT=80] --> <string name="kg_prompt_reason_timeout_password">For additional security, use password instead</string> + <!-- An explanation text that the pin needs to be provided to enter the device for security reasons. [CHAR LIMIT=70] --> + <string name="kg_prompt_added_security_pin">PIN required for additional security</string> + + <!-- An explanation text that the pattern needs to be provided to enter the device for security reasons. [CHAR LIMIT=70] --> + <string name="kg_prompt_added_security_pattern">Pattern required for additional security</string> + + <!-- An explanation text that the password needs to be provided to enter the device for security reasons. [CHAR LIMIT=70] --> + <string name="kg_prompt_added_security_password">Password required for additional security</string> + <!-- An explanation text that the credential needs to be entered because a device admin has locked the device. [CHAR LIMIT=80] --> <string name="kg_prompt_reason_device_admin">Device locked by admin</string> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 25596cce3b97..b8f20f6ad8a2 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1536,8 +1536,12 @@ <!-- Media device casting volume slider label [CHAR_LIMIT=20] --> <string name="media_device_cast">Cast</string> - <!-- A message shown when the notification volume changing is disabled because of the muted ring stream [CHAR_LIMIT=40]--> + <!-- A message shown when the notification volume changing is disabled because of the muted ring stream [CHAR_LIMIT=50]--> <string name="stream_notification_unavailable">Unavailable because ring is muted</string> + <!-- A message shown when the alarm volume changing is disabled because of the don't disturb mode [CHAR_LIMIT=50]--> + <string name="stream_alarm_unavailable">Unavailable because Do Not Disturb is on</string> + <!-- A message shown when the media volume changing is disabled because of the don't disturb mode [CHAR_LIMIT=50]--> + <string name="stream_media_unavailable">Unavailable because Do Not Disturb is on</string> <!-- Shown in the header of quick settings to indicate to the user that their phone ringer is on vibrate. [CHAR_LIMIT=NONE] --> <!-- Shown in the header of quick settings to indicate to the user that their phone ringer is on silent (muted). [CHAR_LIMIT=NONE] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 4e7809ade792..59516be65a5e 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -965,6 +965,10 @@ <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item> </style> + <style name="Widget.SliceView.VolumePanel"> + <item name="hideHeaderRow">true</item> + </style> + <style name="Theme.VolumePanelActivity.Popup" parent="@style/Theme.SystemUI.Dialog"> <item name="android:dialogCornerRadius">44dp</item> <item name="android:colorBackground">?androidprv:attr/materialColorSurfaceContainerHigh diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp index 6e611fe51e8d..42ba05cff906 100644 --- a/packages/SystemUI/shared/Android.bp +++ b/packages/SystemUI/shared/Android.bp @@ -35,6 +35,9 @@ java_library { srcs: [ ":statslog-SystemUI-java-gen", ], + libs: [ + "androidx.annotation_annotation", + ], } android_library { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java index 84c8ea708031..26e91b62d19a 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java @@ -122,7 +122,7 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView { case PROMPT_REASON_USER_REQUEST: return R.string.kg_prompt_after_user_lockdown_password; case PROMPT_REASON_PREPARE_FOR_UPDATE: - return R.string.kg_prompt_reason_timeout_password; + return R.string.kg_prompt_added_security_password; case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT: return R.string.kg_prompt_reason_timeout_password; case PROMPT_REASON_TRUSTAGENT_EXPIRED: diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index bf8900da887a..caa74780538e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -323,7 +323,7 @@ public class KeyguardPatternViewController resId = R.string.kg_prompt_after_user_lockdown_pattern; break; case PROMPT_REASON_PREPARE_FOR_UPDATE: - resId = R.string.kg_prompt_reason_timeout_pattern; + resId = R.string.kg_prompt_added_security_pattern; break; case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT: resId = R.string.kg_prompt_reason_timeout_pattern; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java index bcab6f054dd6..fbe9edfd6680 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java @@ -134,7 +134,7 @@ public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView case PROMPT_REASON_USER_REQUEST: return R.string.kg_prompt_after_user_lockdown_pin; case PROMPT_REASON_PREPARE_FOR_UPDATE: - return R.string.kg_prompt_reason_timeout_pin; + return R.string.kg_prompt_added_security_pin; case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT: return R.string.kg_prompt_reason_timeout_pin; case PROMPT_REASON_TRUSTAGENT_EXPIRED: diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt index c25e748f8668..7f6fc914e92b 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt @@ -23,10 +23,12 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.Flags +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.biometrics.data.repository.FacePropertyRepository import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.bouncer.data.repository.BouncerMessageRepository import com.android.systemui.bouncer.shared.model.BouncerMessageModel +import com.android.systemui.bouncer.shared.model.BouncerMessageStrings import com.android.systemui.bouncer.shared.model.Message import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -35,46 +37,6 @@ import com.android.systemui.flags.SystemPropertiesHelper import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.TrustRepository -import com.android.systemui.res.R.string.bouncer_face_not_recognized -import com.android.systemui.res.R.string.keyguard_enter_password -import com.android.systemui.res.R.string.keyguard_enter_pattern -import com.android.systemui.res.R.string.keyguard_enter_pin -import com.android.systemui.res.R.string.kg_bio_too_many_attempts_password -import com.android.systemui.res.R.string.kg_bio_too_many_attempts_pattern -import com.android.systemui.res.R.string.kg_bio_too_many_attempts_pin -import com.android.systemui.res.R.string.kg_bio_try_again_or_password -import com.android.systemui.res.R.string.kg_bio_try_again_or_pattern -import com.android.systemui.res.R.string.kg_bio_try_again_or_pin -import com.android.systemui.res.R.string.kg_face_locked_out -import com.android.systemui.res.R.string.kg_fp_not_recognized -import com.android.systemui.res.R.string.kg_primary_auth_locked_out_password -import com.android.systemui.res.R.string.kg_primary_auth_locked_out_pattern -import com.android.systemui.res.R.string.kg_primary_auth_locked_out_pin -import com.android.systemui.res.R.string.kg_prompt_after_adaptive_auth_lock -import com.android.systemui.res.R.string.kg_prompt_after_dpm_lock -import com.android.systemui.res.R.string.kg_prompt_after_update_password -import com.android.systemui.res.R.string.kg_prompt_after_update_pattern -import com.android.systemui.res.R.string.kg_prompt_after_update_pin -import com.android.systemui.res.R.string.kg_prompt_after_user_lockdown_password -import com.android.systemui.res.R.string.kg_prompt_after_user_lockdown_pattern -import com.android.systemui.res.R.string.kg_prompt_after_user_lockdown_pin -import com.android.systemui.res.R.string.kg_prompt_auth_timeout -import com.android.systemui.res.R.string.kg_prompt_password_auth_timeout -import com.android.systemui.res.R.string.kg_prompt_pattern_auth_timeout -import com.android.systemui.res.R.string.kg_prompt_pin_auth_timeout -import com.android.systemui.res.R.string.kg_prompt_reason_restart_password -import com.android.systemui.res.R.string.kg_prompt_reason_restart_pattern -import com.android.systemui.res.R.string.kg_prompt_reason_restart_pin -import com.android.systemui.res.R.string.kg_prompt_unattended_update -import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown -import com.android.systemui.res.R.string.kg_trust_agent_disabled -import com.android.systemui.res.R.string.kg_unlock_with_password_or_fp -import com.android.systemui.res.R.string.kg_unlock_with_pattern_or_fp -import com.android.systemui.res.R.string.kg_unlock_with_pin_or_fp -import com.android.systemui.res.R.string.kg_wrong_input_try_fp_suggestion -import com.android.systemui.res.R.string.kg_wrong_password_try_again -import com.android.systemui.res.R.string.kg_wrong_pattern_try_again -import com.android.systemui.res.R.string.kg_wrong_pin_try_again import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.Quint import javax.inject.Inject @@ -130,17 +92,22 @@ constructor( repository.setMessage( when (biometricSourceType) { BiometricSourceType.FINGERPRINT -> - incorrectFingerprintInput(currentSecurityMode) + BouncerMessageStrings.incorrectFingerprintInput( + currentSecurityMode.toAuthModel() + ) + .toMessage() BiometricSourceType.FACE -> - incorrectFaceInput( - currentSecurityMode, - isFingerprintAuthCurrentlyAllowed.value - ) + BouncerMessageStrings.incorrectFaceInput( + currentSecurityMode.toAuthModel(), + isFingerprintAuthCurrentlyAllowed.value + ) + .toMessage() else -> - defaultMessage( - currentSecurityMode, - isFingerprintAuthCurrentlyAllowed.value - ) + BouncerMessageStrings.defaultMessage( + currentSecurityMode.toAuthModel(), + isFingerprintAuthCurrentlyAllowed.value + ) + .toMessage() } ) } @@ -189,45 +156,79 @@ constructor( trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterReboot ) { if (wasRebootedForMainlineUpdate) { - authRequiredForMainlineUpdate(currentSecurityMode) + BouncerMessageStrings.authRequiredForMainlineUpdate( + currentSecurityMode.toAuthModel() + ) + .toMessage() } else { - authRequiredAfterReboot(currentSecurityMode) + BouncerMessageStrings.authRequiredAfterReboot( + currentSecurityMode.toAuthModel() + ) + .toMessage() } } else if (trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterTimeout) { - authRequiredAfterPrimaryAuthTimeout(currentSecurityMode) + BouncerMessageStrings.authRequiredAfterPrimaryAuthTimeout( + currentSecurityMode.toAuthModel() + ) + .toMessage() } else if (flags.isPrimaryAuthRequiredAfterDpmLockdown) { - authRequiredAfterAdminLockdown(currentSecurityMode) + BouncerMessageStrings.authRequiredAfterAdminLockdown( + currentSecurityMode.toAuthModel() + ) + .toMessage() } else if ( - trustOrBiometricsAvailable && flags.primaryAuthRequiredForUnattendedUpdate + trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredForUnattendedUpdate ) { - authRequiredForUnattendedUpdate(currentSecurityMode) + BouncerMessageStrings.authRequiredForUnattendedUpdate( + currentSecurityMode.toAuthModel() + ) + .toMessage() } else if (fpLockedOut) { - class3AuthLockedOut(currentSecurityMode) + BouncerMessageStrings.class3AuthLockedOut(currentSecurityMode.toAuthModel()) + .toMessage() } else if (faceLockedOut) { if (isFaceAuthClass3) { - class3AuthLockedOut(currentSecurityMode) + BouncerMessageStrings.class3AuthLockedOut(currentSecurityMode.toAuthModel()) + .toMessage() } else { - faceLockedOut(currentSecurityMode, isFingerprintAuthCurrentlyAllowed.value) + BouncerMessageStrings.faceLockedOut( + currentSecurityMode.toAuthModel(), + isFingerprintAuthCurrentlyAllowed.value + ) + .toMessage() } } else if (flags.isSomeAuthRequiredAfterAdaptiveAuthRequest) { - authRequiredAfterAdaptiveAuthRequest( - currentSecurityMode, - isFingerprintAuthCurrentlyAllowed.value - ) + BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest( + currentSecurityMode.toAuthModel(), + isFingerprintAuthCurrentlyAllowed.value + ) + .toMessage() } else if ( trustOrBiometricsAvailable && flags.strongerAuthRequiredAfterNonStrongBiometricsTimeout ) { - nonStrongAuthTimeout( - currentSecurityMode, - isFingerprintAuthCurrentlyAllowed.value - ) + BouncerMessageStrings.nonStrongAuthTimeout( + currentSecurityMode.toAuthModel(), + isFingerprintAuthCurrentlyAllowed.value + ) + .toMessage() } else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterUserRequest) { - trustAgentDisabled(currentSecurityMode, isFingerprintAuthCurrentlyAllowed.value) + BouncerMessageStrings.trustAgentDisabled( + currentSecurityMode.toAuthModel(), + isFingerprintAuthCurrentlyAllowed.value + ) + .toMessage() } else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterTrustAgentExpired) { - trustAgentDisabled(currentSecurityMode, isFingerprintAuthCurrentlyAllowed.value) + BouncerMessageStrings.trustAgentDisabled( + currentSecurityMode.toAuthModel(), + isFingerprintAuthCurrentlyAllowed.value + ) + .toMessage() } else if (trustOrBiometricsAvailable && flags.isInUserLockdown) { - authRequiredAfterUserLockdown(currentSecurityMode) + BouncerMessageStrings.authRequiredAfterUserLockdown( + currentSecurityMode.toAuthModel() + ) + .toMessage() } else { defaultMessage } @@ -244,7 +245,11 @@ constructor( override fun onTick(millisUntilFinished: Long) { val secondsRemaining = (millisUntilFinished / 1000.0).roundToInt() - val message = primaryAuthLockedOut(currentSecurityMode) + val message = + BouncerMessageStrings.primaryAuthLockedOut( + currentSecurityMode.toAuthModel() + ) + .toMessage() message.message?.animate = false message.message?.formatterArgs = mutableMapOf<String, Any>(Pair("count", secondsRemaining)) @@ -258,7 +263,11 @@ constructor( if (!Flags.revampedBouncerMessages()) return repository.setMessage( - incorrectSecurityInput(currentSecurityMode, isFingerprintAuthCurrentlyAllowed.value) + BouncerMessageStrings.incorrectSecurityInput( + currentSecurityMode.toAuthModel(), + isFingerprintAuthCurrentlyAllowed.value + ) + .toMessage() ) } @@ -285,7 +294,12 @@ constructor( } private val defaultMessage: BouncerMessageModel - get() = defaultMessage(currentSecurityMode, isFingerprintAuthCurrentlyAllowed.value) + get() = + BouncerMessageStrings.defaultMessage( + currentSecurityMode.toAuthModel(), + isFingerprintAuthCurrentlyAllowed.value + ) + .toMessage() fun onPrimaryBouncerUserInput() { if (!Flags.revampedBouncerMessages()) return @@ -354,283 +368,35 @@ private fun defaultMessage( return BouncerMessageModel( message = Message( - messageResId = defaultMessage(securityMode, fpAuthIsAllowed).message?.messageResId, + messageResId = + BouncerMessageStrings.defaultMessage( + securityMode.toAuthModel(), + fpAuthIsAllowed + ) + .toMessage() + .message + ?.messageResId, animate = false ), secondaryMessage = Message(message = secondaryMessage, animate = false) ) } -private fun defaultMessage( - securityMode: SecurityMode, - fpAuthIsAllowed: Boolean -): BouncerMessageModel { - return if (fpAuthIsAllowed) { - defaultMessageWithFingerprint(securityMode) - } else - when (securityMode) { - SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0) - SecurityMode.Password -> Pair(keyguard_enter_password, 0) - SecurityMode.PIN -> Pair(keyguard_enter_pin, 0) - else -> Pair(0, 0) - }.toMessage() -} - -private fun defaultMessageWithFingerprint(securityMode: SecurityMode): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, 0) - SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, 0) - SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, 0) - else -> Pair(0, 0) - }.toMessage() -} - -private fun incorrectSecurityInput( - securityMode: SecurityMode, - fpAuthIsAllowed: Boolean -): BouncerMessageModel { - return if (fpAuthIsAllowed) { - incorrectSecurityInputWithFingerprint(securityMode) - } else - when (securityMode) { - SecurityMode.Pattern -> Pair(kg_wrong_pattern_try_again, 0) - SecurityMode.Password -> Pair(kg_wrong_password_try_again, 0) - SecurityMode.PIN -> Pair(kg_wrong_pin_try_again, 0) - else -> Pair(0, 0) - }.toMessage() -} - -private fun incorrectSecurityInputWithFingerprint(securityMode: SecurityMode): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> Pair(kg_wrong_pattern_try_again, kg_wrong_input_try_fp_suggestion) - SecurityMode.Password -> Pair(kg_wrong_password_try_again, kg_wrong_input_try_fp_suggestion) - SecurityMode.PIN -> Pair(kg_wrong_pin_try_again, kg_wrong_input_try_fp_suggestion) - else -> Pair(0, 0) - }.toMessage() -} - -private fun incorrectFingerprintInput(securityMode: SecurityMode): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_pattern) - SecurityMode.Password -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_password) - SecurityMode.PIN -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_pin) - else -> Pair(0, 0) - }.toMessage() -} - -private fun incorrectFaceInput( - securityMode: SecurityMode, - fpAuthIsAllowed: Boolean -): BouncerMessageModel { - return if (fpAuthIsAllowed) incorrectFaceInputWithFingerprintAllowed(securityMode) - else - when (securityMode) { - SecurityMode.Pattern -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_pattern) - SecurityMode.Password -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_password) - SecurityMode.PIN -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_pin) - else -> Pair(0, 0) - }.toMessage() -} - -private fun incorrectFaceInputWithFingerprintAllowed( - securityMode: SecurityMode -): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, bouncer_face_not_recognized) - SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, bouncer_face_not_recognized) - SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, bouncer_face_not_recognized) - else -> Pair(0, 0) - }.toMessage() -} - -private fun biometricLockout(securityMode: SecurityMode): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_bio_too_many_attempts_pattern) - SecurityMode.Password -> Pair(keyguard_enter_password, kg_bio_too_many_attempts_password) - SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_bio_too_many_attempts_pin) - else -> Pair(0, 0) - }.toMessage() -} - -private fun authRequiredAfterReboot(securityMode: SecurityMode): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_reason_restart_pattern) - SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_reason_restart_password) - SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin) - else -> Pair(0, 0) - }.toMessage() -} - -private fun authRequiredAfterAdminLockdown(securityMode: SecurityMode): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_dpm_lock) - SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_after_dpm_lock) - SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock) - else -> Pair(0, 0) - }.toMessage() -} - -private fun authRequiredAfterAdaptiveAuthRequest( - securityMode: SecurityMode, - fpAuthIsAllowed: Boolean -): BouncerMessageModel { - return if (fpAuthIsAllowed) authRequiredAfterAdaptiveAuthRequestFingerprintAllowed(securityMode) - else - return when (securityMode) { - SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_adaptive_auth_lock) - SecurityMode.Password -> - Pair(keyguard_enter_password, kg_prompt_after_adaptive_auth_lock) - SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_after_adaptive_auth_lock) - else -> Pair(0, 0) - }.toMessage() -} - -private fun authRequiredAfterAdaptiveAuthRequestFingerprintAllowed( - securityMode: SecurityMode -): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> - Pair(kg_unlock_with_pattern_or_fp, kg_prompt_after_adaptive_auth_lock) - SecurityMode.Password -> - Pair(kg_unlock_with_password_or_fp, kg_prompt_after_adaptive_auth_lock) - SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, kg_prompt_after_adaptive_auth_lock) - else -> Pair(0, 0) - }.toMessage() -} - -private fun authRequiredAfterUserLockdown(securityMode: SecurityMode): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_user_lockdown_pattern) - SecurityMode.Password -> - Pair(keyguard_enter_password, kg_prompt_after_user_lockdown_password) - SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin) - else -> Pair(0, 0) - }.toMessage() -} - -private fun authRequiredForUnattendedUpdate(securityMode: SecurityMode): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_unattended_update) - SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_unattended_update) - SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_unattended_update) - else -> Pair(0, 0) - }.toMessage() -} - -private fun authRequiredForMainlineUpdate(securityMode: SecurityMode): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_update_pattern) - SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_after_update_password) - SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_after_update_pin) - else -> Pair(0, 0) - }.toMessage() -} - -private fun authRequiredAfterPrimaryAuthTimeout(securityMode: SecurityMode): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_pattern_auth_timeout) - SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_password_auth_timeout) - SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout) - else -> Pair(0, 0) - }.toMessage() -} - -private fun nonStrongAuthTimeout( - securityMode: SecurityMode, - fpAuthIsAllowed: Boolean -): BouncerMessageModel { - return if (fpAuthIsAllowed) { - nonStrongAuthTimeoutWithFingerprintAllowed(securityMode) - } else - when (securityMode) { - SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_auth_timeout) - SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_auth_timeout) - SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_auth_timeout) - else -> Pair(0, 0) - }.toMessage() -} - -fun nonStrongAuthTimeoutWithFingerprintAllowed(securityMode: SecurityMode): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_prompt_auth_timeout) - SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_prompt_auth_timeout) - SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, kg_prompt_auth_timeout) - else -> Pair(0, 0) - }.toMessage() -} - -private fun faceLockedOut( - securityMode: SecurityMode, - fpAuthIsAllowed: Boolean -): BouncerMessageModel { - return if (fpAuthIsAllowed) faceLockedOutButFingerprintAvailable(securityMode) - else - when (securityMode) { - SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_face_locked_out) - SecurityMode.Password -> Pair(keyguard_enter_password, kg_face_locked_out) - SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_face_locked_out) - else -> Pair(0, 0) - }.toMessage() -} - -private fun faceLockedOutButFingerprintAvailable(securityMode: SecurityMode): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_face_locked_out) - SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_face_locked_out) - SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, kg_face_locked_out) - else -> Pair(0, 0) - }.toMessage() -} - -private fun class3AuthLockedOut(securityMode: SecurityMode): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_bio_too_many_attempts_pattern) - SecurityMode.Password -> Pair(keyguard_enter_password, kg_bio_too_many_attempts_password) - SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_bio_too_many_attempts_pin) - else -> Pair(0, 0) - }.toMessage() -} - -private fun trustAgentDisabled( - securityMode: SecurityMode, - fpAuthIsAllowed: Boolean -): BouncerMessageModel { - return if (fpAuthIsAllowed) trustAgentDisabledWithFingerprintAllowed(securityMode) - else - when (securityMode) { - SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_trust_agent_disabled) - SecurityMode.Password -> Pair(keyguard_enter_password, kg_trust_agent_disabled) - SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_trust_agent_disabled) - else -> Pair(0, 0) - }.toMessage() -} - -private fun trustAgentDisabledWithFingerprintAllowed( - securityMode: SecurityMode -): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_trust_agent_disabled) - SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_trust_agent_disabled) - SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, kg_trust_agent_disabled) - else -> Pair(0, 0) - }.toMessage() -} - -private fun primaryAuthLockedOut(securityMode: SecurityMode): BouncerMessageModel { - return when (securityMode) { - SecurityMode.Pattern -> - Pair(kg_too_many_failed_attempts_countdown, kg_primary_auth_locked_out_pattern) - SecurityMode.Password -> - Pair(kg_too_many_failed_attempts_countdown, kg_primary_auth_locked_out_password) - SecurityMode.PIN -> - Pair(kg_too_many_failed_attempts_countdown, kg_primary_auth_locked_out_pin) - else -> Pair(0, 0) - }.toMessage() -} - private fun Pair<Int, Int>.toMessage(): BouncerMessageModel { return BouncerMessageModel( message = Message(messageResId = this.first, animate = false), secondaryMessage = Message(messageResId = this.second, animate = false) ) } + +private fun SecurityMode.toAuthModel(): AuthenticationMethodModel { + return when (this) { + SecurityMode.Invalid -> AuthenticationMethodModel.None + SecurityMode.None -> AuthenticationMethodModel.None + SecurityMode.Pattern -> AuthenticationMethodModel.Pattern + SecurityMode.Password -> AuthenticationMethodModel.Password + SecurityMode.PIN -> AuthenticationMethodModel.Pin + SecurityMode.SimPin -> AuthenticationMethodModel.Sim + SecurityMode.SimPuk -> AuthenticationMethodModel.Sim + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt index c3d4cb30e700..7d3075a9dd74 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt @@ -44,6 +44,8 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -70,6 +72,9 @@ constructor( val isLockedEsim: StateFlow<Boolean?> = repository.isLockedEsim val errorDialogMessage: StateFlow<String?> = repository.errorDialogMessage + private val _bouncerMessageChanged = MutableSharedFlow<String?>() + val bouncerMessageChanged: SharedFlow<String?> = _bouncerMessageChanged + /** Returns the default message for the sim pin screen. */ fun getDefaultMessage(): String { val isEsimLocked = repository.isLockedEsim.value ?: false @@ -81,7 +86,7 @@ constructor( return "" } - var count = telephonyManager.activeModemCount + val count = telephonyManager.activeModemCount val info: SubscriptionInfo? = repository.activeSubscriptionInfo.value val displayName = info?.displayName var msg: String = @@ -156,32 +161,24 @@ constructor( repository.setSimVerificationErrorMessage(null) } - /** - * Based on sim state, unlock the locked sim with the given credentials. - * - * @return Any message that should show associated with the provided input. Null means that no - * message needs to be shown. - */ - suspend fun verifySim(input: List<Any>): String? { + /** Based on sim state, unlock the locked sim with the given credentials. */ + suspend fun verifySim(input: List<Any>) { + val code = input.joinToString(separator = "") if (repository.isSimPukLocked.value) { - return verifySimPuk(input.joinToString(separator = "")) + verifySimPuk(code) + } else { + verifySimPin(code) } - - return verifySimPin(input.joinToString(separator = "")) } - /** - * Verifies the input and unlocks the locked sim with a 4-8 digit pin code. - * - * @return Any message that should show associated with the provided input. Null means that no - * message needs to be shown. - */ - private suspend fun verifySimPin(input: String): String? { + /** Verifies the input and unlocks the locked sim with a 4-8 digit pin code. */ + private suspend fun verifySimPin(input: String) { val subscriptionId = repository.subscriptionId.value // A SIM PIN is 4 to 8 decimal digits according to // GSM 02.17 version 5.0.1, Section 5.6 PIN Management if (input.length < MIN_SIM_PIN_LENGTH || input.length > MAX_SIM_PIN_LENGTH) { - return resources.getString(R.string.kg_invalid_sim_pin_hint) + _bouncerMessageChanged.emit(resources.getString(R.string.kg_invalid_sim_pin_hint)) + return } val result = withContext(backgroundDispatcher) { @@ -190,8 +187,10 @@ constructor( telephonyManager.supplyIccLockPin(input) } when (result.result) { - PinResult.PIN_RESULT_TYPE_SUCCESS -> + PinResult.PIN_RESULT_TYPE_SUCCESS -> { keyguardUpdateMonitor.reportSimUnlocked(subscriptionId) + _bouncerMessageChanged.emit(null) + } PinResult.PIN_RESULT_TYPE_INCORRECT -> { if (result.attemptsRemaining <= CRITICAL_NUM_OF_ATTEMPTS) { // Show a dialog to display the remaining number of attempts to verify the sim @@ -199,24 +198,22 @@ constructor( repository.setSimVerificationErrorMessage( getPinPasswordErrorMessage(result.attemptsRemaining) ) + _bouncerMessageChanged.emit(null) } else { - return getPinPasswordErrorMessage(result.attemptsRemaining) + _bouncerMessageChanged.emit( + getPinPasswordErrorMessage(result.attemptsRemaining) + ) } } } - - return null } /** * Verifies the input and unlocks the locked sim with a puk code instead of pin. * * This occurs after incorrectly verifying the sim pin multiple times. - * - * @return Any message that should show associated with the provided input. Null means that no - * message needs to be shown. */ - private suspend fun verifySimPuk(entry: String): String? { + private suspend fun verifySimPuk(entry: String) { val (enteredSimPuk, enteredSimPin) = repository.simPukInputModel val subscriptionId: Int = repository.subscriptionId.value @@ -224,10 +221,11 @@ constructor( if (enteredSimPuk == null) { if (entry.length >= MIN_SIM_PUK_LENGTH) { repository.setSimPukUserInput(enteredSimPuk = entry) - return resources.getString(R.string.kg_puk_enter_pin_hint) + _bouncerMessageChanged.emit(resources.getString(R.string.kg_puk_enter_pin_hint)) } else { - return resources.getString(R.string.kg_invalid_sim_puk_hint) + _bouncerMessageChanged.emit(resources.getString(R.string.kg_invalid_sim_puk_hint)) } + return } // Stage 2: Set a new sim pin to lock the sim card. @@ -237,10 +235,11 @@ constructor( enteredSimPuk = enteredSimPuk, enteredSimPin = entry, ) - return resources.getString(R.string.kg_enter_confirm_pin_hint) + _bouncerMessageChanged.emit(resources.getString(R.string.kg_enter_confirm_pin_hint)) } else { - return resources.getString(R.string.kg_invalid_sim_pin_hint) + _bouncerMessageChanged.emit(resources.getString(R.string.kg_invalid_sim_pin_hint)) } + return } // Stage 3: Confirm the newly set sim pin. @@ -250,7 +249,8 @@ constructor( resources.getString(R.string.kg_invalid_confirm_pin_hint) ) repository.setSimPukUserInput(enteredSimPuk = enteredSimPuk) - return resources.getString(R.string.kg_puk_enter_pin_hint) + _bouncerMessageChanged.emit(resources.getString(R.string.kg_puk_enter_pin_hint)) + return } val result = @@ -261,9 +261,11 @@ constructor( resetSimPukUserInput() when (result.result) { - PinResult.PIN_RESULT_TYPE_SUCCESS -> + PinResult.PIN_RESULT_TYPE_SUCCESS -> { keyguardUpdateMonitor.reportSimUnlocked(subscriptionId) - PinResult.PIN_RESULT_TYPE_INCORRECT -> + _bouncerMessageChanged.emit(null) + } + PinResult.PIN_RESULT_TYPE_INCORRECT -> { if (result.attemptsRemaining <= CRITICAL_NUM_OF_ATTEMPTS) { // Show a dialog to display the remaining number of attempts to verify the sim // puk to the user. @@ -274,17 +276,21 @@ constructor( isEsimLocked = repository.isLockedEsim.value == true ) ) + _bouncerMessageChanged.emit(null) } else { - return getPukPasswordErrorMessage( - result.attemptsRemaining, - isDefault = false, - isEsimLocked = repository.isLockedEsim.value == true + _bouncerMessageChanged.emit( + getPukPasswordErrorMessage( + result.attemptsRemaining, + isDefault = false, + isEsimLocked = repository.isLockedEsim.value == true + ) ) } - else -> return resources.getString(R.string.kg_password_puk_failed) + } + else -> { + _bouncerMessageChanged.emit(resources.getString(R.string.kg_password_puk_failed)) + } } - - return null } private fun getPinPasswordErrorMessage(attemptsRemaining: Int): String { diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerMessageStrings.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerMessageStrings.kt new file mode 100644 index 000000000000..cb12ce50dd23 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerMessageStrings.kt @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2024 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.bouncer.shared.model + +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin +import com.android.systemui.res.R + +typealias BouncerMessagePair = Pair<Int, Int> + +val BouncerMessagePair.primaryMessage: Int + get() = this.first + +val BouncerMessagePair.secondaryMessage: Int + get() = this.second + +object BouncerMessageStrings { + private val EmptyMessage = Pair(0, 0) + + fun defaultMessage( + securityMode: AuthenticationMethodModel, + fpAuthIsAllowed: Boolean + ): BouncerMessagePair { + return when (securityMode) { + Pattern -> Pair(patternDefaultMessage(fpAuthIsAllowed), 0) + Password -> Pair(passwordDefaultMessage(fpAuthIsAllowed), 0) + Pin -> Pair(pinDefaultMessage(fpAuthIsAllowed), 0) + else -> EmptyMessage + } + } + + fun incorrectSecurityInput( + securityMode: AuthenticationMethodModel, + fpAuthIsAllowed: Boolean + ): BouncerMessagePair { + val secondaryMessage = incorrectSecurityInputSecondaryMessage(fpAuthIsAllowed) + return when (securityMode) { + Pattern -> Pair(R.string.kg_wrong_pattern_try_again, secondaryMessage) + Password -> Pair(R.string.kg_wrong_password_try_again, secondaryMessage) + Pin -> Pair(R.string.kg_wrong_pin_try_again, secondaryMessage) + else -> EmptyMessage + } + } + + private fun incorrectSecurityInputSecondaryMessage(fpAuthIsAllowed: Boolean): Int { + return if (fpAuthIsAllowed) R.string.kg_wrong_input_try_fp_suggestion else 0 + } + + fun incorrectFingerprintInput(securityMode: AuthenticationMethodModel): BouncerMessagePair { + val primaryMessage = R.string.kg_fp_not_recognized + return when (securityMode) { + Pattern -> Pair(primaryMessage, R.string.kg_bio_try_again_or_pattern) + Password -> Pair(primaryMessage, R.string.kg_bio_try_again_or_password) + Pin -> Pair(primaryMessage, R.string.kg_bio_try_again_or_pin) + else -> EmptyMessage + } + } + + fun incorrectFaceInput( + securityMode: AuthenticationMethodModel, + fpAuthIsAllowed: Boolean + ): BouncerMessagePair { + return if (fpAuthIsAllowed) incorrectFaceInputWithFingerprintAllowed(securityMode) + else { + val primaryMessage = R.string.bouncer_face_not_recognized + when (securityMode) { + Pattern -> Pair(primaryMessage, R.string.kg_bio_try_again_or_pattern) + Password -> Pair(primaryMessage, R.string.kg_bio_try_again_or_password) + Pin -> Pair(primaryMessage, R.string.kg_bio_try_again_or_pin) + else -> EmptyMessage + } + } + } + + private fun incorrectFaceInputWithFingerprintAllowed( + securityMode: AuthenticationMethodModel + ): BouncerMessagePair { + val secondaryMsg = R.string.bouncer_face_not_recognized + return when (securityMode) { + Pattern -> Pair(patternDefaultMessage(true), secondaryMsg) + Password -> Pair(passwordDefaultMessage(true), secondaryMsg) + Pin -> Pair(pinDefaultMessage(true), secondaryMsg) + else -> EmptyMessage + } + } + + fun authRequiredAfterReboot(securityMode: AuthenticationMethodModel): BouncerMessagePair { + return when (securityMode) { + Pattern -> Pair(patternDefaultMessage(false), R.string.kg_prompt_reason_restart_pattern) + Password -> + Pair(passwordDefaultMessage(false), R.string.kg_prompt_reason_restart_password) + Pin -> Pair(pinDefaultMessage(false), R.string.kg_prompt_reason_restart_pin) + else -> EmptyMessage + } + } + + fun authRequiredAfterAdminLockdown( + securityMode: AuthenticationMethodModel + ): BouncerMessagePair { + val secondaryMsg = R.string.kg_prompt_after_dpm_lock + return when (securityMode) { + Pattern -> Pair(patternDefaultMessage(false), secondaryMsg) + Password -> Pair(passwordDefaultMessage(false), secondaryMsg) + Pin -> Pair(pinDefaultMessage(false), secondaryMsg) + else -> EmptyMessage + } + } + + fun authRequiredAfterAdaptiveAuthRequest( + securityMode: AuthenticationMethodModel, + fpAuthIsAllowed: Boolean + ): BouncerMessagePair { + val secondaryMsg = R.string.kg_prompt_after_adaptive_auth_lock + return when (securityMode) { + Pattern -> Pair(patternDefaultMessage(fpAuthIsAllowed), secondaryMsg) + Password -> Pair(passwordDefaultMessage(fpAuthIsAllowed), secondaryMsg) + Pin -> Pair(pinDefaultMessage(fpAuthIsAllowed), secondaryMsg) + else -> EmptyMessage + } + } + + fun authRequiredAfterUserLockdown(securityMode: AuthenticationMethodModel): BouncerMessagePair { + return when (securityMode) { + Pattern -> + Pair(patternDefaultMessage(false), R.string.kg_prompt_after_user_lockdown_pattern) + Password -> + Pair(passwordDefaultMessage(false), R.string.kg_prompt_after_user_lockdown_password) + Pin -> Pair(pinDefaultMessage(false), R.string.kg_prompt_after_user_lockdown_pin) + else -> EmptyMessage + } + } + + fun authRequiredForUnattendedUpdate( + securityMode: AuthenticationMethodModel + ): BouncerMessagePair { + return when (securityMode) { + Pattern -> Pair(patternDefaultMessage(false), R.string.kg_prompt_added_security_pattern) + Password -> + Pair(passwordDefaultMessage(false), R.string.kg_prompt_added_security_password) + Pin -> Pair(pinDefaultMessage(false), R.string.kg_prompt_added_security_pin) + else -> EmptyMessage + } + } + + fun authRequiredForMainlineUpdate(securityMode: AuthenticationMethodModel): BouncerMessagePair { + return when (securityMode) { + Pattern -> Pair(patternDefaultMessage(false), R.string.kg_prompt_after_update_pattern) + Password -> + Pair(passwordDefaultMessage(false), R.string.kg_prompt_after_update_password) + Pin -> Pair(pinDefaultMessage(false), R.string.kg_prompt_after_update_pin) + else -> EmptyMessage + } + } + + fun authRequiredAfterPrimaryAuthTimeout( + securityMode: AuthenticationMethodModel + ): BouncerMessagePair { + return when (securityMode) { + Pattern -> Pair(patternDefaultMessage(false), R.string.kg_prompt_pattern_auth_timeout) + Password -> + Pair(passwordDefaultMessage(false), R.string.kg_prompt_password_auth_timeout) + Pin -> Pair(pinDefaultMessage(false), R.string.kg_prompt_pin_auth_timeout) + else -> EmptyMessage + } + } + + fun nonStrongAuthTimeout( + securityMode: AuthenticationMethodModel, + fpAuthIsAllowed: Boolean + ): BouncerMessagePair { + val secondaryMsg = R.string.kg_prompt_auth_timeout + return when (securityMode) { + Pattern -> Pair(patternDefaultMessage(fpAuthIsAllowed), secondaryMsg) + Password -> Pair(passwordDefaultMessage(fpAuthIsAllowed), secondaryMsg) + Pin -> Pair(pinDefaultMessage(fpAuthIsAllowed), secondaryMsg) + else -> EmptyMessage + } + } + + fun faceLockedOut( + securityMode: AuthenticationMethodModel, + fpAuthIsAllowed: Boolean + ): BouncerMessagePair { + val secondaryMsg = R.string.kg_face_locked_out + return when (securityMode) { + Pattern -> Pair(patternDefaultMessage(fpAuthIsAllowed), secondaryMsg) + Password -> Pair(passwordDefaultMessage(fpAuthIsAllowed), secondaryMsg) + Pin -> Pair(pinDefaultMessage(fpAuthIsAllowed), secondaryMsg) + else -> EmptyMessage + } + } + + fun class3AuthLockedOut(securityMode: AuthenticationMethodModel): BouncerMessagePair { + return when (securityMode) { + Pattern -> Pair(patternDefaultMessage(false), R.string.kg_bio_too_many_attempts_pattern) + Password -> + Pair(passwordDefaultMessage(false), R.string.kg_bio_too_many_attempts_password) + Pin -> Pair(pinDefaultMessage(false), R.string.kg_bio_too_many_attempts_pin) + else -> EmptyMessage + } + } + + fun trustAgentDisabled( + securityMode: AuthenticationMethodModel, + fpAuthIsAllowed: Boolean + ): BouncerMessagePair { + val secondaryMsg = R.string.kg_trust_agent_disabled + return when (securityMode) { + Pattern -> Pair(patternDefaultMessage(fpAuthIsAllowed), secondaryMsg) + Password -> Pair(passwordDefaultMessage(fpAuthIsAllowed), secondaryMsg) + Pin -> Pair(pinDefaultMessage(fpAuthIsAllowed), secondaryMsg) + else -> EmptyMessage + } + } + + fun primaryAuthLockedOut(securityMode: AuthenticationMethodModel): BouncerMessagePair { + return when (securityMode) { + Pattern -> + Pair( + R.string.kg_too_many_failed_attempts_countdown, + R.string.kg_primary_auth_locked_out_pattern + ) + Password -> + Pair( + R.string.kg_too_many_failed_attempts_countdown, + R.string.kg_primary_auth_locked_out_password + ) + Pin -> + Pair( + R.string.kg_too_many_failed_attempts_countdown, + R.string.kg_primary_auth_locked_out_pin + ) + else -> EmptyMessage + } + } + + private fun patternDefaultMessage(fingerprintAllowed: Boolean): Int { + return if (fingerprintAllowed) R.string.kg_unlock_with_pattern_or_fp + else R.string.keyguard_enter_pattern + } + + private fun pinDefaultMessage(fingerprintAllowed: Boolean): Int { + return if (fingerprintAllowed) R.string.kg_unlock_with_pin_or_fp + else R.string.keyguard_enter_pin + } + + private fun passwordDefaultMessage(fingerprintAllowed: Boolean): Int { + return if (fingerprintAllowed) R.string.kg_unlock_with_password_or_fp + else R.string.keyguard_enter_password + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt index 7f4a0296ebdc..e910a9271ee2 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt @@ -157,8 +157,7 @@ class PinBouncerViewModel( if (authenticationMethod == AuthenticationMethodModel.Sim) { viewModelScope.launch { isSimUnlockingDialogVisible.value = true - val msg = simBouncerInteractor.verifySim(getInput()) - interactor.setMessage(msg) + simBouncerInteractor.verifySim(getInput()) isSimUnlockingDialogVisible.value = false clearInput() } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractor.kt index 96171aa6566e..d495facdfe9d 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractor.kt @@ -18,6 +18,7 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository +import com.android.systemui.keyguard.shared.model.AuthenticationFlags import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -31,9 +32,22 @@ class DeviceEntryBiometricSettingsInteractor constructor( repository: BiometricSettingsRepository, ) { + + /** + * Flags that control the device entry authentication behavior. + * + * This exposes why biometrics may not be currently allowed. + */ + val authenticationFlags: Flow<AuthenticationFlags> = repository.authenticationFlags + + /** Whether the current user has enrolled and enabled fingerprint auth. */ + val isFingerprintAuthEnrolledAndEnabled: Flow<Boolean> = + repository.isFingerprintEnrolledAndEnabled + val fingerprintAuthCurrentlyAllowed: Flow<Boolean> = repository.isFingerprintAuthCurrentlyAllowed - val faceAuthEnrolledAndEnabled: Flow<Boolean> = repository.isFaceAuthEnrolledAndEnabled + /** Whether the current user has enrolled and enabled face auth. */ + val isFaceAuthEnrolledAndEnabled: Flow<Boolean> = repository.isFaceAuthEnrolledAndEnabled val faceAuthCurrentlyAllowed: Flow<Boolean> = repository.isFaceAuthCurrentlyAllowed /** Whether both fingerprint and face are enrolled and enabled for device entry. */ diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt index 99bd25bf0e52..7733de49ce9c 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt @@ -32,6 +32,10 @@ interface DeviceEntryFaceAuthInteractor { /** Current detection status */ val detectionStatus: Flow<FaceDetectionStatus> + val lockedOut: Flow<Boolean> + + val authenticated: Flow<Boolean> + /** Can face auth be run right now */ fun canFaceAuthRun(): Boolean diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt index a5f6f7c77a38..805999397282 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt @@ -16,6 +16,8 @@ package com.android.systemui.deviceentry.domain.interactor +import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository +import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus @@ -23,14 +25,20 @@ import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationS import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class DeviceEntryFingerprintAuthInteractor @Inject constructor( repository: DeviceEntryFingerprintAuthRepository, + biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor, + fingerprintPropertyRepository: FingerprintPropertyRepository, ) { /** Whether fingerprint authentication is currently running or not */ val isRunning: Flow<Boolean> = repository.isRunning @@ -47,4 +55,21 @@ constructor( repository.authenticationStatus.filterIsInstance<ErrorFingerprintAuthenticationStatus>() val fingerprintHelp: Flow<HelpFingerprintAuthenticationStatus> = repository.authenticationStatus.filterIsInstance<HelpFingerprintAuthenticationStatus>() + + /** + * Whether fingerprint authentication is currently allowed for the user. This is true if the + * user has fingerprint auth enabled, enrolled, it is not disabled by any security timeouts by + * [com.android.systemui.keyguard.shared.model.AuthenticationFlags] and not locked out due to + * too many incorrect attempts. + */ + val isFingerprintAuthCurrentlyAllowed: Flow<Boolean> = + combine(isLockedOut, biometricSettingsInteractor.fingerprintAuthCurrentlyAllowed, ::Pair) + .map { (lockedOut, currentlyAllowed) -> !lockedOut && currentlyAllowed } + + /** + * Whether the fingerprint sensor is present under the display as opposed to being on the power + * button or behind/rear of the phone. + */ + val isSensorUnderDisplay = + fingerprintPropertyRepository.sensorType.map(FingerprintSensorType::isUdfps) } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt index 029a4f33cd27..fa2421a3516d 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt @@ -16,24 +16,30 @@ package com.android.systemui.deviceentry.domain.interactor +import androidx.annotation.VisibleForTesting import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository -import com.android.systemui.keyguard.data.repository.TrustRepository +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason +import com.android.systemui.flags.SystemPropertiesHelper +import com.android.systemui.keyguard.domain.interactor.TrustInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.util.kotlin.Quad import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart @@ -55,10 +61,13 @@ constructor( private val repository: DeviceEntryRepository, private val authenticationInteractor: AuthenticationInteractor, private val sceneInteractor: SceneInteractor, - deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository, - trustRepository: TrustRepository, + faceAuthInteractor: DeviceEntryFaceAuthInteractor, + private val fingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, + private val biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor, + private val trustInteractor: TrustInteractor, flags: SceneContainerFlags, deviceUnlockedInteractor: DeviceUnlockedInteractor, + private val systemPropertiesHelper: SystemPropertiesHelper, ) { /** * Whether the device is unlocked. @@ -96,8 +105,8 @@ constructor( */ private val isPassivelyAuthenticated = merge( - trustRepository.isCurrentUserTrusted, - deviceEntryFaceAuthRepository.isAuthenticated, + trustInteractor.isTrusted, + faceAuthInteractor.authenticated, ) .onStart { emit(false) } @@ -134,6 +143,67 @@ constructor( initialValue = null, ) + private val faceEnrolledAndEnabled = biometricSettingsInteractor.isFaceAuthEnrolledAndEnabled + private val fingerprintEnrolledAndEnabled = + biometricSettingsInteractor.isFingerprintAuthEnrolledAndEnabled + private val trustAgentEnabled = trustInteractor.isEnrolledAndEnabled + + private val faceOrFingerprintOrTrustEnabled: Flow<Triple<Boolean, Boolean, Boolean>> = + combine(faceEnrolledAndEnabled, fingerprintEnrolledAndEnabled, trustAgentEnabled, ::Triple) + + /** + * Reason why device entry is restricted to certain authentication methods for the current user. + * + * Emits null when there are no device entry restrictions active. + */ + val deviceEntryRestrictionReason: Flow<DeviceEntryRestrictionReason?> = + faceOrFingerprintOrTrustEnabled.flatMapLatest { + (faceEnabled, fingerprintEnabled, trustEnabled) -> + if (faceEnabled || fingerprintEnabled || trustEnabled) { + combine( + biometricSettingsInteractor.authenticationFlags, + faceAuthInteractor.lockedOut, + fingerprintAuthInteractor.isLockedOut, + trustInteractor.isTrustAgentCurrentlyAllowed, + ::Quad + ) + .map { (authFlags, isFaceLockedOut, isFingerprintLockedOut, trustManaged) -> + when { + authFlags.isPrimaryAuthRequiredAfterReboot && + wasRebootedForMainlineUpdate -> + DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate + authFlags.isPrimaryAuthRequiredAfterReboot -> + DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot + authFlags.isPrimaryAuthRequiredAfterDpmLockdown -> + DeviceEntryRestrictionReason.PolicyLockdown + authFlags.isInUserLockdown -> DeviceEntryRestrictionReason.UserLockdown + authFlags.isPrimaryAuthRequiredForUnattendedUpdate -> + DeviceEntryRestrictionReason.UnattendedUpdate + authFlags.isPrimaryAuthRequiredAfterTimeout -> + DeviceEntryRestrictionReason.SecurityTimeout + authFlags.isPrimaryAuthRequiredAfterLockout -> + DeviceEntryRestrictionReason.BouncerLockedOut + isFingerprintLockedOut -> + DeviceEntryRestrictionReason.StrongBiometricsLockedOut + isFaceLockedOut && faceAuthInteractor.isFaceAuthStrong() -> + DeviceEntryRestrictionReason.StrongBiometricsLockedOut + isFaceLockedOut -> DeviceEntryRestrictionReason.NonStrongFaceLockedOut + authFlags.isSomeAuthRequiredAfterAdaptiveAuthRequest -> + DeviceEntryRestrictionReason.AdaptiveAuthRequest + (trustEnabled && !trustManaged) && + (authFlags.someAuthRequiredAfterTrustAgentExpired || + authFlags.someAuthRequiredAfterUserRequest) -> + DeviceEntryRestrictionReason.TrustAgentDisabled + authFlags.strongerAuthRequiredAfterNonStrongBiometricsTimeout -> + DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout + else -> null + } + } + } else { + flowOf(null) + } + } + /** * Attempt to enter the device and dismiss the lockscreen. If authentication is required to * unlock the device it will transition to bouncer. @@ -187,4 +257,12 @@ constructor( } } } + + private val wasRebootedForMainlineUpdate + get() = systemPropertiesHelper.get(SYS_BOOT_REASON_PROP) == REBOOT_MAINLINE_UPDATE + + companion object { + @VisibleForTesting const val SYS_BOOT_REASON_PROP = "sys.boot.reason.last" + @VisibleForTesting const val REBOOT_MAINLINE_UPDATE = "reboot,mainline_update" + } } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/FaceHelpMessageDeferralInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/FaceHelpMessageDeferralInteractor.kt index fd6fbc9610f4..98deda09613e 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/FaceHelpMessageDeferralInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/FaceHelpMessageDeferralInteractor.kt @@ -77,7 +77,7 @@ constructor( private fun startUpdatingFaceHelpMessageDeferral() { scope.launch { - biometricSettingsInteractor.faceAuthEnrolledAndEnabled + biometricSettingsInteractor.isFaceAuthEnrolledAndEnabled .flatMapLatest { faceEnrolledAndEnabled -> if (faceEnrolledAndEnabled) { faceAcquired @@ -94,7 +94,7 @@ constructor( } scope.launch { - biometricSettingsInteractor.faceAuthEnrolledAndEnabled + biometricSettingsInteractor.isFaceAuthEnrolledAndEnabled .flatMapLatest { faceEnrolledAndEnabled -> if (faceEnrolledAndEnabled) { faceHelp diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt index 3b9416690a85..65f3eb762693 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt @@ -31,10 +31,10 @@ import kotlinx.coroutines.flow.emptyFlow */ @SysUISingleton class NoopDeviceEntryFaceAuthInteractor @Inject constructor() : DeviceEntryFaceAuthInteractor { - override val authenticationStatus: Flow<FaceAuthenticationStatus> - get() = emptyFlow() - override val detectionStatus: Flow<FaceDetectionStatus> - get() = emptyFlow() + override val authenticationStatus: Flow<FaceAuthenticationStatus> = emptyFlow() + override val detectionStatus: Flow<FaceDetectionStatus> = emptyFlow() + override val lockedOut: Flow<Boolean> = emptyFlow() + override val authenticated: Flow<Boolean> = emptyFlow() override fun canFaceAuthRun(): Boolean = false diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt index 0c9fbc27cc5d..a7266503b7a1 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt @@ -272,6 +272,8 @@ constructor( /** Provide the status of face detection */ override val detectionStatus = repository.detectionStatus + override val lockedOut: Flow<Boolean> = repository.isLockedOut + override val authenticated: Flow<Boolean> = repository.isAuthenticated private fun runFaceAuth(uiEvent: FaceAuthUiEvent, fallbackToDetect: Boolean) { if (repository.isLockedOut.value) { diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/shared/model/DeviceEntryRestrictionReason.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/shared/model/DeviceEntryRestrictionReason.kt new file mode 100644 index 000000000000..5b672ac372db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/shared/model/DeviceEntryRestrictionReason.kt @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 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.deviceentry.shared.model + +/** List of reasons why device entry can be restricted to certain authentication methods. */ +enum class DeviceEntryRestrictionReason { + /** + * Reason: Lockdown initiated by the user. + * + * Restriction: Only bouncer based device entry is allowed. + */ + UserLockdown, + + /** + * Reason: Not unlocked since reboot. + * + * Restriction: Only bouncer based device entry is allowed. + */ + DeviceNotUnlockedSinceReboot, + + /** + * Reason: Not unlocked since reboot after a mainline update. + * + * Restriction: Only bouncer based device entry is allowed. + */ + DeviceNotUnlockedSinceMainlineUpdate, + + /** + * Reason: Lockdown initiated by admin through installed device policy + * + * Restriction: Only bouncer based device entry is allowed. + */ + PolicyLockdown, + + /** + * Reason: Device entry credentials need to be used for an unattended update at a later point in + * time. + * + * Restriction: Only bouncer based device entry is allowed. + */ + UnattendedUpdate, + + /** + * Reason: Device was not unlocked using PIN/Pattern/Password for a prolonged period of time. + * + * Restriction: Only bouncer based device entry is allowed. + */ + SecurityTimeout, + + /** + * Reason: A "class 3"/strong biometrics device entry method was locked out after many incorrect + * authentication attempts. + * + * Restriction: Only bouncer based device entry is allowed. + * + * @see + * [Biometric classes](https://source.android.com/docs/security/features/biometric/measure#biometric-classes) + */ + StrongBiometricsLockedOut, + + /** + * Reason: A weak (class 2)/convenience (class 3) strength face biometrics device entry method + * was locked out after many incorrect authentication attempts. + * + * Restriction: Only stronger authentication methods (class 3 or bouncer) are allowed. + * + * @see + * [Biometric classes](https://source.android.com/docs/security/features/biometric/measure#biometric-classes) + */ + NonStrongFaceLockedOut, + + /** + * Reason: Device was last unlocked using a weak/convenience strength biometrics device entry + * method and a stronger authentication method wasn't used to unlock the device for a prolonged + * period of time. + * + * Restriction: Only stronger authentication methods (class 3 or bouncer) are allowed. + * + * @see + * [Biometric classes](https://source.android.com/docs/security/features/biometric/measure#biometric-classes) + */ + NonStrongBiometricsSecurityTimeout, + + /** + * Reason: A trust agent that was granting trust has either expired or disabled by the user by + * opening the power menu. + * + * Restriction: Only non trust agent device entry methods are allowed. + */ + TrustAgentDisabled, + + /** + * Reason: Theft protection is enabled after too many unlock attempts. + * + * Restriction: Only stronger authentication methods (class 3 or bouncer) are allowed. + */ + AdaptiveAuthRequest, + + /** + * Reason: Bouncer was locked out after too many incorrect authentication attempts. + * + * Restriction: Only bouncer based device entry is allowed. + */ + BouncerLockedOut, +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 6bb846491224..a199fea0253f 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -76,23 +76,6 @@ object Flags { val NOTIFICATION_MEMORY_LOGGING_ENABLED = releasedFlag("notification_memory_logging_enabled") - // TODO(b/260335638): Tracking Bug - @JvmField - val NOTIFICATION_INLINE_REPLY_ANIMATION = releasedFlag("notification_inline_reply_animation") - - // TODO(b/288326013): Tracking Bug - @JvmField - val NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION = - unreleasedFlag("notification_async_hybrid_view_inflation", teamfood = false) - - @JvmField - val ANIMATED_NOTIFICATION_SHADE_INSETS = - releasedFlag("animated_notification_shade_insets") - - // TODO(b/268005230): Tracking Bug - @JvmField - val SENSITIVE_REVEAL_ANIM = releasedFlag("sensitive_reveal_anim") - // TODO(b/280783617): Tracking Bug @Keep @JvmField diff --git a/packages/SystemUI/src/com/android/systemui/flags/SystemPropertiesHelper.kt b/packages/SystemUI/src/com/android/systemui/flags/SystemPropertiesHelper.kt index 6fa20de1fb7f..1e3c6049edd4 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/SystemPropertiesHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/SystemPropertiesHelper.kt @@ -20,36 +20,34 @@ import android.os.SystemProperties import javax.inject.Inject import javax.inject.Singleton -/** - * Proxy to make {@link SystemProperties} easily testable. - */ +/** Proxy to make {@link SystemProperties} easily testable. */ @Singleton open class SystemPropertiesHelper @Inject constructor() { - fun get(name: String): String { + open fun get(name: String): String { return SystemProperties.get(name) } - fun get(name: String, def: String?): String { + open fun get(name: String, def: String?): String { return SystemProperties.get(name, def) } - fun getBoolean(name: String, default: Boolean): Boolean { + open fun getBoolean(name: String, default: Boolean): Boolean { return SystemProperties.getBoolean(name, default) } - fun setBoolean(name: String, value: Boolean) { + open fun setBoolean(name: String, value: Boolean) { SystemProperties.set(name, if (value) "1" else "0") } - fun set(name: String, value: String) { + open fun set(name: String, value: String) { SystemProperties.set(name, value) } - fun set(name: String, value: Int) { + open fun set(name: String, value: Int) { set(name, value.toString()) } - fun erase(name: String) { + open fun erase(name: String) { set(name, "") } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/LongPressHapticBuilder.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/LongPressHapticBuilder.kt new file mode 100644 index 000000000000..0143b85a4fbf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/LongPressHapticBuilder.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2024 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.haptics.qs + +import android.os.VibrationEffect +import android.util.Log +import kotlin.math.max + +object LongPressHapticBuilder { + + const val INVALID_DURATION = 0 /* in ms */ + + private const val TAG = "LongPressHapticBuilder" + private const val SPIN_SCALE = 0.2f + private const val CLICK_SCALE = 0.5f + private const val LOW_TICK_SCALE = 0.08f + private const val WARMUP_TIME = 75 /* in ms */ + private const val DAMPING_TIME = 24 /* in ms */ + + /** Create the signal that indicates that a long-press action is available. */ + fun createLongPressHint( + lowTickDuration: Int, + spinDuration: Int, + effectDuration: Int + ): VibrationEffect? { + if (lowTickDuration == 0 || spinDuration == 0) { + Log.d( + TAG, + "The LOW_TICK and/or SPIN primitives are not supported. No signal created.", + ) + return null + } + if (effectDuration < WARMUP_TIME + spinDuration + DAMPING_TIME) { + Log.d( + TAG, + "Cannot fit long-press hint signal in the effect duration. No signal created", + ) + return null + } + + val nLowTicks = WARMUP_TIME / lowTickDuration + val rampDownLowTicks = DAMPING_TIME / lowTickDuration + val composition = VibrationEffect.startComposition() + + // Warmup low ticks + repeat(nLowTicks) { + composition.addPrimitive( + VibrationEffect.Composition.PRIMITIVE_LOW_TICK, + LOW_TICK_SCALE, + 0, + ) + } + + // Spin effect + composition.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, SPIN_SCALE, 0) + + // Damping low ticks + repeat(rampDownLowTicks) { i -> + composition.addPrimitive( + VibrationEffect.Composition.PRIMITIVE_LOW_TICK, + LOW_TICK_SCALE / (i + 1), + 0, + ) + } + + return composition.compose() + } + + /** Create a "snapping" effect that triggers at the end of a long-press gesture */ + fun createSnapEffect(): VibrationEffect? = + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, CLICK_SCALE, 0) + .compose() + + /** Creates a signal that indicates the reversal of the long-press animation. */ + fun createReversedEffect( + pausedProgress: Float, + lowTickDuration: Int, + effectDuration: Int, + ): VibrationEffect? { + val duration = pausedProgress * effectDuration + if (duration == 0f) return null + + if (lowTickDuration == 0) { + Log.d(TAG, "Cannot play reverse haptics because LOW_TICK is not supported") + return null + } + + val nLowTicks = (duration / lowTickDuration).toInt() + if (nLowTicks == 0) return null + + val composition = VibrationEffect.startComposition() + var scale: Float + val step = LOW_TICK_SCALE / nLowTicks + repeat(nLowTicks) { i -> + scale = max(LOW_TICK_SCALE - step * i, 0f) + composition.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, scale, 0) + } + return composition.compose() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt new file mode 100644 index 000000000000..ec72a1422973 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2024 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.haptics.qs + +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.os.VibrationEffect +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import android.view.animation.AccelerateDecelerateInterpolator +import androidx.annotation.VisibleForTesting +import androidx.core.animation.doOnCancel +import androidx.core.animation.doOnEnd +import androidx.core.animation.doOnStart +import com.android.systemui.statusbar.VibratorHelper +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +/** + * A class that handles the long press visuo-haptic effect for a QS tile. + * + * The class is also a [View.OnTouchListener] to handle the touch events, clicks and long-press + * gestures of the tile. The class also provides a [State] that can be used to determine the current + * state of the long press effect. + * + * @property[vibratorHelper] The [VibratorHelper] to deliver haptic effects. + * @property[effectDuration] The duration of the effect in ms. + */ +class QSLongPressEffect( + private val vibratorHelper: VibratorHelper?, + private val effectDuration: Int, +) : View.OnTouchListener { + + /** Current state */ + var state = State.IDLE + @VisibleForTesting set + + /** Flows for view control and action */ + private val _effectProgress = MutableStateFlow<Float?>(null) + val effectProgress = _effectProgress.asStateFlow() + + private val _actionType = MutableStateFlow<ActionType?>(null) + val actionType = _actionType.asStateFlow() + + /** Haptic effects */ + private val durations = + vibratorHelper?.getPrimitiveDurations( + VibrationEffect.Composition.PRIMITIVE_LOW_TICK, + VibrationEffect.Composition.PRIMITIVE_SPIN + ) + + private val longPressHint = + LongPressHapticBuilder.createLongPressHint( + durations?.get(0) ?: LongPressHapticBuilder.INVALID_DURATION, + durations?.get(1) ?: LongPressHapticBuilder.INVALID_DURATION, + effectDuration + ) + + private val snapEffect = LongPressHapticBuilder.createSnapEffect() + + /* A coroutine scope and a timer job that waits for the pressedTimeout */ + var scope: CoroutineScope? = null + private var waitJob: Job? = null + + private val effectAnimator = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = effectDuration.toLong() + interpolator = AccelerateDecelerateInterpolator() + + doOnStart { handleAnimationStart() } + addUpdateListener { _effectProgress.value = animatedValue as Float } + doOnEnd { handleAnimationComplete() } + doOnCancel { handleAnimationCancel() } + } + + private fun reverse() { + val pausedProgress = effectAnimator.animatedFraction + val effect = + LongPressHapticBuilder.createReversedEffect( + pausedProgress, + durations?.get(0) ?: 0, + effectDuration, + ) + vibratorHelper?.cancel() + vibrate(effect) + effectAnimator.reverse() + } + + private fun vibrate(effect: VibrationEffect?) { + if (vibratorHelper != null && effect != null) { + vibratorHelper.vibrate(effect) + } + } + + /** + * Handle relevant touch events for the operation of a Tile. + * + * A click action is performed following the relevant logic that originates from the + * [MotionEvent.ACTION_UP] event depending on the current state. + */ + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(view: View?, event: MotionEvent?): Boolean { + when (event?.actionMasked) { + MotionEvent.ACTION_DOWN -> handleActionDown() + MotionEvent.ACTION_UP -> handleActionUp() + MotionEvent.ACTION_CANCEL -> handleActionCancel() + } + return true + } + + private fun handleActionDown() { + when (state) { + State.IDLE -> { + startPressedTimeoutWait() + state = State.TIMEOUT_WAIT + } + State.RUNNING_BACKWARDS -> effectAnimator.cancel() + else -> {} + } + } + + private fun startPressedTimeoutWait() { + waitJob = + scope?.launch { + try { + delay(PRESSED_TIMEOUT) + handleTimeoutComplete() + } catch (_: CancellationException) { + state = State.IDLE + } + } + } + + private fun handleActionUp() { + when (state) { + State.TIMEOUT_WAIT -> { + waitJob?.cancel() + _actionType.value = ActionType.CLICK + state = State.IDLE + } + State.RUNNING_FORWARD -> { + reverse() + state = State.RUNNING_BACKWARDS + } + else -> {} + } + } + + private fun handleActionCancel() { + when (state) { + State.TIMEOUT_WAIT -> { + waitJob?.cancel() + state = State.IDLE + } + State.RUNNING_FORWARD -> { + reverse() + state = State.RUNNING_BACKWARDS + } + else -> {} + } + } + + private fun handleAnimationStart() { + vibrate(longPressHint) + state = State.RUNNING_FORWARD + } + + /** This function is called both when an animator completes or gets cancelled */ + private fun handleAnimationComplete() { + if (state == State.RUNNING_FORWARD) { + vibrate(snapEffect) + _actionType.value = ActionType.LONG_PRESS + _effectProgress.value = null + } + if (state != State.TIMEOUT_WAIT) { + // This will happen if the animator did not finish by being cancelled + state = State.IDLE + } + } + + private fun handleAnimationCancel() { + _effectProgress.value = 0f + startPressedTimeoutWait() + state = State.TIMEOUT_WAIT + } + + private fun handleTimeoutComplete() { + if (state == State.TIMEOUT_WAIT && !effectAnimator.isRunning) { + effectAnimator.start() + } + } + + fun clearActionType() { + _actionType.value = null + } + + enum class State { + IDLE, /* The effect is idle waiting for touch input */ + TIMEOUT_WAIT, /* The effect is waiting for a [PRESSED_TIMEOUT] period */ + RUNNING_FORWARD, /* The effect is running normally */ + RUNNING_BACKWARDS, /* The effect was interrupted and is now running backwards */ + } + + /* A type of action to perform on the view depending on the effect's state and logic */ + enum class ActionType { + CLICK, + LONG_PRESS, + } + + companion object { + /** + * A timeout to let the tile resolve if it is being swiped/scrolled. Since QS tiles are + * inside a scrollable container, they will be considered pressed only after a tap timeout. + */ + val PRESSED_TIMEOUT = ViewConfiguration.getTapTimeout().toLong() + 20L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt new file mode 100644 index 000000000000..e298154159b2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 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.haptics.qs + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.qs.tileimpl.QSTileViewImpl +import kotlinx.coroutines.launch + +object QSLongPressEffectViewBinder { + + fun bind( + tile: QSTileViewImpl, + effect: QSLongPressEffect?, + ) { + if (effect == null) return + + tile.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + effect.scope = this + + launch { + effect.effectProgress.collect { progress -> + progress?.let { + if (it == 0f) { + tile.bringToFront() + } + tile.updateLongPressEffectProperties(it) + } + } + } + + launch { + effect.actionType.collect { action -> + action?.let { + when (it) { + QSLongPressEffect.ActionType.CLICK -> tile.performClick() + QSLongPressEffect.ActionType.LONG_PRESS -> tile.performLongClick() + } + effect.clearActionType() + } + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TrustInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TrustInteractor.kt new file mode 100644 index 000000000000..2ff6e165293b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TrustInteractor.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 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.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.TrustRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +/** Encapsulates any state relevant to trust agents and trust grants. */ +@SysUISingleton +class TrustInteractor @Inject constructor(repository: TrustRepository) { + /** + * Whether the current user has a trust agent enabled. This is true if the user has at least one + * trust agent enabled in settings. + */ + val isEnrolledAndEnabled: StateFlow<Boolean> = repository.isCurrentUserTrustUsuallyManaged + + /** + * Whether the current user's trust agent is currently allowed, this will be false if trust + * agent is disabled for any reason (security timeout, disabled on lock screen by opening the + * power menu, etc), it does not include temporary biometric lockouts. + */ + val isTrustAgentCurrentlyAllowed: StateFlow<Boolean> = repository.isCurrentUserTrustManaged + + /** Whether the current user is trusted by any of the enabled trust agents. */ + val isTrusted: Flow<Boolean> = repository.isCurrentUserTrusted +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AuthenticationFlags.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AuthenticationFlags.kt index 08904b6ffa86..d6f3634fdcd5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AuthenticationFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AuthenticationFlags.kt @@ -32,6 +32,9 @@ data class AuthenticationFlags(val userId: Int, val flag: Int) { val isPrimaryAuthRequiredAfterTimeout = containsFlag(flag, LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT) + val isPrimaryAuthRequiredAfterLockout = + containsFlag(flag, LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT) + val isPrimaryAuthRequiredAfterDpmLockdown = containsFlag( flag, @@ -47,7 +50,7 @@ data class AuthenticationFlags(val userId: Int, val flag: Int) { LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED ) - val primaryAuthRequiredForUnattendedUpdate = + val isPrimaryAuthRequiredForUnattendedUpdate = containsFlag( flag, LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt index e5b596419efe..abf2372639fa 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt @@ -79,6 +79,8 @@ constructor( val notificationAlpha: Flow<Float> = keyguardAlpha + val shortcutsAlpha: Flow<Float> = keyguardAlpha + val notificationTranslationX: Flow<Float> = keyguardTranslationX.map { it.value }.filterNotNull() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt index 4db942cc460c..c4383fc0857d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt @@ -52,6 +52,7 @@ constructor( occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel, primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel, + glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel, lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel, lockscreenToDreamingHostedTransitionViewModel: LockscreenToDreamingHostedTransitionViewModel, @@ -59,6 +60,7 @@ constructor( lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel, lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel, lockscreenToPrimaryBouncerTransitionViewModel: LockscreenToPrimaryBouncerTransitionViewModel, + lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel, transitionInteractor: KeyguardTransitionInteractor, ) { @@ -110,6 +112,7 @@ constructor( occludedToLockscreenTransitionViewModel.shortcutsAlpha, offToLockscreenTransitionViewModel.shortcutsAlpha, primaryBouncerToLockscreenTransitionViewModel.shortcutsAlpha, + glanceableHubToLockscreenTransitionViewModel.shortcutsAlpha, ) /** alpha while fading the quick affordances in */ @@ -122,6 +125,7 @@ constructor( lockscreenToGoneTransitionViewModel.shortcutsAlpha, lockscreenToOccludedTransitionViewModel.shortcutsAlpha, lockscreenToPrimaryBouncerTransitionViewModel.shortcutsAlpha, + lockscreenToGlanceableHubTransitionViewModel.shortcutsAlpha, shadeExpansionAlpha, ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt index 288ef3c52e21..993e81bfbf69 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt @@ -14,9 +14,15 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.keyguard.ui.viewmodel -import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.Edge +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -27,9 +33,10 @@ import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn /** Models UI state and handles user input for the lockscreen scene. */ @@ -44,37 +51,69 @@ constructor( val longPress: KeyguardLongPressViewModel, val notifications: NotificationsPlaceholderViewModel, ) { - /** The key of the scene we should switch to when swiping up. */ - val upDestinationSceneKey: StateFlow<SceneKey> = - deviceEntryInteractor.isUnlocked - .map { isUnlocked -> upDestinationSceneKey(isUnlocked) } + val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = + combine( + deviceEntryInteractor.isUnlocked, + communalInteractor.isCommunalAvailable, + shadeInteractor.shadeMode, + ) { isDeviceUnlocked, isCommunalAvailable, shadeMode -> + destinationScenes( + isDeviceUnlocked = isDeviceUnlocked, + isCommunalAvailable = isCommunalAvailable, + shadeMode = shadeMode, + ) + } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), - initialValue = upDestinationSceneKey(deviceEntryInteractor.isUnlocked.value), + initialValue = + destinationScenes( + isDeviceUnlocked = deviceEntryInteractor.isUnlocked.value, + isCommunalAvailable = false, + shadeMode = shadeInteractor.shadeMode.value, + ), ) - private fun upDestinationSceneKey(isUnlocked: Boolean): SceneKey { - return if (isUnlocked) Scenes.Gone else Scenes.Bouncer - } + private fun destinationScenes( + isDeviceUnlocked: Boolean, + isCommunalAvailable: Boolean, + shadeMode: ShadeMode, + ): Map<UserAction, UserActionResult> { + val quickSettingsIfSingleShade = + if (shadeMode is ShadeMode.Single) { + Scenes.QuickSettings + } else { + Scenes.Shade + } - /** The key of the scene we should switch to when swiping left. */ - val leftDestinationSceneKey: StateFlow<SceneKey?> = - communalInteractor.isCommunalAvailable - .map { available -> if (available) Scenes.Communal else null } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = null, - ) + return mapOf( + Swipe.Left to UserActionResult(Scenes.Communal).takeIf { isCommunalAvailable }, + Swipe.Up to if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer, - /** The key of the scene we should switch to when swiping down from the top edge. */ - val downFromTopEdgeDestinationSceneKey: StateFlow<SceneKey?> = - shadeInteractor.shadeMode - .map { shadeMode -> Scenes.QuickSettings.takeIf { shadeMode is ShadeMode.Single } } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = null, + // Swiping down from the top edge goes to QS (or shade if in split shade mode). + swipeDownFromTop(pointerCount = 1) to quickSettingsIfSingleShade, + swipeDownFromTop(pointerCount = 2) to quickSettingsIfSingleShade, + + // Swiping down, not from the edge, always navigates to the shade scene. + swipeDown(pointerCount = 1) to Scenes.Shade, + swipeDown(pointerCount = 2) to Scenes.Shade, ) + .filterValues { it != null } + .mapValues { checkNotNull(it.value) } + } + + private fun swipeDownFromTop(pointerCount: Int): Swipe { + return Swipe( + SwipeDirection.Down, + fromSource = Edge.Top, + pointerCount = pointerCount, + ) + } + + private fun swipeDown(pointerCount: Int): Swipe { + return Swipe( + SwipeDirection.Down, + pointerCount = pointerCount, + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt index 978e71e2a825..b7f7b06f6644 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt @@ -81,6 +81,8 @@ constructor( val notificationAlpha: Flow<Float> = keyguardAlpha + val shortcutsAlpha: Flow<Float> = keyguardAlpha + val notificationTranslationX: Flow<Float> = keyguardTranslationX.map { it.value }.filterNotNull() } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 91c86dff34ea..9d0ea5ebd925 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -19,6 +19,7 @@ import static android.view.InputDevice.SOURCE_MOUSE; import static android.view.InputDevice.SOURCE_TOUCHPAD; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; +import static com.android.systemui.Flags.edgebackGestureHandlerGetRunningTasksBackground; import static com.android.systemui.classifier.Classifier.BACK_GESTURE; import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll; import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe; @@ -54,7 +55,6 @@ import android.view.ISystemGestureExclusionListener; import android.view.IWindowManager; import android.view.InputDevice; import android.view.InputEvent; -import android.view.InputMonitor; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; @@ -104,6 +104,7 @@ import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import javax.inject.Inject; @@ -151,7 +152,12 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { @Override public void onTaskStackChanged() { - mGestureBlockingActivityRunning = isGestureBlockingActivityRunning(); + if (edgebackGestureHandlerGetRunningTasksBackground()) { + mBackgroundExecutor.execute(() -> mGestureBlockingActivityRunning.set( + isGestureBlockingActivityRunning())); + } else { + mGestureBlockingActivityRunning.set(isGestureBlockingActivityRunning()); + } } @Override public void onTaskCreated(int taskId, ComponentName componentName) { @@ -241,6 +247,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final PointF mDownPoint = new PointF(); private final PointF mEndPoint = new PointF(); + private AtomicBoolean mGestureBlockingActivityRunning = new AtomicBoolean(); + private boolean mThresholdCrossed = false; private boolean mAllowGesture = false; private boolean mLogGesture = false; @@ -256,7 +264,6 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private boolean mIsEnabled; private boolean mIsNavBarShownTransiently; private boolean mIsBackGestureAllowed; - private boolean mGestureBlockingActivityRunning; private boolean mIsNewBackAffordanceEnabled; private boolean mIsTrackpadGestureFeaturesEnabled; private boolean mIsTrackpadThreeFingerSwipe; @@ -1017,7 +1024,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mInRejectedExclusion = false; boolean isWithinInsets = isWithinInsets((int) ev.getX(), (int) ev.getY()); boolean isBackAllowedCommon = !mDisabledForQuickstep && mIsBackGestureAllowed - && !mGestureBlockingActivityRunning + && !mGestureBlockingActivityRunning.get() && !QuickStepContract.isBackGestureDisabled(mSysUiFlags, mIsTrackpadThreeFingerSwipe) && !isTrackpadScroll(mIsTrackpadGestureFeaturesEnabled, ev); @@ -1053,8 +1060,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack curTime, curTimeStr, mAllowGesture, mIsTrackpadThreeFingerSwipe, mIsOnLeftEdge, mDeferSetIsOnLeftEdge, mIsBackGestureAllowed, QuickStepContract.isBackGestureDisabled(mSysUiFlags, - mIsTrackpadThreeFingerSwipe), - mDisabledForQuickstep, mGestureBlockingActivityRunning, mIsInPip, mDisplaySize, + mIsTrackpadThreeFingerSwipe), mDisabledForQuickstep, + mGestureBlockingActivityRunning.get(), mIsInPip, mDisplaySize, mEdgeWidthLeft, mLeftInset, mEdgeWidthRight, mRightInset, mExcludeRegion)); } else if (mAllowGesture || mLogGesture) { if (!mThresholdCrossed) { @@ -1236,7 +1243,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack pw.println(" mIsBackGestureAllowed=" + mIsBackGestureAllowed); pw.println(" mIsGestureHandlingEnabled=" + mIsGestureHandlingEnabled); pw.println(" mIsNavBarShownTransiently=" + mIsNavBarShownTransiently); - pw.println(" mGestureBlockingActivityRunning=" + mGestureBlockingActivityRunning); + pw.println(" mGestureBlockingActivityRunning=" + mGestureBlockingActivityRunning.get()); pw.println(" mAllowGesture=" + mAllowGesture); pw.println(" mUseMLModel=" + mUseMLModel); pw.println(" mDisabledForQuickstep=" + mDisabledForQuickstep); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java index 2440651555d7..cd6511979375 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java @@ -38,6 +38,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlags; import com.android.systemui.settings.brightness.BrightnessController; import com.android.systemui.settings.brightness.BrightnessMirrorHandler; import com.android.systemui.settings.brightness.BrightnessSliderController; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.SplitShadeStateController; @@ -90,9 +91,11 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { FalsingManager falsingManager, StatusBarKeyguardViewManager statusBarKeyguardViewManager, SplitShadeStateController splitShadeStateController, - SceneContainerFlags sceneContainerFlags) { + SceneContainerFlags sceneContainerFlags, + VibratorHelper vibratorHelper) { super(view, qsHost, qsCustomizerController, usingMediaPlayer, mediaHost, - metricsLogger, uiEventLogger, qsLogger, dumpManager, splitShadeStateController); + metricsLogger, uiEventLogger, qsLogger, dumpManager, splitShadeStateController, + vibratorHelper); mTunerService = tunerService; mQsCustomizerController = qsCustomizerController; mQsTileRevealControllerFactory = qsTileRevealControllerFactory; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index 975c871bd006..5e12b9d4cc34 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -39,6 +39,7 @@ import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileViewImpl; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.SplitShadeStateController; import com.android.systemui.util.ViewController; import com.android.systemui.util.animation.DisappearParameters; @@ -87,6 +88,8 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr private SplitShadeStateController mSplitShadeStateController; + private final VibratorHelper mVibratorHelper; + @VisibleForTesting protected final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener = new QSPanel.OnConfigurationChangedListener() { @@ -144,7 +147,8 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr UiEventLogger uiEventLogger, QSLogger qsLogger, DumpManager dumpManager, - SplitShadeStateController splitShadeStateController + SplitShadeStateController splitShadeStateController, + VibratorHelper vibratorHelper ) { super(view); mHost = host; @@ -158,6 +162,7 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr mSplitShadeStateController = splitShadeStateController; mShouldUseSplitNotificationShade = mSplitShadeStateController.shouldUseSplitNotificationShade(getResources()); + mVibratorHelper = vibratorHelper; } @Override @@ -300,7 +305,8 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr } private void addTile(final QSTile tile, boolean collapsedView) { - final QSTileViewImpl tileView = new QSTileViewImpl(getContext(), collapsedView); + final QSTileViewImpl tileView = new QSTileViewImpl( + getContext(), collapsedView, mVibratorHelper); final TileRecord r = new TileRecord(tile, tileView); // TODO(b/250618218): Remove the QSLogger in QSTileViewImpl once we know the root cause of // b/250618218. diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java index a8e88da5d288..05bb08813cc5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java @@ -32,6 +32,7 @@ import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.dagger.QSScope; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.res.R; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.SplitShadeStateController; import com.android.systemui.util.leak.RotationUtils; @@ -56,10 +57,11 @@ public class QuickQSPanelController extends QSPanelControllerBase<QuickQSPanel> @Named(QS_USING_COLLAPSED_LANDSCAPE_MEDIA) Provider<Boolean> usingCollapsedLandscapeMediaProvider, MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger, - DumpManager dumpManager, SplitShadeStateController splitShadeStateController + DumpManager dumpManager, SplitShadeStateController splitShadeStateController, + VibratorHelper vibratorHelper ) { super(view, qsHost, qsCustomizerController, usingMediaPlayer, mediaHost, metricsLogger, - uiEventLogger, qsLogger, dumpManager, splitShadeStateController); + uiEventLogger, qsLogger, dumpManager, splitShadeStateController, vibratorHelper); mUsingCollapsedLandscapeMediaProvider = usingCollapsedLandscapeMediaProvider; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSLongPressProperties.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSLongPressProperties.kt new file mode 100644 index 000000000000..a2ded6a6aacf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSLongPressProperties.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 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.qs.tileimpl + +/** + * List of properties that define the state of a tile during a long-press gesture. + * + * These properties are used during animation if a tile supports a long-press action. + */ +data class QSLongPressProperties( + var xScale: Float, + var yScale: Float, + var cornerRadius: Float, + var backgroundColor: Int, + var labelColor: Int, + var secondaryLabelColor: Int, + var chevronColor: Int, + var overlayColor: Int, + var iconColor: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index 6cc682ae3c96..63963ded2923 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -26,6 +26,7 @@ import android.content.res.Resources.ID_NULL import android.graphics.Color import android.graphics.PorterDuff import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable import android.graphics.drawable.LayerDrawable import android.graphics.drawable.RippleDrawable import android.os.Trace @@ -36,6 +37,7 @@ import android.util.TypedValue import android.view.Gravity import android.view.LayoutInflater import android.view.View +import android.view.ViewConfiguration import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo @@ -48,9 +50,12 @@ import androidx.annotation.VisibleForTesting import com.android.app.tracing.traceSection import com.android.settingslib.Utils import com.android.systemui.Flags +import com.android.systemui.Flags.quickSettingsVisualHapticsLongpress import com.android.systemui.FontSizeUtils import com.android.systemui.animation.LaunchableView import com.android.systemui.animation.LaunchableViewDelegate +import com.android.systemui.haptics.qs.QSLongPressEffect +import com.android.systemui.haptics.qs.QSLongPressEffectViewBinder import com.android.systemui.plugins.qs.QSIconView import com.android.systemui.plugins.qs.QSTile import com.android.systemui.plugins.qs.QSTile.AdapterState @@ -58,12 +63,15 @@ import com.android.systemui.plugins.qs.QSTileView import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH import com.android.systemui.res.R +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.util.children import java.util.Objects private const val TAG = "QSTileViewImpl" open class QSTileViewImpl @JvmOverloads constructor( context: Context, - private val collapsed: Boolean = false + private val collapsed: Boolean = false, + private val vibratorHelper: VibratorHelper? = null, ) : QSTileView(context), HeightOverrideable, LaunchableView { companion object { @@ -163,6 +171,7 @@ open class QSTileViewImpl @JvmOverloads constructor( private var lastStateDescription: CharSequence? = null private var tileState = false private var lastState = INVALID + private var lastIconTint = 0 private val launchableViewDelegate = LaunchableViewDelegate( this, superSetVisibility = { super.setVisibility(it) }, @@ -171,6 +180,12 @@ open class QSTileViewImpl @JvmOverloads constructor( private val locInScreen = IntArray(2) + /** Visuo-haptic long-press effects */ + private var longPressEffect: QSLongPressEffect? = null + private var initialLongPressProperties: QSLongPressProperties? = null + private var finalLongPressProperties: QSLongPressProperties? = null + private val colorEvaluator = ArgbEvaluator.getInstance() + init { val typedValue = TypedValue() if (!getContext().theme.resolveAttribute(R.attr.isQsTheme, typedValue, true)) { @@ -339,6 +354,9 @@ open class QSTileViewImpl @JvmOverloads constructor( true } ) + if (quickSettingsVisualHapticsLongpress()) { + isHapticFeedbackEnabled = false // Haptics will be handled by the [QSLongPressEffect] + } } private fun init( @@ -589,6 +607,27 @@ open class QSTileViewImpl @JvmOverloads constructor( lastState = state.state lastDisabledByPolicy = state.disabledByPolicy + lastIconTint = icon.getColor(state) + + // Long-press effects + if (quickSettingsVisualHapticsLongpress()){ + if (state.handlesLongClick) { + // initialize the long-press effect and set it as the touch listener + showRippleEffect = false + initializeLongPressEffect() + setOnTouchListener(longPressEffect) + QSLongPressEffectViewBinder.bind(this, longPressEffect) + } else { + // Long-press effects might have been enabled before but the new state does not + // handle a long-press. In this case, we go back to the behaviour of a regular tile + // and clean-up the resources + showRippleEffect = isClickable + setOnTouchListener(null) + longPressEffect = null + initialLongPressProperties = null + finalLongPressProperties = null + } + } } private fun setAllColors( @@ -709,6 +748,140 @@ open class QSTileViewImpl @JvmOverloads constructor( } } + override fun onActivityLaunchAnimationEnd() = resetLongPressEffectProperties() + + fun updateLongPressEffectProperties(effectProgress: Float) { + if (!isLongClickable) return + setAllColors( + colorEvaluator.evaluate( + effectProgress, + initialLongPressProperties?.backgroundColor ?: 0, + finalLongPressProperties?.backgroundColor ?: 0, + ) as Int, + colorEvaluator.evaluate( + effectProgress, + initialLongPressProperties?.labelColor ?: 0, + finalLongPressProperties?.labelColor ?: 0, + ) as Int, + colorEvaluator.evaluate( + effectProgress, + initialLongPressProperties?.secondaryLabelColor ?: 0, + finalLongPressProperties?.secondaryLabelColor ?: 0, + ) as Int, + colorEvaluator.evaluate( + effectProgress, + initialLongPressProperties?.chevronColor ?: 0, + finalLongPressProperties?.chevronColor ?: 0, + ) as Int, + colorEvaluator.evaluate( + effectProgress, + initialLongPressProperties?.overlayColor ?: 0, + finalLongPressProperties?.overlayColor ?: 0, + ) as Int, + ) + icon.setTint( + icon.mIcon as ImageView, + colorEvaluator.evaluate( + effectProgress, + initialLongPressProperties?.iconColor ?: 0, + finalLongPressProperties?.iconColor ?: 0, + ) as Int, + ) + + val newScaleX = + interpolateFloat( + effectProgress, + initialLongPressProperties?.xScale ?: 1f, + finalLongPressProperties?.xScale ?: 1f, + ) + val newScaleY = + interpolateFloat( + effectProgress, + initialLongPressProperties?.xScale ?: 1f, + finalLongPressProperties?.xScale ?: 1f, + ) + val newRadius = + interpolateFloat( + effectProgress, + initialLongPressProperties?.cornerRadius ?: 0f, + finalLongPressProperties?.cornerRadius ?: 0f, + ) + scaleX = newScaleX + scaleY = newScaleY + for (child in children) { + child.scaleX = 1f / newScaleX + child.scaleY = 1f / newScaleY + } + changeCornerRadius(newRadius) + } + + private fun interpolateFloat(fraction: Float, start: Float, end: Float): Float = + start + fraction * (end - start) + + private fun resetLongPressEffectProperties() { + scaleY = 1f + scaleX = 1f + for (child in children) { + child.scaleY = 1f + child.scaleX = 1f + } + changeCornerRadius(resources.getDimensionPixelSize(R.dimen.qs_corner_radius).toFloat()) + setAllColors( + getBackgroundColorForState(lastState, lastDisabledByPolicy), + getLabelColorForState(lastState, lastDisabledByPolicy), + getSecondaryLabelColorForState(lastState, lastDisabledByPolicy), + getChevronColorForState(lastState, lastDisabledByPolicy), + getOverlayColorForState(lastState), + ) + icon.setTint(icon.mIcon as ImageView, lastIconTint) + } + + private fun initializeLongPressEffect() { + initializeLongPressProperties() + longPressEffect = + QSLongPressEffect( + vibratorHelper, + ViewConfiguration.getLongPressTimeout() - ViewConfiguration.getTapTimeout(), + ) + } + + private fun initializeLongPressProperties() { + initialLongPressProperties = + QSLongPressProperties( + /* xScale= */1f, + /* yScale= */1f, + resources.getDimensionPixelSize(R.dimen.qs_corner_radius).toFloat(), + getBackgroundColorForState(lastState), + getLabelColorForState(lastState), + getSecondaryLabelColorForState(lastState), + getChevronColorForState(lastState), + getOverlayColorForState(lastState), + lastIconTint, + ) + + finalLongPressProperties = + QSLongPressProperties( + /* xScale= */1.1f, + /* yScale= */1.2f, + resources.getDimensionPixelSize(R.dimen.qs_corner_radius).toFloat() - 20, + getBackgroundColorForState(Tile.STATE_ACTIVE), + getLabelColorForState(Tile.STATE_ACTIVE), + getSecondaryLabelColorForState(Tile.STATE_ACTIVE), + getChevronColorForState(Tile.STATE_ACTIVE), + getOverlayColorForState(Tile.STATE_ACTIVE), + Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive), + ) + } + + private fun changeCornerRadius(radius: Float) { + for (i in 0 until backgroundDrawable.numberOfLayers) { + val layer = backgroundDrawable.getDrawable(i) + if (layer is GradientDrawable) { + layer.cornerRadius = radius + } + } + } + @VisibleForTesting internal fun getCurrentColors(): List<Int> = listOf( backgroundColor, diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt index 0d9b702b49ec..8a84496e5e32 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt @@ -19,6 +19,7 @@ package com.android.systemui.recordissue import android.app.NotificationManager import android.content.Context import android.content.Intent +import android.content.pm.LauncherApps import android.content.res.Resources import android.net.Uri import android.os.Handler @@ -93,6 +94,10 @@ constructor( } ACTION_STOP, ACTION_STOP_NOTIF -> { + // ViewCapture needs to save it's data before it is disabled, or else the data will + // be lost. This is expected to change in the near future, and when that happens + // this line should be removed. + getSystemService(LauncherApps::class.java)?.saveViewCaptureData() TraceUtils.traceStop(contentResolver) } ACTION_SHARE -> { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt index f01e9bea1372..1f6d2122ebfc 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt @@ -21,9 +21,7 @@ import android.app.Notification import android.content.Context import android.graphics.Bitmap import android.graphics.Rect -import android.graphics.drawable.Drawable import android.util.Log -import android.view.Display import android.view.KeyEvent import android.view.LayoutInflater import android.view.ScrollCaptureResponse @@ -32,45 +30,53 @@ import android.view.ViewTreeObserver import android.view.WindowInsets import android.window.OnBackInvokedCallback import android.window.OnBackInvokedDispatcher +import androidx.appcompat.content.res.AppCompatResources import com.android.internal.logging.UiEventLogger import com.android.systemui.flags.FeatureFlags import com.android.systemui.res.R import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject /** * Legacy implementation of screenshot view methods. Just proxies the calls down into the original * ScreenshotView. */ -class LegacyScreenshotViewProxy(context: Context, private val logger: UiEventLogger) : - ScreenshotViewProxy { +class LegacyScreenshotViewProxy +@AssistedInject +constructor( + private val logger: UiEventLogger, + flags: FeatureFlags, + @Assisted private val context: Context, + @Assisted private val displayId: Int +) : ScreenshotViewProxy { override val view: ScreenshotView = LayoutInflater.from(context).inflate(R.layout.screenshot, null) as ScreenshotView override val screenshotPreview: View - - override var defaultDisplay: Int = Display.DEFAULT_DISPLAY - set(value) { - view.setDefaultDisplay(value) - } - override var defaultTimeoutMillis: Long = 6000 - set(value) { - view.setDefaultTimeoutMillis(value) - } - override var flags: FeatureFlags? = null - set(value) { - view.setFlags(value) - } override var packageName: String = "" set(value) { + field = value view.setPackageName(value) } override var callbacks: ScreenshotView.ScreenshotViewCallback? = null set(value) { + field = value view.setCallbacks(value) } override var screenshot: ScreenshotData? = null set(value) { - view.setScreenshot(value) + field = value + value?.let { + val badgeBg = + AppCompatResources.getDrawable(context, R.drawable.overlay_badge_background) + val user = it.userHandle + if (badgeBg != null && user != null) { + view.badgeScreenshot(context.packageManager.getUserBadgedIcon(badgeBg, user)) + } + view.setScreenshot(it) + } } override val isAttachedToWindow @@ -82,6 +88,8 @@ class LegacyScreenshotViewProxy(context: Context, private val logger: UiEventLog init { view.setUiEventLogger(logger) + view.setDefaultDisplay(displayId) + view.setFlags(flags) addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } if (LogConfig.DEBUG_WINDOW) { @@ -95,8 +103,6 @@ class LegacyScreenshotViewProxy(context: Context, private val logger: UiEventLog override fun updateInsets(insets: WindowInsets) = view.updateInsets(insets) override fun updateOrientation(insets: WindowInsets) = view.updateOrientation(insets) - override fun badgeScreenshot(userBadgedIcon: Drawable) = view.badgeScreenshot(userBadgedIcon) - override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator = view.createScreenshotDropInAnimation(screenRect, showFlash) @@ -130,14 +136,17 @@ class LegacyScreenshotViewProxy(context: Context, private val logger: UiEventLog response: ScrollCaptureResponse, screenBitmap: Bitmap, newScreenshot: Bitmap, - screenshotTakenInPortrait: Boolean - ) = + screenshotTakenInPortrait: Boolean, + onTransitionPrepared: Runnable, + ) { view.prepareScrollingTransition( response, screenBitmap, newScreenshot, screenshotTakenInPortrait ) + view.post { onTransitionPrepared.run() } + } override fun startLongScreenshotTransition( transitionDestination: Rect, @@ -155,10 +164,19 @@ class LegacyScreenshotViewProxy(context: Context, private val logger: UiEventLog override fun announceForAccessibility(string: String) = view.announceForAccessibility(string) - override fun getViewTreeObserver(): ViewTreeObserver = view.viewTreeObserver - - override fun post(runnable: Runnable) { - view.post(runnable) + override fun prepareEntranceAnimation(runnable: Runnable) { + view.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + if (LogConfig.DEBUG_WINDOW) { + Log.d(TAG, "onPreDraw: startAnimation") + } + view.viewTreeObserver.removeOnPreDrawListener(this) + runnable.run() + return true + } + } + ) } private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) { @@ -166,7 +184,7 @@ class LegacyScreenshotViewProxy(context: Context, private val logger: UiEventLog if (LogConfig.DEBUG_INPUT) { Log.d(TAG, "Predictive Back callback dispatched") } - onDismissRequested.invoke(ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER) + onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) } view.addOnAttachStateChangeListener( object : View.OnAttachStateChangeListener { @@ -201,7 +219,7 @@ class LegacyScreenshotViewProxy(context: Context, private val logger: UiEventLog if (LogConfig.DEBUG_INPUT) { Log.d(TAG, "onKeyEvent: $keyCode") } - onDismissRequested.invoke(ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER) + onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) return true } return false @@ -210,10 +228,9 @@ class LegacyScreenshotViewProxy(context: Context, private val logger: UiEventLog ) } - class Factory : ScreenshotViewProxy.Factory { - override fun getProxy(context: Context, logger: UiEventLogger): ScreenshotViewProxy { - return LegacyScreenshotViewProxy(context, logger) - } + @AssistedFactory + interface Factory : ScreenshotViewProxy.Factory { + override fun getProxy(context: Context, displayId: Int): LegacyScreenshotViewProxy } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 6bab956ca09a..198a29c4ed5b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -228,7 +228,7 @@ public class ScreenshotController { // From WizardManagerHelper.java private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete"; - private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000; + static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000; private final WindowContext mContext; private final FeatureFlags mFlags; @@ -344,7 +344,7 @@ public class ScreenshotController { mMessageContainerController = messageContainerController; mAssistContentRequester = assistContentRequester; - mViewProxy = viewProxyFactory.getProxy(mContext, mUiEventLogger); + mViewProxy = viewProxyFactory.getProxy(mContext, mDisplayId); mScreenshotHandler.setOnTimeoutRunnable(() -> { if (DEBUG_UI) { @@ -460,7 +460,7 @@ public class ScreenshotController { attachWindow(); - boolean showFlash = true; + boolean showFlash; if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) { if (screenshot.getScreenBounds() != null && aspectRatiosMatch(screenshot.getBitmap(), screenshot.getInsets(), @@ -472,15 +472,14 @@ public class ScreenshotController { screenshot.setScreenBounds(new Rect(0, 0, screenshot.getBitmap().getWidth(), screenshot.getBitmap().getHeight())); } + } else { + showFlash = true; } - prepareAnimation(screenshot.getScreenBounds(), showFlash, () -> { - mMessageContainerController.onScreenshotTaken(screenshot); - }); + mViewProxy.prepareEntranceAnimation( + () -> startAnimation(screenshot.getScreenBounds(), showFlash, + () -> mMessageContainerController.onScreenshotTaken(screenshot))); - mViewProxy.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon( - mContext.getDrawable(R.drawable.overlay_badge_background), - screenshot.getUserHandle())); mViewProxy.setScreenshot(screenshot); // ignore system bar insets for the purpose of window layout @@ -596,9 +595,6 @@ public class ScreenshotController { setWindowFocusable(false); } }); - mViewProxy.setFlags(mFlags); - mViewProxy.setDefaultDisplay(mDisplayId); - mViewProxy.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis()); if (DEBUG_WINDOW) { Log.d(TAG, "setContentView: " + mViewProxy.getView()); @@ -606,22 +602,6 @@ public class ScreenshotController { setContentView(mViewProxy.getView()); } - private void prepareAnimation(Rect screenRect, boolean showFlash, - Runnable onAnimationComplete) { - mViewProxy.getViewTreeObserver().addOnPreDrawListener( - new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - if (DEBUG_WINDOW) { - Log.d(TAG, "onPreDraw: startAnimation"); - } - mViewProxy.getViewTreeObserver().removeOnPreDrawListener(this); - startAnimation(screenRect, showFlash, onAnimationComplete); - return true; - } - }); - } - private void enqueueScrollCaptureRequest(UserHandle owner) { // Wait until this window is attached to request because it is // the reference used to locate the target window (below). @@ -706,10 +686,14 @@ public class ScreenshotController { Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplayId, new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)); - mViewProxy.prepareScrollingTransition(response, mScreenBitmap, newScreenshot, - mScreenshotTakenInPortrait); - // delay starting scroll capture to make sure the scrim is up before the app moves - mViewProxy.post(() -> runBatchScrollCapture(response, owner)); + if (newScreenshot != null) { + // delay starting scroll capture to make sure scrim is up before the app moves + mViewProxy.prepareScrollingTransition( + response, mScreenBitmap, newScreenshot, mScreenshotTakenInPortrait, + () -> runBatchScrollCapture(response, owner)); + } else { + Log.wtf(TAG, "failed to capture current screenshot for scroll transition"); + } }); } catch (InterruptedException | ExecutionException e) { Log.e(TAG, "requestScrollCapture failed", e); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index 8a8766dbab94..1c5a8a1a9fd3 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -26,6 +26,7 @@ import static com.android.systemui.screenshot.LogConfig.DEBUG_SCROLL; import static com.android.systemui.screenshot.LogConfig.DEBUG_UI; import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW; import static com.android.systemui.screenshot.LogConfig.logTag; +import static com.android.systemui.screenshot.ScreenshotController.SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS; import static java.util.Objects.requireNonNull; @@ -33,6 +34,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.BroadcastOptions; import android.app.Notification; @@ -168,7 +170,6 @@ public class ScreenshotView extends FrameLayout implements private ScreenshotData mScreenshotData; private final InteractionJankMonitor mInteractionJankMonitor; - private long mDefaultTimeoutOfTimeoutHandler; private FeatureFlags mFlags; private final Bundle mInteractiveBroadcastOption; @@ -244,10 +245,6 @@ public class ScreenshotView extends FrameLayout implements return InteractionJankMonitor.getInstance(); } - void setDefaultTimeoutMillis(long timeout) { - mDefaultTimeoutOfTimeoutHandler = timeout; - } - public void hideScrollChip() { mScrollChip.setVisibility(View.GONE); } @@ -755,7 +752,7 @@ public class ScreenshotView extends FrameLayout implements InteractionJankMonitor.Configuration.Builder.withView( CUJ_TAKE_SCREENSHOT, mScreenshotStatic) .setTag("Actions") - .setTimeout(mDefaultTimeoutOfTimeoutHandler); + .setTimeout(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS); mInteractionJankMonitor.begin(builder); } }); @@ -781,7 +778,7 @@ public class ScreenshotView extends FrameLayout implements return animator; } - void badgeScreenshot(Drawable badge) { + void badgeScreenshot(@Nullable Drawable badge) { mScreenshotBadge.setImageDrawable(badge); mScreenshotBadge.setVisibility(badge != null ? View.VISIBLE : View.GONE); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt index d5c7f95ce289..182b8894677a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt @@ -21,23 +21,16 @@ import android.app.Notification import android.content.Context import android.graphics.Bitmap import android.graphics.Rect -import android.graphics.drawable.Drawable import android.view.ScrollCaptureResponse import android.view.View import android.view.ViewGroup -import android.view.ViewTreeObserver import android.view.WindowInsets -import com.android.internal.logging.UiEventLogger -import com.android.systemui.flags.FeatureFlags /** Abstraction of the surface between ScreenshotController and ScreenshotView */ interface ScreenshotViewProxy { val view: ViewGroup val screenshotPreview: View - var defaultDisplay: Int - var defaultTimeoutMillis: Long - var flags: FeatureFlags? var packageName: String var callbacks: ScreenshotView.ScreenshotViewCallback? var screenshot: ScreenshotData? @@ -49,7 +42,6 @@ interface ScreenshotViewProxy { fun reset() fun updateInsets(insets: WindowInsets) fun updateOrientation(insets: WindowInsets) - fun badgeScreenshot(userBadgedIcon: Drawable) fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator fun addQuickShareChip(quickShareAction: Notification.Action) fun setChipIntents(imageData: ScreenshotController.SavedImageData) @@ -61,7 +53,8 @@ interface ScreenshotViewProxy { response: ScrollCaptureResponse, screenBitmap: Bitmap, newScreenshot: Bitmap, - screenshotTakenInPortrait: Boolean + screenshotTakenInPortrait: Boolean, + onTransitionPrepared: Runnable, ) fun startLongScreenshotTransition( transitionDestination: Rect, @@ -73,10 +66,9 @@ interface ScreenshotViewProxy { fun stopInputListening() fun requestFocus() fun announceForAccessibility(string: String) - fun getViewTreeObserver(): ViewTreeObserver - fun post(runnable: Runnable) + fun prepareEntranceAnimation(runnable: Runnable) interface Factory { - fun getProxy(context: Context, logger: UiEventLogger): ScreenshotViewProxy + fun getProxy(context: Context, displayId: Int): ScreenshotViewProxy } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java b/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java index 71c2cb4a5cb9..5df6c45295b6 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java @@ -40,7 +40,7 @@ public class TimeoutHandler extends Handler { private final Context mContext; private Runnable mOnTimeout; - private int mDefaultTimeout = DEFAULT_TIMEOUT_MILLIS; + int mDefaultTimeout = DEFAULT_TIMEOUT_MILLIS; @Inject public TimeoutHandler(Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java index a00c81d43b9e..cdb9abb15e84 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java @@ -86,7 +86,8 @@ public abstract class ScreenshotModule { ScreenshotSoundControllerImpl screenshotSoundProviderImpl); @Provides - static ScreenshotViewProxy.Factory providesScreenshotViewProxyFactory() { - return new LegacyScreenshotViewProxy.Factory(); + static ScreenshotViewProxy.Factory providesScreenshotViewProxyFactory( + LegacyScreenshotViewProxy.Factory legacyScreenshotViewProxyFactory) { + return legacyScreenshotViewProxyFactory; } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index cdb520fb0879..8b791de429ed 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1540,13 +1540,13 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mKeyguardBottomArea = keyguardBottomArea; } - @Override + /** Sets a listener to be notified when the shade starts opening or finishes closing. */ public void setOpenCloseListener(OpenCloseListener openCloseListener) { SceneContainerFlag.assertInLegacyMode(); mOpenCloseListener = openCloseListener; } - @Override + /** Sets a listener to be notified when touch tracking begins. */ public void setTrackingStartedListener(TrackingStartedListener trackingStartedListener) { mTrackingStartedListener = trackingStartedListener; } @@ -1753,10 +1753,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private void updateKeyguardStatusViewAlignment(boolean animate) { + boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered(); if (migrateClocksToBlueprint()) { + mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered); return; } - boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered(); ConstraintLayout layout = mNotificationContainerParent; mKeyguardStatusViewController.updateAlignment( layout, mSplitShadeEnabled, shouldBeCentered, animate); @@ -2603,7 +2604,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump return maxHeight; } - @Override public boolean isExpandingOrCollapsing() { float lockscreenExpansionProgress = mQsController.getLockscreenShadeDragProgress(); return mIsExpandingOrCollapsing diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/UiState.kt b/packages/SystemUI/src/com/android/systemui/shade/OpenCloseListener.kt index 98a9e93047ae..108dd47874c3 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/UiState.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/OpenCloseListener.kt @@ -14,16 +14,13 @@ * limitations under the License. */ -package com.android.credentialmanager.ui.screens +package com.android.systemui.shade -import androidx.activity.result.IntentSenderRequest +/** Listens for when shade begins opening or finishes closing. */ +interface OpenCloseListener { + /** Called when the shade finishes closing. */ + fun onClosingFinished() -sealed class UiState { - data object CredentialScreen : UiState() - - data class CredentialSelected( - val intentSenderRequest: IntentSenderRequest? - ) : UiState() - - data object Cancel : UiState() + /** Called when the shade starts opening. */ + fun onOpenStarted() } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index 27168a799086..177c3db6b720 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -89,8 +89,7 @@ constructor( override fun isShadeFullyOpen(): Boolean = shadeInteractor.isAnyFullyExpanded.value - override fun isExpandingOrCollapsing(): Boolean = - shadeInteractor.anyExpansion.value > 0f && shadeInteractor.anyExpansion.value < 1f + override fun isExpandingOrCollapsing(): Boolean = shadeInteractor.isUserInteracting.value override fun instantExpandShade() { // Do nothing diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLockscreenInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLockscreenInteractor.kt index a9ba6f96b7d2..859fce53a371 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLockscreenInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLockscreenInteractor.kt @@ -27,9 +27,6 @@ interface ShadeLockscreenInteractor { */ @Deprecated("Use ShadeInteractor instead") fun expandToNotifications() - /** Returns whether the shade is expanding or collapsing itself or quick settings. */ - val isExpandingOrCollapsing: Boolean - /** * Returns whether the shade height is greater than zero (i.e. partially or fully expanded), * there is a HUN, the shade is animating, or the shade is instantly expanding. diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt index 07236d1e5ab7..de21a73e312b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt @@ -31,9 +31,6 @@ import java.util.function.Consumer * @see NotificationPanelViewController */ interface ShadeViewController { - /** Returns whether the shade is expanding or collapsing itself or quick settings. */ - val isExpandingOrCollapsing: Boolean - /** * Returns whether the shade height is greater than zero or the shade is expecting a synthesized * down event. @@ -52,15 +49,9 @@ interface ShadeViewController { /** Returns whether the shade's top level view is enabled. */ val isViewEnabled: Boolean - /** Sets a listener to be notified when the shade starts opening or finishes closing. */ - fun setOpenCloseListener(openCloseListener: OpenCloseListener) - /** Returns whether status bar icons should be hidden when the shade is expanded. */ fun shouldHideStatusBarIconsWhenExpanded(): Boolean - /** Sets a listener to be notified when touch tracking begins. */ - fun setTrackingStartedListener(trackingStartedListener: TrackingStartedListener) - /** * Disables the shade header. * @@ -250,17 +241,3 @@ interface ShadeViewStateProvider { /** Return the fraction of the shade that's expanded, when in lockscreen. */ val lockscreenShadeDragProgress: Float } - -/** Listens for when touch tracking begins. */ -interface TrackingStartedListener { - fun onTrackingStarted() -} - -/** Listens for when shade begins opening or finishes closing. */ -interface OpenCloseListener { - /** Called when the shade finishes closing. */ - fun onClosingFinished() - - /** Called when the shade starts opening. */ - fun onOpenStarted() -} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt index 6e036863216b..b67156f4b982 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt @@ -34,7 +34,6 @@ class ShadeViewControllerEmptyImpl @Inject constructor() : ShadeLockscreenInteractor, PanelExpansionInteractor { override fun expandToNotifications() {} - override val isExpandingOrCollapsing: Boolean = false override val isExpanded: Boolean = false override val isPanelExpanded: Boolean = false override fun animateCollapseQs(fullyCollapse: Boolean) {} @@ -43,10 +42,8 @@ class ShadeViewControllerEmptyImpl @Inject constructor() : override val isFullyCollapsed: Boolean = false override val isTracking: Boolean = false override val isViewEnabled: Boolean = false - override fun setOpenCloseListener(openCloseListener: OpenCloseListener) {} override fun shouldHideStatusBarIconsWhenExpanded() = false override fun blockExpansionForCurrentTouch() {} - override fun setTrackingStartedListener(trackingStartedListener: TrackingStartedListener) {} override fun disableHeader(state1: Int, state2: Int, animated: Boolean) {} override fun startExpandLatencyTracking() {} override fun startBouncerPreHideAnimation() {} diff --git a/packages/SystemUI/src/com/android/systemui/shade/TrackingStartedListener.kt b/packages/SystemUI/src/com/android/systemui/shade/TrackingStartedListener.kt new file mode 100644 index 000000000000..3803c27d5f37 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/TrackingStartedListener.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 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.shade + +/** Listens for when touch tracking begins. */ +interface TrackingStartedListener { + fun onTrackingStarted() +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt index 1f78ae8b6e99..3d9337ee5c89 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt @@ -38,8 +38,6 @@ constructor( changeToShadeScene() } - override val isExpandingOrCollapsing = shadeInteractor.isUserInteracting.value - override val isExpanded = shadeInteractor.isAnyExpanded.value override fun startBouncerPreHideAnimation() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 615534809c97..5171a5c9144c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -39,8 +39,6 @@ import com.android.app.animation.Interpolators; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.SystemBarUtils; import com.android.systemui.animation.ShadeInterpolation; -import com.android.systemui.flags.Flags; -import com.android.systemui.flags.RefactorFlag; import com.android.systemui.res.R; import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.notification.ColorUpdateLogger; @@ -95,8 +93,6 @@ public class NotificationShelf extends ActivatableNotificationView { private float mCornerAnimationDistance; private float mActualWidth = -1; private int mMaxIconsOnLockscreen; - private final RefactorFlag mSensitiveRevealAnim = - RefactorFlag.forView(Flags.SENSITIVE_REVEAL_ANIM); private boolean mCanModifyColorOfNotifications; private boolean mCanInteract; private NotificationStackScrollLayout mHostLayout; @@ -266,7 +262,7 @@ public class NotificationShelf extends ActivatableNotificationView { } final float stackEnd = ambientState.getStackY() + ambientState.getStackHeight(); - if (mSensitiveRevealAnim.isEnabled() && viewState.hidden) { + if (viewState.hidden) { // if the shelf is hidden, position it at the end of the stack (plus the clip // padding), such that when it appears animated, it will smoothly move in from the // bottom, without jump cutting any notifications @@ -398,10 +394,6 @@ public class NotificationShelf extends ActivatableNotificationView { // find the first view that doesn't overlap with the shelf int notGoneIndex = 0; int colorOfViewBeforeLast = NO_COLOR; - boolean backgroundForceHidden = false; - if (mHideBackground && !((ShelfState) getViewState()).hasItemsInStableShelf) { - backgroundForceHidden = true; - } int colorTwoBefore = NO_COLOR; int previousColor = NO_COLOR; float transitionAmount = 0.0f; @@ -429,8 +421,7 @@ public class NotificationShelf extends ActivatableNotificationView { expandingAnimated, isLastChild, shelfClipStart); // TODO(b/172289889) scale mPaddingBetweenElements with expansion amount - if ((!mSensitiveRevealAnim.isEnabled() && ((isLastChild && !child.isInShelf()) - || backgroundForceHidden)) || aboveShelf) { + if (aboveShelf) { notificationClipEnd = shelfStart + getIntrinsicHeight(); } else { notificationClipEnd = shelfStart - mPaddingBetweenElements; @@ -440,8 +431,7 @@ public class NotificationShelf extends ActivatableNotificationView { // If the current row is an ExpandableNotificationRow, update its color, roundedness, // and icon state. - if (child instanceof ExpandableNotificationRow) { - ExpandableNotificationRow expandableRow = (ExpandableNotificationRow) child; + if (child instanceof ExpandableNotificationRow expandableRow) { numViewsInShelf += inShelfAmount; int ownColorUntinted = expandableRow.getBackgroundColorWithoutTint(); if (viewStart >= shelfStart && mNotGoneIndex == -1) { @@ -471,16 +461,8 @@ public class NotificationShelf extends ActivatableNotificationView { notGoneIndex++; } - if (child instanceof ActivatableNotificationView) { - ActivatableNotificationView anv = (ActivatableNotificationView) child; - // Because we show whole notifications on the lockscreen, the bottom notification is - // always "just about to enter the shelf" by normal scrolling rules. This is fine - // if the shelf is visible, but if the shelf is hidden, it causes incorrect curling. - // notificationClipEnd handles the discrepancy between a visible and hidden shelf, - // so we use that when on the keyguard (and while animating away) to reduce curling. - final float keyguardSafeShelfStart = !mSensitiveRevealAnim.isEnabled() - && mAmbientState.isOnKeyguard() ? notificationClipEnd : shelfStart; - updateCornerRoundnessOnScroll(anv, viewStart, keyguardSafeShelfStart); + if (child instanceof ActivatableNotificationView anv) { + updateCornerRoundnessOnScroll(anv, viewStart, shelfStart); } } @@ -519,11 +501,10 @@ public class NotificationShelf extends ActivatableNotificationView { mShelfIcons.applyIconStates(); for (int i = 0; i < getHostLayoutChildCount(); i++) { View child = getHostLayoutChildAt(i); - if (!(child instanceof ExpandableNotificationRow) + if (!(child instanceof ExpandableNotificationRow row) || child.getVisibility() == GONE) { continue; } - ExpandableNotificationRow row = (ExpandableNotificationRow) child; updateContinuousClipping(row); } boolean hideBackground = isHidden; @@ -613,8 +594,7 @@ public class NotificationShelf extends ActivatableNotificationView { private void clipTransientViews() { for (int i = 0; i < getHostLayoutTransientViewCount(); i++) { View transientView = getHostLayoutTransientView(i); - if (transientView instanceof ExpandableView) { - ExpandableView transientExpandableView = (ExpandableView) transientView; + if (transientView instanceof ExpandableView transientExpandableView) { updateNotificationClipHeight(transientExpandableView, getTranslationY(), -1); } } @@ -871,10 +851,9 @@ public class NotificationShelf extends ActivatableNotificationView { } private void setIconTransformationAmount(ExpandableView view, float transitionAmount) { - if (!(view instanceof ExpandableNotificationRow)) { + if (!(view instanceof ExpandableNotificationRow row)) { return; } - ExpandableNotificationRow row = (ExpandableNotificationRow) view; StatusBarIconView icon = row.getShelfIcon(); NotificationIconContainer.IconState iconState = getIconState(icon); if (iconState == null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 5f3a83aa35e0..c05c3c3df2c9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -278,8 +278,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private OnExpandClickListener mOnExpandClickListener; private View.OnClickListener mOnFeedbackClickListener; private Path mExpandingClipPath; - private final RefactorFlag mInlineReplyAnimation = - RefactorFlag.forView(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION); private static boolean shouldSimulateSlowMeasure() { return Compile.IS_DEBUG && RefactorFlag.forView( @@ -355,7 +353,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView nowExpanded = !isExpanded(); setUserExpanded(nowExpanded); } - notifyHeightChanged(true); + notifyHeightChanged(/* needsAnimation= */ true); mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded); mMetricsLogger.action(MetricsEvent.ACTION_NOTIFICATION_EXPANDER, nowExpanded); } @@ -778,7 +776,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainer.updateGroupOverflow(); } if (intrinsicBefore != getIntrinsicHeight()) { - notifyHeightChanged(false /* needsAnimation */); + notifyHeightChanged(/* needsAnimation= */ false); } if (isHeadsUp) { mMustStayOnScreen = true; @@ -826,7 +824,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mChildrenContainer != null) { mChildrenContainer.setHeaderVisibleAmount(headerVisibleAmount); } - notifyHeightChanged(false /* needsAnimation */); + notifyHeightChanged(/* needsAnimation= */ false); } } @@ -1088,7 +1086,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView boolean wasAboveShelf = isAboveShelf(); mIsPinned = pinned; if (intrinsicHeight != getIntrinsicHeight()) { - notifyHeightChanged(false /* needsAnimation */); + notifyHeightChanged(/* needsAnimation= */ false); } if (pinned) { setAnimationRunning(true); @@ -2611,7 +2609,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView onExpansionChanged(true /* userAction */, wasExpanded); if (!wasExpanded && isExpanded() && getActualHeight() != getIntrinsicHeight()) { - notifyHeightChanged(true /* needsAnimation */); + notifyHeightChanged(/* needsAnimation= */ true); } } @@ -2623,7 +2621,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren) { mChildrenContainer.onExpansionChanged(); } - notifyHeightChanged(false /* needsAnimation */); + notifyHeightChanged(/* needsAnimation= */ false); } updateShelfIconColor(); } @@ -2661,7 +2659,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (expand != mIsSystemExpanded) { final boolean wasExpanded = isExpanded(); mIsSystemExpanded = expand; - notifyHeightChanged(false /* needsAnimation */); + notifyHeightChanged(/* needsAnimation= */ false); onExpansionChanged(false /* userAction */, wasExpanded); if (mIsSummaryWithChildren) { mChildrenContainer.updateGroupOverflow(); @@ -2680,7 +2678,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren) { mChildrenContainer.updateGroupOverflow(); } - notifyHeightChanged(false /* needsAnimation */); + notifyHeightChanged(/* needsAnimation= */ false); } if (isAboveShelf() != wasAboveShelf) { mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf); @@ -2837,7 +2835,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView super.onLayout(changed, left, top, right, bottom); if (intrinsicBefore != getIntrinsicHeight() && (intrinsicBefore != 0 || getActualHeight() > 0)) { - notifyHeightChanged(true /* needsAnimation */); + notifyHeightChanged(/* needsAnimation= */ true); } if (mMenuRow != null && mMenuRow.getMenuView() != null) { mMenuRow.onParentHeightUpdate(); @@ -2880,8 +2878,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mSensitiveHiddenInGeneral = hideSensitive; int intrinsicAfter = getIntrinsicHeight(); if (intrinsicBefore != intrinsicAfter) { - boolean needsAnimation = mFeatureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM); - notifyHeightChanged(needsAnimation); + notifyHeightChanged(/* needsAnimation= */ true); } } @@ -3018,7 +3015,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (isChildInGroup()) { mGroupExpansionManager.setGroupExpanded(mEntry, true); } - notifyHeightChanged(false /* needsAnimation */); + notifyHeightChanged(/* needsAnimation= */ false); } public void setChildrenExpanded(boolean expanded, boolean animate) { @@ -3241,13 +3238,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mGuts.setActualHeight(height); return; } - int contentHeight = Math.max(getMinHeight(), height); for (NotificationContentView l : mLayouts) { - if (mInlineReplyAnimation.isEnabled()) { - l.setContentHeight(height); - } else { - l.setContentHeight(contentHeight); - } + l.setContentHeight(height); } if (mIsSummaryWithChildren) { mChildrenContainer.setActualHeight(height); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 137e1b2ab809..8a3e7e8a0580 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -699,8 +699,7 @@ public class NotificationContentView extends FrameLayout implements Notification int hint; if (mHeadsUpChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_HEADSUP)) { hint = getViewHeight(VISIBLE_TYPE_HEADSUP); - if (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isAnimatingAppearance() - && mHeadsUpRemoteInputController.isFocusAnimationFlagActive()) { + if (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isAnimatingAppearance()) { // While the RemoteInputView is animating its appearance, it should be allowed // to overlap the hint, therefore no space is reserved for the hint during the // appearance animation of the RemoteInputView diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index b20507117558..77e94257c832 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -87,7 +87,6 @@ import com.android.systemui.Dumpable; import com.android.systemui.ExpandHelper; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; -import com.android.systemui.flags.RefactorFlag; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.res.R; @@ -197,8 +196,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ private Set<Integer> mDebugTextUsedYPositions; private final boolean mDebugRemoveAnimation; - private final boolean mSensitiveRevealAnimEndabled; - private final RefactorFlag mAnimatedInsets; private int mContentHeight; private float mIntrinsicContentHeight; private int mPaddingBetweenElements; @@ -619,9 +616,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable Flags.LOCKSCREEN_ENABLE_LANDSCAPE); mDebugLines = mFeatureFlags.isEnabled(Flags.NSSL_DEBUG_LINES); mDebugRemoveAnimation = mFeatureFlags.isEnabled(Flags.NSSL_DEBUG_REMOVE_ANIMATION); - mSensitiveRevealAnimEndabled = mFeatureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM); - mAnimatedInsets = - new RefactorFlag(mFeatureFlags, Flags.ANIMATED_NOTIFICATION_SHADE_INSETS); mSectionsManager = Dependency.get(NotificationSectionsManager.class); mScreenOffAnimationController = Dependency.get(ScreenOffAnimationController.class); @@ -656,9 +650,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mGroupMembershipManager = Dependency.get(GroupMembershipManager.class); mGroupExpansionManager = Dependency.get(GroupExpansionManager.class); setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); - if (mAnimatedInsets.isEnabled()) { - setWindowInsetsAnimationCallback(mInsetsCallback); - } + setWindowInsetsAnimationCallback(mInsetsCallback); } /** @@ -1734,11 +1726,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return; } mForcedScroll = v; - if (mAnimatedInsets.isEnabled()) { - updateForcedScroll(); - } else { - scrollTo(v); - } + updateForcedScroll(); } public boolean scrollTo(View v) { @@ -1783,31 +1771,15 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { - if (!mAnimatedInsets.isEnabled()) { - mBottomInset = insets.getInsets(WindowInsets.Type.ime()).bottom; - } mWaterfallTopInset = 0; final DisplayCutout cutout = insets.getDisplayCutout(); if (cutout != null) { mWaterfallTopInset = cutout.getWaterfallInsets().top; } - if (mAnimatedInsets.isEnabled() && !mIsInsetAnimationRunning) { + if (!mIsInsetAnimationRunning) { // update bottom inset e.g. after rotation updateBottomInset(insets); } - if (!mAnimatedInsets.isEnabled()) { - int range = getScrollRange(); - if (mOwnScrollY > range) { - // HACK: We're repeatedly getting staggered insets here while the IME is - // animating away. To work around that we'll wait until things have settled. - removeCallbacks(mReclamp); - postDelayed(mReclamp, 50); - } else if (mForcedScroll != null) { - // The scroll was requested before we got the actual inset - in case we need - // to scroll up some more do so now. - scrollTo(mForcedScroll); - } - } return insets; } @@ -2576,7 +2548,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return; } child.setOnHeightChangedListener(null); - if (child instanceof ExpandableNotificationRow && mSensitiveRevealAnimEndabled) { + if (child instanceof ExpandableNotificationRow) { NotificationEntry entry = ((ExpandableNotificationRow) child).getEntry(); entry.removeOnSensitivityChangedListener(mOnChildSensitivityChangedListener); } @@ -2872,7 +2844,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private void onViewAddedInternal(ExpandableView child) { updateHideSensitiveForChild(child); child.setOnHeightChangedListener(mOnChildHeightChangedListener); - if (child instanceof ExpandableNotificationRow && mSensitiveRevealAnimEndabled) { + if (child instanceof ExpandableNotificationRow) { NotificationEntry entry = ((ExpandableNotificationRow) child).getEntry(); entry.addOnSensitivityChangedListener(mOnChildSensitivityChangedListener); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index e2e13a192429..ab9ecab8e0c8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -313,7 +313,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba mHeadsUpManager.unpinAll(true /* userUnpinned */); mMetricsLogger.count("panel_open", 1); } else if (!mQsController.getExpanded() - && !mShadeViewController.isExpandingOrCollapsing()) { + && !mShadeController.isExpandingOrCollapsing()) { mShadeController.animateExpandQs(); mMetricsLogger.count("panel_open_qs", 1); } 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 ba89d4ac22cd..7dd328a4aad0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -579,7 +579,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb final boolean hideBouncerOverDream = mDreamOverlayStateController.isOverlayActive() && (mShadeLockscreenInteractor.isExpanded() - || mShadeLockscreenInteractor.isExpandingOrCollapsing()); + || mShadeController.get().isExpandingOrCollapsing()); final boolean isUserTrackingStarted = event.getFraction() != EXPANSION_HIDDEN && event.getTracking(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index 5bced934be7a..9633cb085afa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -135,7 +135,6 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene @Nullable private RevealParams mRevealParams; private Rect mContentBackgroundBounds; - private boolean mIsFocusAnimationFlagActive; private boolean mIsAnimatingAppearance = false; // TODO(b/193539698): move these to a Controller @@ -432,7 +431,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene // case to prevent flicker. if (!mRemoved) { ViewGroup parent = (ViewGroup) getParent(); - if (animate && parent != null && mIsFocusAnimationFlagActive) { + if (animate && parent != null) { ViewGroup grandParent = (ViewGroup) parent.getParent(); View actionsContainer = getActionsContainerLayout(); @@ -497,8 +496,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } private void setTopMargin(int topMargin) { - if (!(getLayoutParams() instanceof FrameLayout.LayoutParams)) return; - final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams(); + if (!(getLayoutParams() instanceof FrameLayout.LayoutParams layoutParams)) return; layoutParams.topMargin = topMargin; setLayoutParams(layoutParams); } @@ -608,24 +606,10 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } /** - * Sets whether the feature flag for the revised inline reply animation is active or not. - * @param active - */ - public void setIsFocusAnimationFlagActive(boolean active) { - mIsFocusAnimationFlagActive = active; - } - - /** * Focuses the RemoteInputView and animates its appearance */ public void focusAnimated() { - if (!mIsFocusAnimationFlagActive && getVisibility() != VISIBLE - && mRevealParams != null) { - android.animation.Animator animator = mRevealParams.createCircularRevealAnimator(this); - animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); - animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); - animator.start(); - } else if (mIsFocusAnimationFlagActive && getVisibility() != VISIBLE) { + if (getVisibility() != VISIBLE) { mIsAnimatingAppearance = true; setAlpha(0f); Animator focusAnimator = getFocusAnimator(getActionsContainerLayout()); @@ -680,37 +664,19 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } private void reset() { - if (mIsFocusAnimationFlagActive) { - mProgressBar.setVisibility(INVISIBLE); - mResetting = true; - mSending = false; - mController.removeSpinning(mEntry.getKey(), mToken); - onDefocus(true /* animate */, false /* logClose */, () -> { - mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); - mEditText.getText().clear(); - mEditText.setEnabled(isAggregatedVisible()); - mSendButton.setVisibility(VISIBLE); - updateSendButton(); - setAttachment(null); - mResetting = false; - }); - return; - } - + mProgressBar.setVisibility(INVISIBLE); mResetting = true; mSending = false; - mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); - - mEditText.getText().clear(); - mEditText.setEnabled(isAggregatedVisible()); - mSendButton.setVisibility(VISIBLE); - mProgressBar.setVisibility(INVISIBLE); mController.removeSpinning(mEntry.getKey(), mToken); - updateSendButton(); - onDefocus(false /* animate */, false /* logClose */, null /* doAfterDefocus */); - setAttachment(null); - - mResetting = false; + onDefocus(true /* animate */, false /* logClose */, () -> { + mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); + mEditText.getText().clear(); + mEditText.setEnabled(isAggregatedVisible()); + mSendButton.setVisibility(VISIBLE); + updateSendButton(); + setAttachment(null); + mResetting = false; + }); } @Override @@ -854,7 +820,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); - if (mIsFocusAnimationFlagActive) setPivotY(getMeasuredHeight()); + setPivotY(getMeasuredHeight()); if (mContentBackgroundBounds != null) { mContentBackground.setBounds(mContentBackgroundBounds); } @@ -1015,9 +981,9 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene private RemoteInputView mRemoteInputView; boolean mShowImeOnInputConnection; - private LightBarController mLightBarController; + private final LightBarController mLightBarController; private InputMethodManager mInputMethodManager; - private ArraySet<String> mSupportedMimes = new ArraySet<>(); + private final ArraySet<String> mSupportedMimes = new ArraySet<>(); UserHandle mUser; public RemoteEditText(Context context, AttributeSet attrs) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt index 6c0d43394074..bfee9adf1f15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt @@ -32,7 +32,6 @@ import android.view.View import com.android.internal.logging.UiEventLogger import com.android.systemui.res.R import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags.NOTIFICATION_INLINE_REPLY_ANIMATION import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.RemoteInputController import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -64,8 +63,6 @@ interface RemoteInputViewController { var revealParams: RevealParams? - val isFocusAnimationFlagActive: Boolean - /** * Sets the smart reply that should be inserted in the remote input, or `null` if the user is * not editing a smart reply. @@ -155,9 +152,6 @@ class RemoteInputViewControllerImpl @Inject constructor( override val isActive: Boolean get() = view.isActive - override val isFocusAnimationFlagActive: Boolean - get() = mFlags.isEnabled(NOTIFICATION_INLINE_REPLY_ANIMATION) - override fun bind() { if (isBound) return isBound = true @@ -168,7 +162,6 @@ class RemoteInputViewControllerImpl @Inject constructor( view.setSupportedMimeTypes(it.allowedDataTypes) } view.setRevealParameters(revealParams) - view.setIsFocusAnimationFlagActive(isFocusAnimationFlagActive) view.addOnEditTextFocusChangedListener(onFocusChangeListener) view.addOnSendRemoteInputListener(onSendRemoteInputListener) diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt index 71df8e53b5e2..1bceee9b2d34 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt @@ -36,3 +36,7 @@ sealed interface MediaDeviceSession { /** Current media state is unknown yet. */ data object Unknown : MediaDeviceSession } + +/** Returns true when the audio is playing for the [MediaDeviceSession]. */ +fun MediaDeviceSession.isPlaying(): Boolean = + this is MediaDeviceSession.Active && playbackState?.isActive == true diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt index 37661b53c98a..d49cb1ea6958 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt @@ -24,6 +24,7 @@ import com.android.systemui.res.R import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel import javax.inject.Inject @@ -110,9 +111,6 @@ constructor( null, ) - private fun MediaDeviceSession.isPlaying(): Boolean = - this is MediaDeviceSession.Active && playbackState?.isActive == true - fun onBarClick(expandable: Expandable) { actionsInteractor.onBarClick(mediaDeviceSession.value, expandable) volumePanelViewModel.dismissPanel() diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index faf743475579..532e517bf45d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -21,6 +21,7 @@ import android.media.AudioManager import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.AudioStreamModel +import com.android.settingslib.volume.shared.model.RingerMode import com.android.systemui.common.shared.model.Icon import com.android.systemui.res.R import com.android.systemui.volume.panel.component.volume.domain.interactor.VolumeSliderInteractor @@ -54,14 +55,6 @@ constructor( AudioStream(AudioManager.STREAM_NOTIFICATION) to R.drawable.ic_volume_ringer, AudioStream(AudioManager.STREAM_ALARM) to R.drawable.ic_volume_alarm, ) - private val mutedIconsByStream = - mapOf( - AudioStream(AudioManager.STREAM_MUSIC) to R.drawable.ic_volume_off, - AudioStream(AudioManager.STREAM_VOICE_CALL) to R.drawable.ic_volume_off, - AudioStream(AudioManager.STREAM_RING) to R.drawable.ic_volume_off, - AudioStream(AudioManager.STREAM_NOTIFICATION) to R.drawable.ic_volume_off, - AudioStream(AudioManager.STREAM_ALARM) to R.drawable.ic_volume_off, - ) private val labelsByStream = mapOf( AudioStream(AudioManager.STREAM_MUSIC) to R.string.stream_music, @@ -74,6 +67,8 @@ constructor( mapOf( AudioStream(AudioManager.STREAM_NOTIFICATION) to R.string.stream_notification_unavailable, + AudioStream(AudioManager.STREAM_ALARM) to R.string.stream_alarm_unavailable, + AudioStream(AudioManager.STREAM_MUSIC) to R.string.stream_media_unavailable, ) private var value = 0f @@ -81,8 +76,9 @@ constructor( combine( audioVolumeInteractor.getAudioStream(audioStream), audioVolumeInteractor.canChangeVolume(audioStream), - ) { model, isEnabled -> - model.toState(value, isEnabled) + audioVolumeInteractor.ringerMode, + ) { model, isEnabled, ringerMode -> + model.toState(value, isEnabled, ringerMode) } .stateIn(coroutineScope, SharingStarted.Eagerly, EmptyState) @@ -100,7 +96,11 @@ constructor( } } - private fun AudioStreamModel.toState(value: Float, isEnabled: Boolean): State { + private fun AudioStreamModel.toState( + value: Float, + isEnabled: Boolean, + ringerMode: RingerMode, + ): State { return State( value = volumeSliderInteractor.processVolumeToValue( @@ -110,7 +110,7 @@ constructor( isMuted, ), valueRange = volumeSliderInteractor.displayValueRange, - icon = getIcon(this), + icon = getIcon(ringerMode), label = labelsByStream[audioStream]?.let(context::getString) ?: error("No label for the stream: $audioStream"), disabledMessage = disabledTextByStream[audioStream]?.let(context::getString), @@ -119,14 +119,31 @@ constructor( ) } - private fun getIcon(model: AudioStreamModel): Icon { - val isMutedOrNoVolume = model.isMuted || model.volume == model.minVolume + private fun AudioStreamModel.getIcon(ringerMode: RingerMode): Icon { + val isMutedOrNoVolume = isMuted || volume == minVolume val iconRes = if (isMutedOrNoVolume) { - mutedIconsByStream + when (audioStream.value) { + AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_off + AudioManager.STREAM_VOICE_CALL -> R.drawable.ic_volume_off + AudioManager.STREAM_RING -> + if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) { + R.drawable.ic_volume_ringer_vibrate + } else { + R.drawable.ic_volume_off + } + AudioManager.STREAM_NOTIFICATION -> + if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) { + R.drawable.ic_volume_ringer_vibrate + } else { + R.drawable.ic_volume_off + } + AudioManager.STREAM_ALARM -> R.drawable.ic_volume_off + else -> null + } } else { - iconsByStream - }[audioStream] + iconsByStream[audioStream] + } ?: error("No icon for the stream: $audioStream") return Icon.Resource(iconRes, null) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt index 2824323775d3..aaee24b9357f 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt @@ -18,6 +18,8 @@ package com.android.systemui.volume.panel.component.volume.ui.viewmodel import android.media.AudioManager import com.android.settingslib.volume.shared.model.AudioStream +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.AudioStreamSliderViewModel import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.CastVolumeSliderViewModel @@ -28,12 +30,17 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.launch /** * Controls the behaviour of the whole audio @@ -46,6 +53,7 @@ class AudioVolumeComponentViewModel constructor( @VolumePanelScope private val scope: CoroutineScope, castVolumeInteractor: CastVolumeInteractor, + mediaOutputInteractor: MediaOutputInteractor, private val streamSliderViewModelFactory: AudioStreamSliderViewModel.Factory, private val castVolumeSliderViewModelFactory: CastVolumeSliderViewModel.Factory, ) { @@ -90,4 +98,17 @@ constructor( remoteSessionsViewModels + streamViewModels } .stateIn(scope, SharingStarted.Eagerly, emptyList()) + + private val mutableIsExpanded = MutableSharedFlow<Boolean>() + + val isExpanded: StateFlow<Boolean> = + merge( + mutableIsExpanded.onStart { emit(false) }, + mediaOutputInteractor.mediaDeviceSession.map { !it.isPlaying() }, + ) + .stateIn(scope, SharingStarted.Eagerly, false) + + fun onExpandedChanged(isExpanded: Boolean) { + scope.launch { mutableIsExpanded.emit(isExpanded) } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java index dd428f562bd1..ccdcee5e0318 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java @@ -32,7 +32,6 @@ import androidx.test.filters.SmallTest; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.animation.AnimatorTestRule; import com.android.systemui.flags.FakeFeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; @@ -57,7 +56,6 @@ public class ExpandHelperTest extends SysuiTestCase { @Before public void setUp() throws Exception { - mFeatureFlags.setDefault(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION); mDependency.injectMockDependency(KeyguardUpdateMonitor.class); mDependency.injectMockDependency(NotificationMediaManager.class); allowTestableLooperAsMainThread(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt index e7963031411d..701b7039a1ed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt @@ -401,7 +401,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to Pair("Enter PIN", "PIN is required after lockdown"), LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to - Pair("Enter PIN", "Update will install when device not in use"), + Pair("Enter PIN", "PIN required for additional security"), LockPatternUtils.StrongAuthTracker .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to Pair( @@ -439,7 +439,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to Pair("Enter PIN", "PIN is required after lockdown"), LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to - Pair("Enter PIN", "Update will install when device not in use"), + Pair("Enter PIN", "PIN required for additional security"), LockPatternUtils.StrongAuthTracker .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to Pair( @@ -481,7 +481,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to Pair("Enter PIN", "PIN is required after lockdown"), LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to - Pair("Enter PIN", "Update will install when device not in use"), + Pair("Enter PIN", "PIN required for additional security"), LockPatternUtils.StrongAuthTracker .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to Pair( diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt index e1e9fcb2d059..dac88a340cb1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt @@ -532,6 +532,18 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { assertThat(faceAuthRepository.runningAuthRequest.value).isNull() } + @Test + fun lockedOut_providesSameValueFromRepository() = + testScope.runTest { + assertThat(underTest.lockedOut).isSameInstanceAs(faceAuthRepository.isLockedOut) + } + + @Test + fun authenticated_providesSameValueFromRepository() = + testScope.runTest { + assertThat(underTest.authenticated).isSameInstanceAs(faceAuthRepository.isAuthenticated) + } + companion object { private const val primaryUserId = 1 private val primaryUser = UserInfo(primaryUserId, "test user", UserInfo.FLAG_PRIMARY) diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt new file mode 100644 index 000000000000..decbdaf0feee --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 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.deviceentry.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class DeviceEntryFingerprintAuthInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest = kosmos.deviceEntryFingerprintAuthInteractor + private val fingerprintAuthRepository = kosmos.deviceEntryFingerprintAuthRepository + private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository + private val biometricSettingsRepository = kosmos.biometricSettingsRepository + + @Test + fun isFingerprintAuthCurrentlyAllowed_allowedOnlyWhenItIsNotLockedOutAndAllowedBySettings() = + testScope.runTest { + val currentlyAllowed by collectLastValue(underTest.isFingerprintAuthCurrentlyAllowed) + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + fingerprintAuthRepository.setLockedOut(true) + + assertThat(currentlyAllowed).isFalse() + + fingerprintAuthRepository.setLockedOut(false) + assertThat(currentlyAllowed).isTrue() + + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(false) + assertThat(currentlyAllowed).isFalse() + } + + @Test + fun isSensorUnderDisplay_trueForUdfpsSensorTypes() = + testScope.runTest { + val isSensorUnderDisplay by collectLastValue(underTest.isSensorUnderDisplay) + + fingerprintPropertyRepository.supportsUdfps() + assertThat(isSensorUnderDisplay).isTrue() + + fingerprintPropertyRepository.supportsRearFps() + assertThat(isSensorUnderDisplay).isFalse() + + fingerprintPropertyRepository.supportsSideFps() + assertThat(isSensorUnderDisplay).isFalse() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt index bcec6109faf6..b80dcd41d53e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt @@ -62,6 +62,7 @@ import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth import kotlin.math.min +import kotlin.test.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow @@ -77,7 +78,6 @@ import org.mockito.ArgumentMatchers import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations -import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -134,7 +134,12 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { private lateinit var lockscreenToPrimaryBouncerTransitionViewModel: LockscreenToPrimaryBouncerTransitionViewModel @Mock - private lateinit var transitionInteractor: KeyguardTransitionInteractor + private lateinit var lockscreenToGlanceableHubTransitionViewModel: + LockscreenToGlanceableHubTransitionViewModel + @Mock + private lateinit var glanceableHubToLockscreenTransitionViewModel: + GlanceableHubToLockscreenTransitionViewModel + @Mock private lateinit var transitionInteractor: KeyguardTransitionInteractor private lateinit var underTest: KeyguardQuickAffordancesCombinedViewModel @@ -271,6 +276,10 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { whenever(lockscreenToOccludedTransitionViewModel.shortcutsAlpha).thenReturn(emptyFlow()) whenever(lockscreenToPrimaryBouncerTransitionViewModel.shortcutsAlpha) .thenReturn(emptyFlow()) + whenever(lockscreenToGlanceableHubTransitionViewModel.shortcutsAlpha) + .thenReturn(emptyFlow()) + whenever(glanceableHubToLockscreenTransitionViewModel.shortcutsAlpha) + .thenReturn(emptyFlow()) whenever(shadeInteractor.anyExpansion).thenReturn(intendedShadeAlphaMutableStateFlow) whenever(transitionInteractor.finishedKeyguardState) .thenReturn(intendedFinishedKeyguardStateFlow) @@ -307,6 +316,8 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { offToLockscreenTransitionViewModel = offToLockscreenTransitionViewModel, primaryBouncerToLockscreenTransitionViewModel = primaryBouncerToLockscreenTransitionViewModel, + glanceableHubToLockscreenTransitionViewModel = + glanceableHubToLockscreenTransitionViewModel, lockscreenToAodTransitionViewModel = lockscreenToAodTransitionViewModel, lockscreenToDozingTransitionViewModel = lockscreenToDozingTransitionViewModel, lockscreenToDreamingHostedTransitionViewModel = @@ -316,6 +327,8 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel, lockscreenToPrimaryBouncerTransitionViewModel = lockscreenToPrimaryBouncerTransitionViewModel, + lockscreenToGlanceableHubTransitionViewModel = + lockscreenToGlanceableHubTransitionViewModel, transitionInteractor = transitionInteractor, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java index 65ede89a1514..0101741a9242 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java @@ -51,6 +51,7 @@ import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.res.R; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; import com.android.systemui.util.animation.DisappearParameters; @@ -100,6 +101,8 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { Configuration mConfiguration; @Mock Runnable mHorizontalLayoutListener; + @Mock + VibratorHelper mVibratorHelper; private QSPanelControllerBase<QSPanel> mController; @@ -110,7 +113,8 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger, DumpManager dumpManager) { super(view, host, qsCustomizerController, true, mediaHost, metricsLogger, uiEventLogger, - qsLogger, dumpManager, new ResourcesSplitShadeStateController()); + qsLogger, dumpManager, new ResourcesSplitShadeStateController(), + mVibratorHelper); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt index 85d7d9865c7c..916e8ddb6e8a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt @@ -19,6 +19,7 @@ import com.android.systemui.qs.logging.QSLogger import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags import com.android.systemui.settings.brightness.BrightnessController import com.android.systemui.settings.brightness.BrightnessSliderController +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController import com.android.systemui.tuner.TunerService @@ -61,6 +62,7 @@ class QSPanelControllerTest : SysuiTestCase() { @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager @Mock private lateinit var configuration: Configuration @Mock private lateinit var pagedTileLayout: PagedTileLayout + @Mock private lateinit var vibratorHelper: VibratorHelper private val sceneContainerFlags = FakeSceneContainerFlags() @@ -101,6 +103,7 @@ class QSPanelControllerTest : SysuiTestCase() { statusBarKeyguardViewManager, ResourcesSplitShadeStateController(), sceneContainerFlags, + vibratorHelper, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt index 2c1430844d12..71a9a8b3318f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt @@ -30,6 +30,7 @@ import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.customize.QSCustomizerController import com.android.systemui.qs.logging.QSLogger +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController import com.android.systemui.util.leak.RotationUtils import org.junit.After @@ -59,6 +60,7 @@ class QuickQSPanelControllerTest : SysuiTestCase() { @Mock private lateinit var tile: QSTile @Mock private lateinit var tileLayout: TileLayout @Captor private lateinit var captor: ArgumentCaptor<QSPanel.OnConfigurationChangedListener> + @Mock private lateinit var vibratorHelper: VibratorHelper private val uiEventLogger = UiEventLoggerFake() private val dumpManager = DumpManager() @@ -89,7 +91,8 @@ class QuickQSPanelControllerTest : SysuiTestCase() { metricsLogger, uiEventLogger, qsLogger, - dumpManager + dumpManager, + vibratorHelper, ) controller.init() @@ -157,7 +160,8 @@ class QuickQSPanelControllerTest : SysuiTestCase() { metricsLogger: MetricsLogger, uiEventLogger: UiEventLoggerFake, qsLogger: QSLogger, - dumpManager: DumpManager + dumpManager: DumpManager, + vibratorHelper: VibratorHelper, ) : QuickQSPanelController( view, @@ -170,7 +174,8 @@ class QuickQSPanelControllerTest : SysuiTestCase() { uiEventLogger, qsLogger, dumpManager, - ResourcesSplitShadeStateController() + ResourcesSplitShadeStateController(), + vibratorHelper, ) { private var rotation = RotationUtils.ROTATION_NONE diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index 42a6924b95e1..b114e13bb25c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -60,7 +60,6 @@ import com.android.internal.widget.CachingIconView; import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestableContext; import com.android.systemui.flags.FakeFeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.AboveShelfChangedListener; @@ -104,7 +103,6 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { TestableLooper.get(this), mFeatureFlags); mNotificationTestHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL); - mFeatureFlags.setDefault(Flags.SENSITIVE_REVEAL_ANIM); } @Test @@ -186,14 +184,6 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - public void testSetSensitiveOnNotifRowNotifiesOfHeightChange_withOtherFlagValue() - throws Exception { - FakeFeatureFlags flags = mFeatureFlags; - flags.set(Flags.SENSITIVE_REVEAL_ANIM, !flags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM)); - testSetSensitiveOnNotifRowNotifiesOfHeightChange(); - } - - @Test public void testSetSensitiveOnNotifRowNotifiesOfHeightChange() throws Exception { // GIVEN a sensitive notification row that's currently redacted ExpandableNotificationRow row = mNotificationTestHelper.createRow(); @@ -210,19 +200,10 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { // WHEN the row is set to no longer be sensitive row.setSensitive(false, true); - boolean expectAnimation = mFeatureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM); // VERIFY that the height change listener is invoked assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPrivateLayout()); assertThat(row.getIntrinsicHeight()).isGreaterThan(0); - verify(listener).onHeightChanged(eq(row), eq(expectAnimation)); - } - - @Test - public void testSetSensitiveOnGroupRowNotifiesOfHeightChange_withOtherFlagValue() - throws Exception { - FakeFeatureFlags flags = mFeatureFlags; - flags.set(Flags.SENSITIVE_REVEAL_ANIM, !flags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM)); - testSetSensitiveOnGroupRowNotifiesOfHeightChange(); + verify(listener).onHeightChanged(eq(row), eq(true)); } @Test @@ -242,19 +223,10 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { // WHEN the row is set to no longer be sensitive group.setSensitive(false, true); - boolean expectAnimation = mFeatureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM); // VERIFY that the height change listener is invoked assertThat(group.getShowingLayout()).isSameInstanceAs(group.getPrivateLayout()); assertThat(group.getIntrinsicHeight()).isGreaterThan(0); - verify(listener).onHeightChanged(eq(group), eq(expectAnimation)); - } - - @Test - public void testSetSensitiveOnPublicRowDoesNotNotifyOfHeightChange_withOtherFlagValue() - throws Exception { - FakeFeatureFlags flags = mFeatureFlags; - flags.set(Flags.SENSITIVE_REVEAL_ANIM, !flags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM)); - testSetSensitiveOnPublicRowDoesNotNotifyOfHeightChange(); + verify(listener).onHeightChanged(eq(group), eq(true)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt index b938029cc6b8..9a7b8ec2ec07 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt @@ -10,7 +10,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.res.R import com.android.systemui.shade.transition.LargeScreenShadeInterpolator import com.android.systemui.statusbar.NotificationShelf @@ -23,7 +22,6 @@ import com.android.systemui.util.mockito.mock import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue -import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -38,7 +36,6 @@ import org.mockito.Mockito.`when` as whenever @RunWithLooper open class NotificationShelfTest : SysuiTestCase() { - open val useSensitiveReveal: Boolean = false private val flags = FakeFeatureFlags() @Mock private lateinit var largeScreenShadeInterpolator: LargeScreenShadeInterpolator @@ -53,7 +50,6 @@ open class NotificationShelfTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) mDependency.injectTestDependency(FeatureFlags::class.java, flags) - flags.set(Flags.SENSITIVE_REVEAL_ANIM, useSensitiveReveal) val root = FrameLayout(context) shelf = LayoutInflater.from(root.context) @@ -335,7 +331,6 @@ open class NotificationShelfTest : SysuiTestCase() { @Test fun updateState_withNullLastVisibleBackgroundChild_hideShelf() { // GIVEN - assumeTrue(useSensitiveReveal) whenever(ambientState.stackY).thenReturn(100f) whenever(ambientState.stackHeight).thenReturn(100f) val paddingBetweenElements = @@ -362,7 +357,6 @@ open class NotificationShelfTest : SysuiTestCase() { @Test fun updateState_withNullFirstViewInShelf_hideShelf() { // GIVEN - assumeTrue(useSensitiveReveal) whenever(ambientState.stackY).thenReturn(100f) whenever(ambientState.stackHeight).thenReturn(100f) val paddingBetweenElements = @@ -389,7 +383,6 @@ open class NotificationShelfTest : SysuiTestCase() { @Test fun updateState_withCollapsedShade_hideShelf() { // GIVEN - assumeTrue(useSensitiveReveal) whenever(ambientState.stackY).thenReturn(100f) whenever(ambientState.stackHeight).thenReturn(100f) val paddingBetweenElements = @@ -416,7 +409,6 @@ open class NotificationShelfTest : SysuiTestCase() { @Test fun updateState_withHiddenSectionBeforeShelf_hideShelf() { // GIVEN - assumeTrue(useSensitiveReveal) whenever(ambientState.stackY).thenReturn(100f) whenever(ambientState.stackHeight).thenReturn(100f) val paddingBetweenElements = @@ -476,10 +468,3 @@ open class NotificationShelfTest : SysuiTestCase() { assertEquals(expectedAlpha, shelf.viewState.alpha) } } - -@SmallTest -@RunWith(AndroidTestingRunner::class) -@RunWithLooper -class NotificationShelfWithSensitiveRevealTest : NotificationShelfTest() { - override val useSensitiveReveal: Boolean = true -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 220305cc6bda..13df09134b23 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -165,8 +165,6 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { // TODO: Ideally we wouldn't need to set these unless a test actually reads them, // and then we would test both configurations, but currently they are all read // in the constructor. - mFeatureFlags.setDefault(Flags.SENSITIVE_REVEAL_ANIM); - mFeatureFlags.setDefault(Flags.ANIMATED_NOTIFICATION_SHADE_INSETS); mSetFlagsRule.enableFlags(FLAG_NEW_AOD_TRANSITION); mFeatureFlags.setDefault(Flags.UNCLEARED_TRANSIENT_HUN_FIX); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index 13167b2d281f..c25978271b17 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -71,7 +71,6 @@ import com.android.systemui.res.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.AnimatorTestRule; import com.android.systemui.flags.FakeFeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -455,7 +454,6 @@ public class RemoteInputViewTest extends SysuiTestCase { private RemoteInputViewController bindController( RemoteInputView view, NotificationEntry entry) { - mFeatureFlags.set(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION, true); RemoteInputViewControllerImpl viewController = new RemoteInputViewControllerImpl( view, entry, diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index 974e396fe280..5206db4aa13a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -95,6 +95,7 @@ import junit.framework.Assert; 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; @@ -292,6 +293,7 @@ public class VolumeDialogImplTest extends SysuiTestCase { assertEquals(VolumeDialogImpl.PROGRESS_HAPTICS_DISABLED, type); } + @Ignore("Causing breakages so ignoring to resolve, b/329099861") @Test @EnableFlags(FLAG_HAPTIC_VOLUME_SLIDER) public void testVolumeChange_withSliderHaptics_deliversOnProgressChangedHapticsEagerly() { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSystemUiModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSystemUiModule.kt index 6dd8d07b356b..0660d0095c7d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSystemUiModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSystemUiModule.kt @@ -18,6 +18,7 @@ package com.android.systemui import com.android.systemui.classifier.FakeClassifierModule import com.android.systemui.data.FakeSystemUiDataLayerModule import com.android.systemui.flags.FakeFeatureFlagsClassicModule +import com.android.systemui.flags.FakeSystemPropertiesHelperModule import com.android.systemui.log.FakeUiEventLoggerModule import com.android.systemui.settings.FakeSettingsModule import com.android.systemui.statusbar.policy.FakeConfigurationControllerModule @@ -33,6 +34,7 @@ import dagger.Module FakeConfigurationControllerModule::class, FakeExecutorModule::class, FakeFeatureFlagsClassicModule::class, + FakeSystemPropertiesHelperModule::class, FakeSettingsModule::class, FakeSplitShadeStateControllerModule::class, FakeSystemClockModule::class, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt index 69b769eb2321..bc0bf9dd069f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt @@ -27,6 +27,9 @@ import com.android.systemui.coroutines.collectValues import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfigModule +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.SystemUIDeviceEntryFaceAuthInteractor import com.android.systemui.scene.SceneContainerFrameworkModule import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.scene.shared.model.SceneDataSource @@ -56,6 +59,7 @@ import kotlinx.coroutines.test.runTest CoroutineTestScopeModule::class, FakeSystemUiModule::class, SceneContainerFrameworkModule::class, + FaceWakeUpTriggersConfigModule::class, ] ) interface SysUITestModule { @@ -69,6 +73,11 @@ interface SysUITestModule { @Binds @SysUISingleton fun bindsShadeInteractor(sii: ShadeInteractorImpl): ShadeInteractor @Binds fun bindSceneDataSource(delegator: SceneDataSourceDelegator): SceneDataSource + @Binds + fun provideFaceAuthInteractor( + sysUIFaceAuthInteractor: SystemUIDeviceEntryFaceAuthInteractor + ): DeviceEntryFaceAuthInteractor + companion object { @Provides fun provideSysuiTestableContext(test: SysuiTestCase): SysuiTestableContext = test.context diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt index 62a1aa93f35a..3d84291292c9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt @@ -17,6 +17,7 @@ package com.android.systemui import android.app.ActivityManager import android.app.admin.DevicePolicyManager +import android.app.trust.TrustManager import android.os.UserManager import android.service.notification.NotificationListenerService import android.util.DisplayMetrics @@ -27,6 +28,7 @@ import com.android.keyguard.KeyguardSecurityModel import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardViewController import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.demomode.DemoModeController import com.android.systemui.dump.DumpManager @@ -36,6 +38,7 @@ import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.log.LogBuffer import com.android.systemui.log.dagger.BiometricLog import com.android.systemui.log.dagger.BroadcastDispatcherLog +import com.android.systemui.log.dagger.FaceAuthLog import com.android.systemui.log.dagger.SceneFrameworkLog import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.model.SysUiState @@ -65,10 +68,12 @@ import com.android.systemui.statusbar.phone.ScrimController import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.HeadsUpManager +import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.ZenModeController import com.android.systemui.statusbar.window.StatusBarWindowController import com.android.systemui.unfold.UnfoldTransitionProgressProvider import com.android.systemui.util.mockito.mock +import com.android.systemui.util.settings.GlobalSettings import com.android.wm.shell.bubbles.Bubbles import dagger.Binds import dagger.Module @@ -123,6 +128,10 @@ data class TestMocksModule( @get:Provides val deviceEntryIconTransitions: Set<DeviceEntryIconTransition> = emptySet(), @get:Provides val communalInteractor: CommunalInteractor = mock(), @get:Provides val sceneLogger: SceneLogger = mock(), + @get:Provides val trustManager: TrustManager = mock(), + @get:Provides val primaryBouncerInteractor: PrimaryBouncerInteractor = mock(), + @get:Provides val keyguardStateController: KeyguardStateController = mock(), + @get:Provides val globalSettings: GlobalSettings = mock(), // log buffers @get:[Provides BroadcastDispatcherLog] @@ -131,6 +140,8 @@ data class TestMocksModule( val sceneLogBuffer: LogBuffer = mock(), @get:[Provides BiometricLog] val biometricLogger: LogBuffer = mock(), + @get:[Provides FaceAuthLog] + val faceAuthLogger: LogBuffer = mock(), @get:Provides val lsShadeTransitionLogger: LSShadeTransitionLogger = mock(), // framework mocks diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt index 68ef55573dc8..8a951365ecea 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt @@ -19,10 +19,15 @@ package com.android.systemui.biometrics.data.repository import android.graphics.Point import com.android.systemui.biometrics.shared.model.LockoutMode +import com.android.systemui.dagger.SysUISingleton +import dagger.Binds +import dagger.Module +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class FakeFacePropertyRepository : FacePropertyRepository { +@SysUISingleton +class FakeFacePropertyRepository @Inject constructor() : FacePropertyRepository { private val faceSensorInfo = MutableStateFlow<FaceSensorInfo?>(null) override val sensorInfo: StateFlow<FaceSensorInfo?> get() = faceSensorInfo @@ -56,3 +61,8 @@ class FakeFacePropertyRepository : FacePropertyRepository { currentCameraInfo.value = value } } + +@Module +interface FakeFacePropertyRepositoryModule { + @Binds fun bindFake(fake: FakeFacePropertyRepository): FacePropertyRepository +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt index bd30fb4cac2b..60d61ac4dcef 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt @@ -68,6 +68,16 @@ class FakeFingerprintPropertyRepository @Inject constructor() : FingerprintPrope ) } + /** setProperties as if the device supports POWER_BUTTON fingerprint sensor. */ + fun supportsSideFps() { + setProperties( + sensorId = 0, + strength = SensorStrength.STRONG, + sensorType = FingerprintSensorType.POWER_BUTTON, + sensorLocations = emptyMap(), + ) + } + /** setProperties as if the device supports the rear fingerprint sensor. */ fun supportsRearFps() { setProperties( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt index 1c8190eee773..fbb8ea22d615 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt @@ -16,6 +16,7 @@ package com.android.systemui.deviceentry.data import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepositoryModule +import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepositoryModule import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepositoryModule import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepositoryModule import com.android.systemui.display.data.repository.FakeDisplayRepositoryModule @@ -35,6 +36,7 @@ import dagger.Module FakeDisplayRepositoryModule::class, FakeDisplayStateRepositoryModule::class, FakeFingerprintPropertyRepositoryModule::class, + FakeFacePropertyRepositoryModule::class, FakeTrustRepositoryModule::class, ] ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorKosmos.kt index 66c6f86c452d..ebed922c423e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.deviceentry.domain.interactor +import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository import com.android.systemui.kosmos.Kosmos import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -25,6 +26,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.deviceEntryFingerprintAuthInteractor by Kosmos.Fixture { DeviceEntryFingerprintAuthInteractor( + biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor, repository = deviceEntryFingerprintAuthRepository, + fingerprintPropertyRepository = fingerprintPropertyRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt index 0d1a31f9605e..e73e2950bbb9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt @@ -18,8 +18,8 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.deviceentry.data.repository.deviceEntryRepository -import com.android.systemui.keyguard.data.repository.deviceEntryFaceAuthRepository -import com.android.systemui.keyguard.data.repository.trustRepository +import com.android.systemui.flags.fakeSystemPropertiesHelper +import com.android.systemui.keyguard.domain.interactor.trustInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -34,9 +34,12 @@ val Kosmos.deviceEntryInteractor by repository = deviceEntryRepository, authenticationInteractor = authenticationInteractor, sceneInteractor = sceneInteractor, - deviceEntryFaceAuthRepository = deviceEntryFaceAuthRepository, - trustRepository = trustRepository, + faceAuthInteractor = deviceEntryFaceAuthInteractor, + trustInteractor = trustInteractor, flags = sceneContainerFlags, deviceUnlockedInteractor = deviceUnlockedInteractor, + fingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, + biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor, + systemPropertiesHelper = fakeSystemPropertiesHelper, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeSystemPropertiesHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeSystemPropertiesHelper.kt new file mode 100644 index 000000000000..2f30d3431a1e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeSystemPropertiesHelper.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 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.flags + +import com.android.systemui.dagger.SysUISingleton +import dagger.Binds +import dagger.Module +import javax.inject.Inject + +@SysUISingleton +class FakeSystemPropertiesHelper @Inject constructor() : SystemPropertiesHelper() { + private val fakeProperties = mutableMapOf<String, Any>() + + override fun get(name: String): String { + return fakeProperties[name] as String + } + + override fun get(name: String, def: String?): String { + return checkNotNull(fakeProperties[name] as String? ?: def) + } + + override fun getBoolean(name: String, default: Boolean): Boolean { + return fakeProperties[name] as Boolean? ?: default + } + + override fun setBoolean(name: String, value: Boolean) { + fakeProperties[name] = value + } + + override fun set(name: String, value: String) { + fakeProperties[name] = value + } + + override fun set(name: String, value: Int) { + fakeProperties[name] = value + } + + override fun erase(name: String) { + fakeProperties.remove(name) + } +} + +@Module +interface FakeSystemPropertiesHelperModule { + @Binds fun bindFake(fake: FakeSystemPropertiesHelper): SystemPropertiesHelper +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt index 365d97f3ac15..d6f2f77ca67a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt @@ -62,6 +62,7 @@ val Kosmos.featureFlagsClassicRelease by } val Kosmos.systemPropertiesHelper by Kosmos.Fixture { SystemPropertiesHelper() } +val Kosmos.fakeSystemPropertiesHelper by Kosmos.Fixture { FakeSystemPropertiesHelper() } var Kosmos.serverFlagReader: ServerFlagReader by Kosmos.Fixture { serverFlagReaderFake } val Kosmos.serverFlagReaderFake by Kosmos.Fixture { ServerFlagReaderFake() } var Kosmos.restarter: Restarter by Kosmos.Fixture { mock() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/TrustInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/TrustInteractorKosmos.kt new file mode 100644 index 000000000000..0ebf1642478e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/TrustInteractorKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 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.keyguard.domain.interactor + +import com.android.systemui.keyguard.data.repository.trustRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.trustInteractor by Fixture { TrustInteractor(repository = trustRepository) } diff --git a/packages/Tethering/OWNERS b/packages/Tethering/OWNERS deleted file mode 100644 index aa87958f1d53..000000000000 --- a/packages/Tethering/OWNERS +++ /dev/null @@ -1 +0,0 @@ -include /services/core/java/com/android/server/net/OWNERS diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java index 10e6ed4542c7..323917564bf2 100644 --- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java +++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java @@ -59,6 +59,8 @@ import android.hardware.camera2.extension.Request; import android.hardware.camera2.extension.SizeList; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.impl.PhysicalCaptureResultInfo; +import android.hardware.camera2.params.ColorSpaceProfiles; +import android.hardware.camera2.params.DynamicRangeProfiles; import android.hardware.camera2.utils.SurfaceUtils; import android.media.Image; import android.media.ImageReader; @@ -1228,7 +1230,6 @@ public class CameraExtensionsProxyService extends Service { return null; } - } private class CaptureCallbackStub implements SessionProcessorImpl.CaptureCallback { @@ -1585,11 +1586,13 @@ public class CameraExtensionsProxyService extends Service { Camera2SessionConfigImpl sessionConfig; if (LATENCY_IMPROVEMENTS_SUPPORTED) { + int outputsColorSpace = getColorSpaceFromOutputSurfaces(previewSurface, + imageCaptureSurface, postviewSurface); OutputSurfaceConfigurationImplStub outputSurfaceConfigs = new OutputSurfaceConfigurationImplStub(mOutputPreviewSurfaceImpl, // Image Analysis Output is currently only supported in CameraX mOutputImageCaptureSurfaceImpl, null /*imageAnalysisSurfaceConfig*/, - mOutputPostviewSurfaceImpl); + mOutputPostviewSurfaceImpl, outputsColorSpace); sessionConfig = mSessionProcessor.initSession(cameraId, getCharacteristicsMap(charsMapNative), @@ -1616,6 +1619,11 @@ public class CameraExtensionsProxyService extends Service { } ret.outputConfigs.add(entry); } + if (Flags.extension10Bit() && EFV_SUPPORTED) { + ret.colorSpace = sessionConfig.getColorSpace(); + } else { + ret.colorSpace = ColorSpaceProfiles.UNSPECIFIED; + } ret.sessionTemplateId = sessionConfig.getSessionTemplateId(); ret.sessionType = -1; if (LATENCY_IMPROVEMENTS_SUPPORTED) { @@ -1720,6 +1728,24 @@ public class CameraExtensionsProxyService extends Service { public void binderDied() { mSessionProcessor.deInitSession(); } + + // Get the color space of the output configurations. All of the OutputSurfaces + // can be assumed to have the same color space so return the color space + // of any non-null OutputSurface + private int getColorSpaceFromOutputSurfaces(OutputSurface previewSurface, + OutputSurface imageCaptureSurface, OutputSurface postviewSurface) { + int colorSpace = ColorSpaceProfiles.UNSPECIFIED; + + if (previewSurface.surface != null) { + colorSpace = previewSurface.colorSpace; + } else if (imageCaptureSurface.surface != null) { + colorSpace = imageCaptureSurface.colorSpace; + } else if (postviewSurface.surface != null) { + colorSpace = postviewSurface.colorSpace; + } + + return colorSpace; + } } private class OutputSurfaceConfigurationImplStub implements OutputSurfaceConfigurationImpl { @@ -1727,6 +1753,17 @@ public class CameraExtensionsProxyService extends Service { private OutputSurfaceImpl mOutputImageCaptureSurfaceImpl; private OutputSurfaceImpl mOutputImageAnalysisSurfaceImpl; private OutputSurfaceImpl mOutputPostviewSurfaceImpl; + private int mColorSpace; + + public OutputSurfaceConfigurationImplStub(OutputSurfaceImpl previewOutput, + OutputSurfaceImpl imageCaptureOutput, OutputSurfaceImpl imageAnalysisOutput, + OutputSurfaceImpl postviewOutput, int colorSpace) { + mOutputPreviewSurfaceImpl = previewOutput; + mOutputImageCaptureSurfaceImpl = imageCaptureOutput; + mOutputImageAnalysisSurfaceImpl = imageAnalysisOutput; + mOutputPostviewSurfaceImpl = postviewOutput; + mColorSpace = colorSpace; + } public OutputSurfaceConfigurationImplStub(OutputSurfaceImpl previewOutput, OutputSurfaceImpl imageCaptureOutput, OutputSurfaceImpl imageAnalysisOutput, @@ -1735,6 +1772,7 @@ public class CameraExtensionsProxyService extends Service { mOutputImageCaptureSurfaceImpl = imageCaptureOutput; mOutputImageAnalysisSurfaceImpl = imageAnalysisOutput; mOutputPostviewSurfaceImpl = postviewOutput; + mColorSpace = ColorSpaceProfiles.UNSPECIFIED; } @Override @@ -1756,6 +1794,11 @@ public class CameraExtensionsProxyService extends Service { public OutputSurfaceImpl getPostviewOutputSurface() { return mOutputPostviewSurfaceImpl; } + + @Override + public int getColorSpace() { + return mColorSpace; + } } private class OutputSurfaceImplStub implements OutputSurfaceImpl { @@ -1764,11 +1807,10 @@ public class CameraExtensionsProxyService extends Service { private final int mImageFormat; private final int mDataspace; private final long mUsage; + private final long mDynamicRangeProfile; public OutputSurfaceImplStub(OutputSurface outputSurface) { mSurface = outputSurface.surface; - mSize = new Size(outputSurface.size.width, outputSurface.size.height); - mImageFormat = outputSurface.imageFormat; if (mSurface != null) { mDataspace = SurfaceUtils.getSurfaceDataspace(mSurface); mUsage = SurfaceUtils.getSurfaceUsage(mSurface); @@ -1776,6 +1818,9 @@ public class CameraExtensionsProxyService extends Service { mDataspace = -1; mUsage = 0; } + mDynamicRangeProfile = outputSurface.dynamicRangeProfile; + mSize = new Size(outputSurface.size.width, outputSurface.size.height); + mImageFormat = outputSurface.imageFormat; } @Override @@ -1802,6 +1847,12 @@ public class CameraExtensionsProxyService extends Service { public long getUsage() { return mUsage; } + + @Override + public long getDynamicRangeProfile() { + return mDynamicRangeProfile; + } + } private class PreviewExtenderImplStub extends IPreviewExtenderImpl.Stub implements @@ -2531,6 +2582,11 @@ public class CameraExtensionsProxyService extends Service { private static CameraOutputConfig getCameraOutputConfig(Camera2OutputConfigImpl output) { CameraOutputConfig ret = new CameraOutputConfig(); + if (Flags.extension10Bit() && EFV_SUPPORTED) { + ret.dynamicRangeProfile = output.getDynamicRangeProfile(); + } else { + ret.dynamicRangeProfile = DynamicRangeProfiles.STANDARD; + } ret.outputId = new OutputConfigId(); ret.outputId.id = output.getId(); ret.physicalCameraId = output.getPhysicalCameraId(); diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index af47ed28e3b0..73584154df3a 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -611,12 +611,12 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ if (svcConnTracingEnabled()) { logTraceSvcConn("getWindow", "windowId=" + windowId); } + int displayId = Display.INVALID_DISPLAY; + if (windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID) { + displayId = mA11yWindowManager.getDisplayIdByUserIdAndWindowId( + mSystemSupport.getCurrentUserIdLocked(), windowId); + } synchronized (mLock) { - int displayId = Display.INVALID_DISPLAY; - if (windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID) { - displayId = mA11yWindowManager.getDisplayIdByUserIdAndWindowIdLocked( - mSystemSupport.getCurrentUserIdLocked(), windowId); - } ensureWindowsAvailableTimedLocked(displayId); if (!hasRightsToCurrentUserLocked()) { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 3cbfd42c279d..4be303ad9e57 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -1261,15 +1261,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub // the computation for performance reasons. boolean shouldComputeWindows = false; int displayId = event.getDisplayId(); + final int windowId = event.getWindowId(); + if (windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID + && displayId == Display.INVALID_DISPLAY) { + displayId = mA11yWindowManager.getDisplayIdByUserIdAndWindowId( + resolvedUserId, windowId); + event.setDisplayId(displayId); + } synchronized (mLock) { - final int windowId = event.getWindowId(); - if (windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID - && displayId == Display.INVALID_DISPLAY) { - displayId = mA11yWindowManager.getDisplayIdByUserIdAndWindowIdLocked( - resolvedUserId, windowId); - event.setDisplayId(displayId); - } - if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && displayId != Display.INVALID_DISPLAY && mA11yWindowManager.isTrackingWindowsLocked(displayId)) { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java index b8181505b9c4..8c06bc8f4607 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java @@ -2038,8 +2038,11 @@ public class AccessibilityWindowManager { * @param windowId The windowId * @return The display ID */ - public int getDisplayIdByUserIdAndWindowIdLocked(int userId, int windowId) { - final IBinder windowToken = getWindowTokenForUserAndWindowIdLocked(userId, windowId); + public int getDisplayIdByUserIdAndWindowId(int userId, int windowId) { + final IBinder windowToken; + synchronized (mLock) { + windowToken = getWindowTokenForUserAndWindowIdLocked(userId, windowId); + } if (traceWMEnabled()) { logTraceWM("getDisplayIdForWindow", "token=" + windowToken); } diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java index d9e25ef7dcdc..e13994e75690 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java @@ -525,8 +525,9 @@ public class TouchState { mReceivedPointersDown |= pointerFlag; mReceivedPointers[pointerId].set( event.getX(pointerIndex), event.getY(pointerIndex), event.getEventTime()); - - mPrimaryPointerId = pointerId; + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + mPrimaryPointerId = pointerId; + } } /** diff --git a/services/backup/BACKUP_OWNERS b/services/backup/BACKUP_OWNERS index f8f4f4f4bf2e..29ae2027fc3a 100644 --- a/services/backup/BACKUP_OWNERS +++ b/services/backup/BACKUP_OWNERS @@ -2,9 +2,10 @@ jstemmer@google.com martinoh@google.com -millmore@google.com niamhfw@google.com piee@google.com philippov@google.com rthakohov@google.com -sarpm@google.com
\ No newline at end of file +sarpm@google.com +beatricemarch@google.com +azilio@google.com
\ No newline at end of file diff --git a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING index 82ab0980a22b..340bc327fc7f 100644 --- a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING +++ b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING @@ -38,7 +38,8 @@ { "exclude-annotation": "androidx.test.filters.FlakyTest" } - ] + ], + "keywords": ["primary-device"] }, { "name": "CtsHardwareTestCases", diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 60bfc637225f..5e6ff55f4e94 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -4755,6 +4755,7 @@ public class ActivityManagerService extends IActivityManager.Stub autofillOptions, contentCaptureOptions, app.getDisabledCompatChanges(), + app.getLoggableCompatChanges(), serializedSystemFontMap, app.getStartElapsedTime(), app.getStartUptime()); diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java index 0ce1407004f5..48daef801245 100644 --- a/services/core/java/com/android/server/am/AppProfiler.java +++ b/services/core/java/com/android/server/am/AppProfiler.java @@ -711,7 +711,7 @@ public class AppProfiler { } } if (profile != null) { - long startTime = SystemClock.currentThreadTimeMillis(); + long startTime = SystemClock.uptimeMillis(); // skip background PSS calculation under the following situations: // - app is capturing camera imagery // - app is frozen and we have already collected PSS once. @@ -721,7 +721,7 @@ public class AppProfiler { || mService.isCameraActiveForUid(profile.mApp.uid) || mService.mConstants.APP_PROFILER_PSS_PROFILING_DISABLED; long pss = skipPSSCollection ? 0 : Debug.getPss(pid, tmp, null); - long endTime = SystemClock.currentThreadTimeMillis(); + long endTime = SystemClock.uptimeMillis(); synchronized (mProfilerLock) { if (pss != 0 && profile.getThread() != null && profile.getSetProcState() == procState @@ -852,7 +852,7 @@ public class AppProfiler { } } if (profile != null) { - long startTime = SystemClock.currentThreadTimeMillis(); + long startTime = SystemClock.uptimeMillis(); // skip background RSS calculation under the following situations: // - app is capturing camera imagery // - app is frozen and we have already collected RSS once. @@ -862,7 +862,7 @@ public class AppProfiler { || mService.isCameraActiveForUid(profile.mApp.uid) || mService.mConstants.APP_PROFILER_PSS_PROFILING_DISABLED; long rss = skipRSSCollection ? 0 : Debug.getRss(pid, null); - long endTime = SystemClock.currentThreadTimeMillis(); + long endTime = SystemClock.uptimeMillis(); synchronized (mProfilerLock) { if (rss != 0 && profile.getThread() != null && profile.getSetProcState() == procState diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index cd6964ea2631..7f6d62c29648 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -1051,7 +1051,7 @@ public class OomAdjuster { assignCachedAdjIfNecessary(mProcessList.getLruProcessesLOSP()); - postUpdateOomAdjInnerLSP(oomAdjReason, activeUids, now, nowElapsed, oldTime); + postUpdateOomAdjInnerLSP(oomAdjReason, activeUids, now, nowElapsed, oldTime, true); if (startProfiling) { mService.mOomAdjProfiler.oomAdjEnded(); @@ -1073,12 +1073,12 @@ public class OomAdjuster { @GuardedBy({"mService", "mProcLock"}) protected void postUpdateOomAdjInnerLSP(@OomAdjReason int oomAdjReason, ActiveUids activeUids, - long now, long nowElapsed, long oldTime) { + long now, long nowElapsed, long oldTime, boolean doingAll) { mNumNonCachedProcs = 0; mNumCachedHiddenProcs = 0; final boolean allChanged = updateAndTrimProcessLSP(now, nowElapsed, oldTime, activeUids, - oomAdjReason); + oomAdjReason, doingAll); mNumServiceProcs = mNewNumServiceProcs; if (mService.mAlwaysFinishActivities) { @@ -1288,7 +1288,8 @@ public class OomAdjuster { @GuardedBy({"mService", "mProcLock"}) private boolean updateAndTrimProcessLSP(final long now, final long nowElapsed, - final long oldTime, final ActiveUids activeUids, @OomAdjReason int oomAdjReason) { + final long oldTime, final ActiveUids activeUids, @OomAdjReason int oomAdjReason, + boolean doingAll) { ArrayList<ProcessRecord> lruList = mProcessList.getLruProcessesLOSP(); final int numLru = lruList.size(); @@ -1321,7 +1322,7 @@ public class OomAdjuster { if (!app.isKilledByAm() && app.getThread() != null) { // We don't need to apply the update for the process which didn't get computed if (state.getCompletedAdjSeq() == mAdjSeq) { - applyOomAdjLSP(app, true, now, nowElapsed, oomAdjReason); + applyOomAdjLSP(app, doingAll, now, nowElapsed, oomAdjReason); } if (app.isPendingFinishAttach()) { diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java index dd75bc0442d0..46bdfe892040 100644 --- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java +++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java @@ -820,7 +820,7 @@ public class OomAdjusterModernImpl extends OomAdjuster { computeConnectionsLSP(); assignCachedAdjIfNecessary(mProcessList.getLruProcessesLOSP()); - postUpdateOomAdjInnerLSP(oomAdjReason, mActiveUids, now, nowElapsed, oldTime); + postUpdateOomAdjInnerLSP(oomAdjReason, mActiveUids, now, nowElapsed, oldTime, true); } /** @@ -908,7 +908,7 @@ public class OomAdjusterModernImpl extends OomAdjuster { } } - postUpdateOomAdjInnerLSP(oomAdjReason, activeUids, now, nowElapsed, oldTime); + postUpdateOomAdjInnerLSP(oomAdjReason, activeUids, now, nowElapsed, oldTime, false); } /** diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 27d6c608cf6c..48a9d6af0df4 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -2088,8 +2088,10 @@ public final class ProcessList { + " with non-zero pid:" + app.getPid()); } app.setDisabledCompatChanges(null); + app.setLoggableCompatChanges(null); if (mPlatformCompat != null) { app.setDisabledCompatChanges(mPlatformCompat.getDisabledChanges(app.info)); + app.setLoggableCompatChanges(mPlatformCompat.getLoggableChanges(app.info)); } final long startSeq = ++mProcStartSeqCounter; app.setStartSeq(startSeq); diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index 9fa3a8bf6f3b..b93908974a42 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -259,6 +259,12 @@ class ProcessRecord implements WindowProcessListener { private long[] mDisabledCompatChanges; /** + * Set of compat changes for the process that are intended to be logged to logcat. + */ + @GuardedBy("mService") + private long[] mLoggableCompatChanges; + + /** * Who is watching for the death. */ @GuardedBy("mService") @@ -935,11 +941,21 @@ class ProcessRecord implements WindowProcessListener { } @GuardedBy("mService") + long[] getLoggableCompatChanges() { + return mLoggableCompatChanges; + } + + @GuardedBy("mService") void setDisabledCompatChanges(long[] disabledCompatChanges) { mDisabledCompatChanges = disabledCompatChanges; } @GuardedBy("mService") + void setLoggableCompatChanges(long[] loggableCompatChanges) { + mLoggableCompatChanges = loggableCompatChanges; + } + + @GuardedBy("mService") void unlinkDeathRecipient() { if (mDeathRecipient != null && mThread != null) { mThread.asBinder().unlinkToDeath(mDeathRecipient, 0); diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index d1bda7991f45..7df5fdd282c3 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -170,6 +170,7 @@ public class SettingsToPropertiesMapper { "pixel_connectivity_gps", "pixel_system_sw_video", "pixel_watch", + "platform_compat", "platform_security", "pmw", "power", diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index e0c24256f5b1..14428c41ef26 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -157,7 +157,7 @@ public class AudioDeviceInventory { * corresponding peers in case of BLE */ void addAudioDeviceInInventoryIfNeeded(int deviceType, String address, String peerAddress, - @AudioDeviceCategory int category) { + @AudioDeviceCategory int category, boolean userDefined) { if (!isBluetoothOutDevice(deviceType)) { return; } @@ -167,7 +167,11 @@ public class AudioDeviceInventory { ads = findBtDeviceStateForAddress(peerAddress, deviceType); } if (ads != null) { - if (ads.getAudioDeviceCategory() != category) { + // if category is user defined allow to change back to unknown otherwise + // do not reset the category back to unknown since it might have been set + // before by the user + if (ads.getAudioDeviceCategory() != category && (userDefined + || category != AUDIO_DEVICE_CATEGORY_UNKNOWN)) { ads.setAudioDeviceCategory(category); mDeviceBroker.postUpdatedAdiDeviceState(ads); mDeviceBroker.postPersistAudioDeviceSettings(); @@ -220,9 +224,9 @@ public class AudioDeviceInventory { void addAudioDeviceWithCategoryInInventoryIfNeeded(@NonNull String address, @AudioDeviceCategory int btAudioDeviceCategory) { addAudioDeviceInInventoryIfNeeded(DEVICE_OUT_BLE_HEADSET, - address, "", btAudioDeviceCategory); + address, "", btAudioDeviceCategory, /*userDefined=*/true); addAudioDeviceInInventoryIfNeeded(DEVICE_OUT_BLUETOOTH_A2DP, - address, "", btAudioDeviceCategory); + address, "", btAudioDeviceCategory, /*userDefined=*/true); } @AudioDeviceCategory @@ -1733,7 +1737,7 @@ public class AudioDeviceInventory { purgeDevicesRoles_l(); } else { addAudioDeviceInInventoryIfNeeded(device, address, "", - BtHelper.getBtDeviceCategory(address)); + BtHelper.getBtDeviceCategory(address), /*userDefined=*/false); } AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "SCO " + (AudioSystem.isInputDevice(device) ? "source" : "sink") @@ -2023,7 +2027,7 @@ public class AudioDeviceInventory { updateBluetoothPreferredModes_l(btInfo.mDevice /*connectedDevice*/); addAudioDeviceInInventoryIfNeeded(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, "", - BtHelper.getBtDeviceCategory(address)); + BtHelper.getBtDeviceCategory(address), /*userDefined=*/false); } static final int[] CAPTURE_PRESETS = new int[] {AudioSource.MIC, AudioSource.CAMCORDER, @@ -2357,7 +2361,7 @@ public class AudioDeviceInventory { DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable"); setCurrentAudioRouteNameIfPossible(name, false /*fromA2dp*/); addAudioDeviceInInventoryIfNeeded(DEVICE_OUT_HEARING_AID, address, "", - BtHelper.getBtDeviceCategory(address)); + BtHelper.getBtDeviceCategory(address), /*userDefined=*/false); new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceAvailable") .set(MediaMetrics.Property.ADDRESS, address != null ? address : "") .set(MediaMetrics.Property.DEVICE, @@ -2488,7 +2492,7 @@ public class AudioDeviceInventory { mDeviceBroker.postAccessoryPlugMediaUnmute(device); setCurrentAudioRouteNameIfPossible(name, /*fromA2dp=*/false); addAudioDeviceInInventoryIfNeeded(device, address, peerAddress, - BtHelper.getBtDeviceCategory(address)); + BtHelper.getBtDeviceCategory(address), /*userDefined=*/false); } if (streamType == AudioSystem.STREAM_DEFAULT) { diff --git a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java index 03acf72725e7..d93ff9dac91f 100644 --- a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java +++ b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java @@ -82,13 +82,6 @@ public final class BroadcastRadioServiceImpl { Slogf.w(TAG, "No module %s with id %d (HAL AIDL)", name, moduleId); return; } - try { - radioModule.setInternalHalCallback(); - } catch (RemoteException ex) { - Slogf.wtf(TAG, ex, "Broadcast radio module %s with id %d (HAL AIDL) " - + "cannot register HAL callback", name, moduleId); - return; - } if (DEBUG) { Slogf.d(TAG, "Loaded broadcast radio module %s with id %d (HAL AIDL)", name, moduleId); diff --git a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java index 4b3444db38e5..cd865105c48e 100644 --- a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java +++ b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java @@ -246,10 +246,6 @@ final class RadioModule { return mProperties; } - void setInternalHalCallback() throws RemoteException { - mService.setTunerCallback(mHalTunerCallback); - } - TunerSession openSession(android.hardware.radio.ITunerCallback userCb) throws RemoteException { mLogger.logRadioEvent("Open TunerSession"); @@ -257,10 +253,14 @@ final class RadioModule { Boolean antennaConnected; RadioManager.ProgramInfo currentProgramInfo; synchronized (mLock) { + boolean isFirstTunerSession = mAidlTunerSessions.isEmpty(); tunerSession = new TunerSession(this, mService, userCb); mAidlTunerSessions.add(tunerSession); antennaConnected = mAntennaConnected; currentProgramInfo = mCurrentProgramInfo; + if (isFirstTunerSession) { + mService.setTunerCallback(mHalTunerCallback); + } } // Propagate state to new client. // Note: These callbacks are invoked while holding mLock to prevent race conditions @@ -284,7 +284,6 @@ final class RadioModule { synchronized (mLock) { tunerSessions = new TunerSession[mAidlTunerSessions.size()]; mAidlTunerSessions.toArray(tunerSessions); - mAidlTunerSessions.clear(); } for (TunerSession tunerSession : tunerSessions) { @@ -402,6 +401,14 @@ final class RadioModule { mAidlTunerSessions.remove(tunerSession); } onTunerSessionProgramListFilterChanged(null); + if (mAidlTunerSessions.isEmpty()) { + try { + mService.unsetTunerCallback(); + } catch (RemoteException ex) { + Slogf.wtf(TAG, ex, "Failed to unregister HAL callback for module %d", + mProperties.getId()); + } + } } // add to mHandler queue diff --git a/services/core/java/com/android/server/compat/CompatConfig.java b/services/core/java/com/android/server/compat/CompatConfig.java index 9102cfd0d426..79025d00d128 100644 --- a/services/core/java/com/android/server/compat/CompatConfig.java +++ b/services/core/java/com/android/server/compat/CompatConfig.java @@ -25,6 +25,7 @@ import android.compat.Compatibility.ChangeConfig; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.os.Build; import android.os.Environment; import android.text.TextUtils; import android.util.LongArray; @@ -72,7 +73,6 @@ import javax.xml.datatype.DatatypeConfigurationException; * been configured. */ final class CompatConfig { - private static final String TAG = "CompatConfig"; private static final String APP_COMPAT_DATA_DIR = "/data/misc/appcompat"; private static final String STATIC_OVERRIDES_PRODUCT_DIR = "/product/etc/appcompat"; @@ -149,6 +149,56 @@ final class CompatConfig { } /** + * Retrieves the set of changes that are intended to be logged. This includes changes that + * target the most recent SDK version and are not disabled. + * + * @param app the app in question + * @return a sorted long array of change IDs + */ + long[] getLoggableChanges(ApplicationInfo app) { + LongArray loggable = new LongArray(mChanges.size()); + for (CompatChange c : mChanges.values()) { + long changeId = c.getId(); + boolean isLatestSdk = isChangeTargetingLatestSdk(c, app.targetSdkVersion); + if (c.isEnabled(app, mAndroidBuildClassifier) && isLatestSdk) { + loggable.add(changeId); + } + } + final long[] sortedChanges = loggable.toArray(); + Arrays.sort(sortedChanges); + return sortedChanges; + } + + /** + * Whether the change indicated by the given changeId is targeting the latest SDK version. + * @param c the change for which to check the target SDK version + * @param appSdkVersion the target sdk version of the app + * @return true if the changeId targets the current sdk version or the current development + * version. + */ + boolean isChangeTargetingLatestSdk(CompatChange c, int appSdkVersion) { + int maxTargetSdk = maxTargetSdkForCompatChange(c) + 1; + if (maxTargetSdk <= 0) { + // No max target sdk found. + return false; + } + + return maxTargetSdk == Build.VERSION_CODES.CUR_DEVELOPMENT || maxTargetSdk == appSdkVersion; + } + + /** + * Retrieves the CompatChange associated with the given changeId. Will return null if the + * changeId is not found. Used only for performance improvement purposes, in order to reduce + * lookups. + * + * @param changeId for which to look up the CompatChange + * @return the found compat change, or null if not found. + */ + CompatChange getCompatChange(long changeId) { + return mChanges.get(changeId); + } + + /** * Looks up a change ID by name. * * @param name name of the change to look up @@ -164,7 +214,7 @@ final class CompatConfig { } /** - * Checks if a given change is enabled for a given application. + * Checks if a given change id is enabled for a given application. * * @param changeId the ID of the change in question * @param app app to check for @@ -173,6 +223,18 @@ final class CompatConfig { */ boolean isChangeEnabled(long changeId, ApplicationInfo app) { CompatChange c = mChanges.get(changeId); + return isChangeEnabled(c, app); + } + + /** + * Checks if a given change is enabled for a given application. + * + * @param c the CompatChange in question + * @param app the app to check for + * @return {@code true} if the change is enabled for this app. Also returns {@code true} if the + * change ID is not known, as unknown changes are enabled by default. + */ + boolean isChangeEnabled(CompatChange c, ApplicationInfo app) { if (c == null) { // we know nothing about this change: default behaviour is enabled. return true; @@ -301,9 +363,21 @@ final class CompatConfig { /** * Returns the maximum SDK version for which this change can be opted in (or -1 if it is not * target SDK gated). + * + * @param changeId the id of the CompatChange to check for the max target sdk */ int maxTargetSdkForChangeIdOptIn(long changeId) { CompatChange c = mChanges.get(changeId); + return maxTargetSdkForCompatChange(c); + } + + /** + * Returns the maximum SDK version for which this change can be opted in (or -1 if it is not + * target SDK gated). + * + * @param c the CompatChange to check for the max target sdk + */ + int maxTargetSdkForCompatChange(CompatChange c) { if (c != null && c.getEnableSinceTargetSdk() != -1) { return c.getEnableSinceTargetSdk() - 1; } diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java index 6cca130af35d..f8fd0a0790e0 100644 --- a/services/core/java/com/android/server/compat/PlatformCompat.java +++ b/services/core/java/com/android/server/compat/PlatformCompat.java @@ -120,8 +120,16 @@ public class PlatformCompat extends IPlatformCompat.Stub { reportChangeInternal(changeId, uid, ChangeReporter.STATE_LOGGED); } + /** + * Report the change, but skip over the sdk target version check. This can be used to force the + * debug logs. + * + * @param changeId of the change to report + * @param uid of the user + * @param state of the change - enabled/disabled/logged + */ private void reportChangeInternal(long changeId, int uid, int state) { - mChangeReporter.reportChange(uid, changeId, state); + mChangeReporter.reportChange(uid, changeId, state, true); } @Override @@ -164,15 +172,25 @@ public class PlatformCompat extends IPlatformCompat.Stub { } /** - * Internal version of {@link #isChangeEnabled(long, ApplicationInfo)}. + * Internal version of {@link #isChangeEnabled(long, ApplicationInfo)}. If the provided appInfo + * is not null, also reports the change. + * + * @param changeId of the change to report + * @param appInfo the app to check * * <p>Does not perform costly permission check. */ public boolean isChangeEnabledInternal(long changeId, ApplicationInfo appInfo) { - boolean enabled = isChangeEnabledInternalNoLogging(changeId, appInfo); + // Fetch the CompatChange. This is done here instead of in mCompatConfig to avoid multiple + // fetches. + CompatChange c = mCompatConfig.getCompatChange(changeId); + + boolean enabled = mCompatConfig.isChangeEnabled(c, appInfo); + int state = enabled ? ChangeReporter.STATE_ENABLED : ChangeReporter.STATE_DISABLED; if (appInfo != null) { - reportChangeInternal(changeId, appInfo.uid, - enabled ? ChangeReporter.STATE_ENABLED : ChangeReporter.STATE_DISABLED); + boolean isTargetingLatestSdk = + mCompatConfig.isChangeTargetingLatestSdk(c, appInfo.targetSdkVersion); + mChangeReporter.reportChange(appInfo.uid, changeId, state, isTargetingLatestSdk); } return enabled; } @@ -399,6 +417,19 @@ public class PlatformCompat extends IPlatformCompat.Stub { } /** + * Retrieves the set of changes that should be logged for a given app. Any change ID not in the + * returned array is ignored for logging purposes. + * + * @param appInfo The app in question + * @return A sorted long array of change IDs. We use a primitive array to minimize memory + * footprint: Every app process will store this array statically so we aim to reduce + * overhead as much as possible. + */ + public long[] getLoggableChanges(ApplicationInfo appInfo) { + return mCompatConfig.getLoggableChanges(appInfo); + } + + /** * Look up a change ID by name. * * @param name Name of the change to look up diff --git a/services/core/java/com/android/server/display/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java index 3b05b47eb542..a7748f4fae98 100644 --- a/services/core/java/com/android/server/display/DisplayDevice.java +++ b/services/core/java/com/android/server/display/DisplayDevice.java @@ -44,6 +44,15 @@ import java.io.PrintWriter; * </p> */ abstract class DisplayDevice { + /** + * Maximum acceptable anisotropy for the output image. + * + * Necessary to avoid unnecessary scaling when pixels are almost square, as they are non ideal + * anyway. For external displays, we expect an anisotropy of about 2% even if the pixels + * are, in fact, square due to the imprecision of the display's actual size (parsed from edid + * and rounded to the nearest cm). + */ + static final float MAX_ANISOTROPY = 1.025f; private static final String TAG = "DisplayDevice"; private static final Display.Mode EMPTY_DISPLAY_MODE = new Display.Mode.Builder().build(); @@ -69,13 +78,21 @@ abstract class DisplayDevice { // Do not use for any other purpose. DisplayDeviceInfo mDebugLastLoggedDeviceInfo; - public DisplayDevice(DisplayAdapter displayAdapter, IBinder displayToken, String uniqueId, + private final boolean mIsAnisotropyCorrectionEnabled; + + DisplayDevice(DisplayAdapter displayAdapter, IBinder displayToken, String uniqueId, Context context) { + this(displayAdapter, displayToken, uniqueId, context, false); + } + + DisplayDevice(DisplayAdapter displayAdapter, IBinder displayToken, String uniqueId, + Context context, boolean isAnisotropyCorrectionEnabled) { mDisplayAdapter = displayAdapter; mDisplayToken = displayToken; mUniqueId = uniqueId; mDisplayDeviceConfig = null; mContext = context; + mIsAnisotropyCorrectionEnabled = isAnisotropyCorrectionEnabled; } /** @@ -143,8 +160,17 @@ abstract class DisplayDevice { DisplayDeviceInfo displayDeviceInfo = getDisplayDeviceInfoLocked(); final boolean isRotated = mCurrentOrientation == ROTATION_90 || mCurrentOrientation == ROTATION_270; - return isRotated ? new Point(displayDeviceInfo.height, displayDeviceInfo.width) - : new Point(displayDeviceInfo.width, displayDeviceInfo.height); + var width = displayDeviceInfo.width; + var height = displayDeviceInfo.height; + if (mIsAnisotropyCorrectionEnabled && displayDeviceInfo.yDpi > 0 + && displayDeviceInfo.xDpi > 0) { + if (displayDeviceInfo.xDpi > displayDeviceInfo.yDpi * MAX_ANISOTROPY) { + height = (int) (height * displayDeviceInfo.xDpi / displayDeviceInfo.yDpi + 0.5); + } else if (displayDeviceInfo.xDpi * MAX_ANISOTROPY < displayDeviceInfo.yDpi) { + width = (int) (width * displayDeviceInfo.yDpi / displayDeviceInfo.xDpi + 0.5); + } + } + return isRotated ? new Point(height, width) : new Point(width, height); } /** diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 88c24e0a7eff..b2fd9edf61fe 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -257,7 +257,8 @@ final class LocalDisplayAdapter extends DisplayAdapter { SurfaceControl.DynamicDisplayInfo dynamicInfo, SurfaceControl.DesiredDisplayModeSpecs modeSpecs, boolean isFirstDisplay) { super(LocalDisplayAdapter.this, displayToken, UNIQUE_ID_PREFIX + physicalDisplayId, - getContext()); + getContext(), + getFeatureFlags().isPixelAnisotropyCorrectionInLogicalDisplayEnabled()); mPhysicalDisplayId = physicalDisplayId; mIsFirstDisplay = isFirstDisplay; updateDisplayPropertiesLocked(staticDisplayInfo, dynamicInfo, modeSpecs); diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java index db636d619bd3..5eaaf3504e85 100644 --- a/services/core/java/com/android/server/display/LogicalDisplay.java +++ b/services/core/java/com/android/server/display/LogicalDisplay.java @@ -35,7 +35,6 @@ import android.view.SurfaceControl; import com.android.server.display.layout.Layout; import com.android.server.display.mode.DisplayModeDirector; -import com.android.server.wm.utils.DisplayInfoOverrides; import com.android.server.wm.utils.InsetUtils; import java.io.PrintWriter; @@ -204,7 +203,28 @@ final class LogicalDisplay { private SparseArray<SurfaceControl.RefreshRateRange> mThermalRefreshRateThrottling = new SparseArray<>(); + /** + * If the aspect ratio of the resolution of the display does not match the physical aspect + * ratio of the display, then without this feature enabled, picture would appear stretched to + * the user. This is because applications assume that they are rendered on square pixels + * (meaning density of pixels in x and y directions are equal). This would result into circles + * appearing as ellipses to the user. + * To compensate for non-square (anisotropic) pixels, if this feature is enabled: + * 1. LogicalDisplay will add more pixels for the applications to render on, as if the pixels + * were square and occupied the full display. + * 2. SurfaceFlinger will squeeze this taller/wider surface into the available number of + * physical pixels in the current display resolution. + * 3. If a setting on the display itself is set to "fill the entire display panel" then the + * display will stretch the pixels to fill the display fully. + */ + private final boolean mIsAnisotropyCorrectionEnabled; + LogicalDisplay(int displayId, int layerStack, DisplayDevice primaryDisplayDevice) { + this(displayId, layerStack, primaryDisplayDevice, false); + } + + LogicalDisplay(int displayId, int layerStack, DisplayDevice primaryDisplayDevice, + boolean isAnisotropyCorrectionEnabled) { mDisplayId = displayId; mLayerStack = layerStack; mPrimaryDisplayDevice = primaryDisplayDevice; @@ -215,6 +235,7 @@ final class LogicalDisplay { mThermalBrightnessThrottlingDataId = DisplayDeviceConfig.DEFAULT_ID; mPowerThrottlingDataId = DisplayDeviceConfig.DEFAULT_ID; mBaseDisplayInfo.thermalBrightnessThrottlingDataId = mThermalBrightnessThrottlingDataId; + mIsAnisotropyCorrectionEnabled = isAnisotropyCorrectionEnabled; } public void setDevicePositionLocked(int position) { @@ -453,6 +474,14 @@ final class LogicalDisplay { int maskedWidth = deviceInfo.width - maskingInsets.left - maskingInsets.right; int maskedHeight = deviceInfo.height - maskingInsets.top - maskingInsets.bottom; + if (mIsAnisotropyCorrectionEnabled && deviceInfo.xDpi > 0 && deviceInfo.yDpi > 0) { + if (deviceInfo.xDpi > deviceInfo.yDpi * DisplayDevice.MAX_ANISOTROPY) { + maskedHeight = (int) (maskedHeight * deviceInfo.xDpi / deviceInfo.yDpi + 0.5); + } else if (deviceInfo.xDpi * DisplayDevice.MAX_ANISOTROPY < deviceInfo.yDpi) { + maskedWidth = (int) (maskedWidth * deviceInfo.yDpi / deviceInfo.xDpi + 0.5); + } + } + mBaseDisplayInfo.type = deviceInfo.type; mBaseDisplayInfo.address = deviceInfo.address; mBaseDisplayInfo.deviceProductInfo = deviceInfo.deviceProductInfo; @@ -666,6 +695,31 @@ final class LogicalDisplay { physWidth -= maskingInsets.left + maskingInsets.right; physHeight -= maskingInsets.top + maskingInsets.bottom; + var displayLogicalWidth = displayInfo.logicalWidth; + var displayLogicalHeight = displayInfo.logicalHeight; + + if (mIsAnisotropyCorrectionEnabled && displayDeviceInfo.xDpi > 0 + && displayDeviceInfo.yDpi > 0) { + if (displayDeviceInfo.xDpi > displayDeviceInfo.yDpi * DisplayDevice.MAX_ANISOTROPY) { + var scalingFactor = displayDeviceInfo.yDpi / displayDeviceInfo.xDpi; + if (rotated) { + displayLogicalWidth = (int) ((float) displayLogicalWidth * scalingFactor + 0.5); + } else { + displayLogicalHeight = (int) ((float) displayLogicalHeight * scalingFactor + + 0.5); + } + } else if (displayDeviceInfo.xDpi * DisplayDevice.MAX_ANISOTROPY + < displayDeviceInfo.yDpi) { + var scalingFactor = displayDeviceInfo.xDpi / displayDeviceInfo.yDpi; + if (rotated) { + displayLogicalHeight = (int) ((float) displayLogicalHeight * scalingFactor + + 0.5); + } else { + displayLogicalWidth = (int) ((float) displayLogicalWidth * scalingFactor + 0.5); + } + } + } + // Determine whether the width or height is more constrained to be scaled. // physWidth / displayInfo.logicalWidth => letter box // or physHeight / displayInfo.logicalHeight => pillar box @@ -675,16 +729,16 @@ final class LogicalDisplay { // comparing them. int displayRectWidth, displayRectHeight; if ((displayInfo.flags & Display.FLAG_SCALING_DISABLED) != 0 || mDisplayScalingDisabled) { - displayRectWidth = displayInfo.logicalWidth; - displayRectHeight = displayInfo.logicalHeight; - } else if (physWidth * displayInfo.logicalHeight - < physHeight * displayInfo.logicalWidth) { + displayRectWidth = displayLogicalWidth; + displayRectHeight = displayLogicalHeight; + } else if (physWidth * displayLogicalHeight + < physHeight * displayLogicalWidth) { // Letter box. displayRectWidth = physWidth; - displayRectHeight = displayInfo.logicalHeight * physWidth / displayInfo.logicalWidth; + displayRectHeight = displayLogicalHeight * physWidth / displayLogicalWidth; } else { // Pillar box. - displayRectWidth = displayInfo.logicalWidth * physHeight / displayInfo.logicalHeight; + displayRectWidth = displayLogicalWidth * physHeight / displayLogicalHeight; displayRectHeight = physHeight; } int displayRectTop = (physHeight - displayRectHeight) / 2; diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 3452e0f188c3..e092fdae7cc7 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -1151,7 +1151,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { */ private LogicalDisplay createNewLogicalDisplayLocked(DisplayDevice device, int displayId) { final int layerStack = assignLayerStackLocked(displayId); - final LogicalDisplay display = new LogicalDisplay(displayId, layerStack, device); + final LogicalDisplay display = new LogicalDisplay(displayId, layerStack, device, + mFlags.isPixelAnisotropyCorrectionInLogicalDisplayEnabled()); display.updateLocked(mDisplayDeviceRepo); final DisplayInfo info = display.getDisplayInfoLocked(); diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index 3c98ee453913..15ee9372b46a 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -121,6 +121,11 @@ public class DisplayManagerFlags { Flags::refreshRateVotingTelemetry ); + private final FlagState mPixelAnisotropyCorrectionEnabled = new FlagState( + Flags.FLAG_ENABLE_PIXEL_ANISOTROPY_CORRECTION, + Flags::enablePixelAnisotropyCorrection + ); + private final FlagState mSensorBasedBrightnessThrottling = new FlagState( Flags.FLAG_SENSOR_BASED_BRIGHTNESS_THROTTLING, Flags::sensorBasedBrightnessThrottling @@ -259,6 +264,10 @@ public class DisplayManagerFlags { return mRefreshRateVotingTelemetry.isEnabled(); } + public boolean isPixelAnisotropyCorrectionInLogicalDisplayEnabled() { + return mPixelAnisotropyCorrectionEnabled.isEnabled(); + } + public boolean isSensorBasedBrightnessThrottlingEnabled() { return mSensorBasedBrightnessThrottling.isEnabled(); } @@ -290,6 +299,7 @@ public class DisplayManagerFlags { pw.println(" " + mAutoBrightnessModesFlagState); pw.println(" " + mFastHdrTransitions); pw.println(" " + mRefreshRateVotingTelemetry); + pw.println(" " + mPixelAnisotropyCorrectionEnabled); pw.println(" " + mSensorBasedBrightnessThrottling); pw.println(" " + mRefactorDisplayPowerController); } diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index 34045273c93a..9bf36e4e605f 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -186,6 +186,14 @@ flag { } flag { + name: "enable_pixel_anisotropy_correction" + namespace: "display_manager" + description: "Feature flag for enabling display anisotropy correction through LogicalDisplay upscaling" + bug: "317363416" + is_fixed_read_only: true +} + +flag { name: "sensor_based_brightness_throttling" namespace: "display_manager" description: "Feature flag for enabling brightness throttling using sensor from config." diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index 8b4e1ff4981f..64cbd5488d90 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -1015,12 +1015,15 @@ public class DisplayModeDirector { // Infinity means that we want the highest possible refresh rate minRefreshRate = highestRefreshRate; - if (!mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled) { - // The flag had been turned off, we need to restore the original value + if (!mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled + && displayId == Display.DEFAULT_DISPLAY) { + // The flag has been turned off, we need to restore the original value. We'll + // use the peak refresh rate of the default display. Settings.System.putFloatForUser(cr, Settings.System.MIN_REFRESH_RATE, highestRefreshRate, cr.getUserId()); } } else if (mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled + && displayId == Display.DEFAULT_DISPLAY && Math.round(minRefreshRate) == Math.round(highestRefreshRate)) { // The flag has been turned on, we need to upgrade the setting Settings.System.putFloatForUser(cr, Settings.System.MIN_REFRESH_RATE, @@ -1033,12 +1036,15 @@ public class DisplayModeDirector { // Infinity means that we want the highest possible refresh rate peakRefreshRate = highestRefreshRate; - if (!mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled) { - // The flag had been turned off, we need to restore the original value + if (!mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled + && displayId == Display.DEFAULT_DISPLAY) { + // The flag has been turned off, we need to restore the original value. We'll + // use the peak refresh rate of the default display. Settings.System.putFloatForUser(cr, Settings.System.PEAK_REFRESH_RATE, highestRefreshRate, cr.getUserId()); } } else if (mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled + && displayId == Display.DEFAULT_DISPLAY && Math.round(peakRefreshRate) == Math.round(highestRefreshRate)) { // The flag has been turned on, we need to upgrade the setting Settings.System.putFloatForUser(cr, Settings.System.PEAK_REFRESH_RATE, diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java index 0ef23e903b6a..e6bf2c968350 100644 --- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java +++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java @@ -61,7 +61,6 @@ import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; import java.nio.charset.StandardCharsets; /** @@ -133,7 +132,11 @@ public class GrammaticalInflectionService extends SystemService { @Override public int getSystemGrammaticalGender(AttributionSource attributionSource, int userId) { - return canGetSystemGrammaticalGender(attributionSource) + if (!checkSystemGrammaticalGenderPermission(mPermissionManager, attributionSource)) { + throw new SecurityException("AttributionSource: " + attributionSource + + " does not have READ_SYSTEM_GRAMMATICAL_GENDER permission."); + } + return checkSystemTermsOfAddressIsEnabled() ? GrammaticalInflectionService.this.getSystemGrammaticalGender( attributionSource, userId) : GRAMMATICAL_GENDER_NOT_SPECIFIED; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 996477d9c9ba..fef56610b406 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -67,7 +67,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiThread; import android.annotation.UserIdInt; -import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -196,21 +195,16 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.security.InvalidParameterException; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Locale; import java.util.Objects; import java.util.OptionalInt; import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -730,344 +724,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private final CopyOnWriteArrayList<InputMethodListListener> mInputMethodListListeners = new CopyOnWriteArrayList<>(); - /** - * Internal state snapshot when - * {@link IInputMethod#startInput(IInputMethod.StartInputParams)} is about to be called. - * - * <p>Calling that IPC endpoint basically means that - * {@link InputMethodService#doStartInput(InputConnection, EditorInfo, boolean)} will be called - * back in the current IME process shortly, which will also affect what the current IME starts - * receiving from {@link InputMethodService#getCurrentInputConnection()}. In other words, this - * snapshot will be taken every time when {@link InputMethodManagerService} is initiating a new - * logical input session between the client application and the current IME.</p> - * - * <p>Be careful to not keep strong references to this object forever, which can prevent - * {@link StartInputInfo#mImeToken} and {@link StartInputInfo#mTargetWindow} from being GC-ed. - * </p> - */ - private static class StartInputInfo { - private static final AtomicInteger sSequenceNumber = new AtomicInteger(0); - - final int mSequenceNumber; - final long mTimestamp; - final long mWallTime; - @UserIdInt - final int mImeUserId; - @NonNull - final IBinder mImeToken; - final int mImeDisplayId; - @NonNull - final String mImeId; - @StartInputReason - final int mStartInputReason; - final boolean mRestarting; - @UserIdInt - final int mTargetUserId; - final int mTargetDisplayId; - @Nullable - final IBinder mTargetWindow; - @NonNull - final EditorInfo mEditorInfo; - @SoftInputModeFlags - final int mTargetWindowSoftInputMode; - final int mClientBindSequenceNumber; - - StartInputInfo(@UserIdInt int imeUserId, @NonNull IBinder imeToken, int imeDisplayId, - @NonNull String imeId, @StartInputReason int startInputReason, boolean restarting, - @UserIdInt int targetUserId, int targetDisplayId, @Nullable IBinder targetWindow, - @NonNull EditorInfo editorInfo, @SoftInputModeFlags int targetWindowSoftInputMode, - int clientBindSequenceNumber) { - mSequenceNumber = sSequenceNumber.getAndIncrement(); - mTimestamp = SystemClock.uptimeMillis(); - mWallTime = System.currentTimeMillis(); - mImeUserId = imeUserId; - mImeToken = imeToken; - mImeDisplayId = imeDisplayId; - mImeId = imeId; - mStartInputReason = startInputReason; - mRestarting = restarting; - mTargetUserId = targetUserId; - mTargetDisplayId = targetDisplayId; - mTargetWindow = targetWindow; - mEditorInfo = editorInfo; - mTargetWindowSoftInputMode = targetWindowSoftInputMode; - mClientBindSequenceNumber = clientBindSequenceNumber; - } - } - @GuardedBy("ImfLock.class") private final WeakHashMap<IBinder, IBinder> mImeTargetWindowMap = new WeakHashMap<>(); - @VisibleForTesting - static final class SoftInputShowHideHistory { - private final Entry[] mEntries = new Entry[16]; - private int mNextIndex = 0; - private static final AtomicInteger sSequenceNumber = new AtomicInteger(0); - - static final class Entry { - final int mSequenceNumber = sSequenceNumber.getAndIncrement(); - @Nullable - final ClientState mClientState; - @SoftInputModeFlags - final int mFocusedWindowSoftInputMode; - @SoftInputShowHideReason - final int mReason; - // The timing of handling showCurrentInputLocked() or hideCurrentInputLocked(). - final long mTimestamp; - final long mWallTime; - final boolean mInFullscreenMode; - @NonNull - final String mFocusedWindowName; - @Nullable - final EditorInfo mEditorInfo; - @NonNull - final String mRequestWindowName; - @Nullable - final String mImeControlTargetName; - @Nullable - final String mImeTargetNameFromWm; - @Nullable - final String mImeSurfaceParentName; - - Entry(ClientState client, EditorInfo editorInfo, - String focusedWindowName, @SoftInputModeFlags int softInputMode, - @SoftInputShowHideReason int reason, - boolean inFullscreenMode, String requestWindowName, - @Nullable String imeControlTargetName, @Nullable String imeTargetName, - @Nullable String imeSurfaceParentName) { - mClientState = client; - mEditorInfo = editorInfo; - mFocusedWindowName = focusedWindowName; - mFocusedWindowSoftInputMode = softInputMode; - mReason = reason; - mTimestamp = SystemClock.uptimeMillis(); - mWallTime = System.currentTimeMillis(); - mInFullscreenMode = inFullscreenMode; - mRequestWindowName = requestWindowName; - mImeControlTargetName = imeControlTargetName; - mImeTargetNameFromWm = imeTargetName; - mImeSurfaceParentName = imeSurfaceParentName; - } - } - - void addEntry(@NonNull Entry entry) { - final int index = mNextIndex; - mEntries[index] = entry; - mNextIndex = (mNextIndex + 1) % mEntries.length; - } - - void dump(@NonNull PrintWriter pw, @NonNull String prefix) { - final DateTimeFormatter formatter = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US) - .withZone(ZoneId.systemDefault()); - - for (int i = 0; i < mEntries.length; ++i) { - final Entry entry = mEntries[(i + mNextIndex) % mEntries.length]; - if (entry == null) { - continue; - } - pw.print(prefix); - pw.println("SoftInputShowHide #" + entry.mSequenceNumber + ":"); - - pw.print(prefix); - pw.println(" time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime)) - + " (timestamp=" + entry.mTimestamp + ")"); - - pw.print(prefix); - pw.print(" reason=" + InputMethodDebug.softInputDisplayReasonToString( - entry.mReason)); - pw.println(" inFullscreenMode=" + entry.mInFullscreenMode); - - pw.print(prefix); - pw.println(" requestClient=" + entry.mClientState); - - pw.print(prefix); - pw.println(" focusedWindowName=" + entry.mFocusedWindowName); - - pw.print(prefix); - pw.println(" requestWindowName=" + entry.mRequestWindowName); - - pw.print(prefix); - pw.println(" imeControlTargetName=" + entry.mImeControlTargetName); - - pw.print(prefix); - pw.println(" imeTargetNameFromWm=" + entry.mImeTargetNameFromWm); - - pw.print(prefix); - pw.println(" imeSurfaceParentName=" + entry.mImeSurfaceParentName); - - pw.print(prefix); - pw.print(" editorInfo:"); - if (entry.mEditorInfo != null) { - pw.print(" inputType=" + entry.mEditorInfo.inputType); - pw.print(" privateImeOptions=" + entry.mEditorInfo.privateImeOptions); - pw.println(" fieldId (viewId)=" + entry.mEditorInfo.fieldId); - } else { - pw.println(" null"); - } - - pw.print(prefix); - pw.println(" focusedWindowSoftInputMode=" + InputMethodDebug.softInputModeToString( - entry.mFocusedWindowSoftInputMode)); - } - } - } - - /** - * A ring buffer to store the history of {@link StartInputInfo}. - */ - private static final class StartInputHistory { - /** - * Entry size for non low-RAM devices. - * - * <p>TODO: Consider to follow what other system services have been doing to manage - * constants (e.g. {@link android.provider.Settings.Global#ACTIVITY_MANAGER_CONSTANTS}).</p> - */ - private static final int ENTRY_SIZE_FOR_HIGH_RAM_DEVICE = 32; - - /** - * Entry size for low-RAM devices. - * - * <p>TODO: Consider to follow what other system services have been doing to manage - * constants (e.g. {@link android.provider.Settings.Global#ACTIVITY_MANAGER_CONSTANTS}).</p> - */ - private static final int ENTRY_SIZE_FOR_LOW_RAM_DEVICE = 5; - - private static int getEntrySize() { - if (ActivityManager.isLowRamDeviceStatic()) { - return ENTRY_SIZE_FOR_LOW_RAM_DEVICE; - } else { - return ENTRY_SIZE_FOR_HIGH_RAM_DEVICE; - } - } - - /** - * Backing store for the ring buffer. - */ - private final Entry[] mEntries = new Entry[getEntrySize()]; - - /** - * An index of {@link #mEntries}, to which next {@link #addEntry(StartInputInfo)} should - * write. - */ - private int mNextIndex = 0; - - /** - * Recyclable entry to store the information in {@link StartInputInfo}. - */ - private static final class Entry { - int mSequenceNumber; - long mTimestamp; - long mWallTime; - @UserIdInt - int mImeUserId; - @NonNull - String mImeTokenString; - int mImeDisplayId; - @NonNull - String mImeId; - @StartInputReason - int mStartInputReason; - boolean mRestarting; - @UserIdInt - int mTargetUserId; - int mTargetDisplayId; - @NonNull - String mTargetWindowString; - @NonNull - EditorInfo mEditorInfo; - @SoftInputModeFlags - int mTargetWindowSoftInputMode; - int mClientBindSequenceNumber; - - Entry(@NonNull StartInputInfo original) { - set(original); - } - - void set(@NonNull StartInputInfo original) { - mSequenceNumber = original.mSequenceNumber; - mTimestamp = original.mTimestamp; - mWallTime = original.mWallTime; - mImeUserId = original.mImeUserId; - // Intentionally convert to String so as not to keep a strong reference to a Binder - // object. - mImeTokenString = String.valueOf(original.mImeToken); - mImeDisplayId = original.mImeDisplayId; - mImeId = original.mImeId; - mStartInputReason = original.mStartInputReason; - mRestarting = original.mRestarting; - mTargetUserId = original.mTargetUserId; - mTargetDisplayId = original.mTargetDisplayId; - // Intentionally convert to String so as not to keep a strong reference to a Binder - // object. - mTargetWindowString = String.valueOf(original.mTargetWindow); - mEditorInfo = original.mEditorInfo; - mTargetWindowSoftInputMode = original.mTargetWindowSoftInputMode; - mClientBindSequenceNumber = original.mClientBindSequenceNumber; - } - } - - /** - * Add a new entry and discard the oldest entry as needed. - * @param info {@link StartInputInfo} to be added. - */ - void addEntry(@NonNull StartInputInfo info) { - final int index = mNextIndex; - if (mEntries[index] == null) { - mEntries[index] = new Entry(info); - } else { - mEntries[index].set(info); - } - mNextIndex = (mNextIndex + 1) % mEntries.length; - } - - void dump(@NonNull PrintWriter pw, @NonNull String prefix) { - final DateTimeFormatter formatter = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US) - .withZone(ZoneId.systemDefault()); - - for (int i = 0; i < mEntries.length; ++i) { - final Entry entry = mEntries[(i + mNextIndex) % mEntries.length]; - if (entry == null) { - continue; - } - pw.print(prefix); - pw.println("StartInput #" + entry.mSequenceNumber + ":"); - - pw.print(prefix); - pw.println(" time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime)) - + " (timestamp=" + entry.mTimestamp + ")" - + " reason=" - + InputMethodDebug.startInputReasonToString(entry.mStartInputReason) - + " restarting=" + entry.mRestarting); - - pw.print(prefix); - pw.print(" imeToken=" + entry.mImeTokenString + " [" + entry.mImeId + "]"); - pw.print(" imeUserId=" + entry.mImeUserId); - pw.println(" imeDisplayId=" + entry.mImeDisplayId); - - pw.print(prefix); - pw.println(" targetWin=" + entry.mTargetWindowString - + " [" + entry.mEditorInfo.packageName + "]" - + " targetUserId=" + entry.mTargetUserId - + " targetDisplayId=" + entry.mTargetDisplayId - + " clientBindSeq=" + entry.mClientBindSequenceNumber); - - pw.print(prefix); - pw.println(" softInputMode=" + InputMethodDebug.softInputModeToString( - entry.mTargetWindowSoftInputMode)); - - pw.print(prefix); - pw.println(" inputType=0x" + Integer.toHexString(entry.mEditorInfo.inputType) - + " imeOptions=0x" + Integer.toHexString(entry.mEditorInfo.imeOptions) - + " fieldId=0x" + Integer.toHexString(entry.mEditorInfo.fieldId) - + " fieldName=" + entry.mEditorInfo.fieldName - + " actionId=" + entry.mEditorInfo.actionId - + " actionLabel=" + entry.mEditorInfo.actionLabel); - } - } - } - @GuardedBy("ImfLock.class") @NonNull private final StartInputHistory mStartInputHistory = new StartInputHistory(); diff --git a/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java b/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java new file mode 100644 index 000000000000..3023603dc437 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2024 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.server.inputmethod; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.SystemClock; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; + +import com.android.internal.inputmethod.InputMethodDebug; +import com.android.internal.inputmethod.SoftInputShowHideReason; + +import java.io.PrintWriter; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; + +final class SoftInputShowHideHistory { + private static final AtomicInteger sSequenceNumber = new AtomicInteger(0); + + private final Entry[] mEntries = new Entry[16]; + private int mNextIndex = 0; + + static final class Entry { + final int mSequenceNumber = sSequenceNumber.getAndIncrement(); + @Nullable + final ClientState mClientState; + @WindowManager.LayoutParams.SoftInputModeFlags + final int mFocusedWindowSoftInputMode; + @SoftInputShowHideReason + final int mReason; + // The timing of handling showCurrentInputLocked() or hideCurrentInputLocked(). + final long mTimestamp; + final long mWallTime; + final boolean mInFullscreenMode; + @NonNull + final String mFocusedWindowName; + @Nullable + final EditorInfo mEditorInfo; + @NonNull + final String mRequestWindowName; + @Nullable + final String mImeControlTargetName; + @Nullable + final String mImeTargetNameFromWm; + @Nullable + final String mImeSurfaceParentName; + + Entry(ClientState client, EditorInfo editorInfo, + String focusedWindowName, + @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, + @SoftInputShowHideReason int reason, + boolean inFullscreenMode, String requestWindowName, + @Nullable String imeControlTargetName, @Nullable String imeTargetName, + @Nullable String imeSurfaceParentName) { + mClientState = client; + mEditorInfo = editorInfo; + mFocusedWindowName = focusedWindowName; + mFocusedWindowSoftInputMode = softInputMode; + mReason = reason; + mTimestamp = SystemClock.uptimeMillis(); + mWallTime = System.currentTimeMillis(); + mInFullscreenMode = inFullscreenMode; + mRequestWindowName = requestWindowName; + mImeControlTargetName = imeControlTargetName; + mImeTargetNameFromWm = imeTargetName; + mImeSurfaceParentName = imeSurfaceParentName; + } + } + + void addEntry(@NonNull Entry entry) { + final int index = mNextIndex; + mEntries[index] = entry; + mNextIndex = (mNextIndex + 1) % mEntries.length; + } + + void dump(@NonNull PrintWriter pw, @NonNull String prefix) { + final DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US) + .withZone(ZoneId.systemDefault()); + + for (int i = 0; i < mEntries.length; ++i) { + final Entry entry = mEntries[(i + mNextIndex) % mEntries.length]; + if (entry == null) { + continue; + } + pw.print(prefix); + pw.println("SoftInputShowHide #" + entry.mSequenceNumber + ":"); + + pw.print(prefix); + pw.println(" time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime)) + + " (timestamp=" + entry.mTimestamp + ")"); + + pw.print(prefix); + pw.print(" reason=" + InputMethodDebug.softInputDisplayReasonToString( + entry.mReason)); + pw.println(" inFullscreenMode=" + entry.mInFullscreenMode); + + pw.print(prefix); + pw.println(" requestClient=" + entry.mClientState); + + pw.print(prefix); + pw.println(" focusedWindowName=" + entry.mFocusedWindowName); + + pw.print(prefix); + pw.println(" requestWindowName=" + entry.mRequestWindowName); + + pw.print(prefix); + pw.println(" imeControlTargetName=" + entry.mImeControlTargetName); + + pw.print(prefix); + pw.println(" imeTargetNameFromWm=" + entry.mImeTargetNameFromWm); + + pw.print(prefix); + pw.println(" imeSurfaceParentName=" + entry.mImeSurfaceParentName); + + pw.print(prefix); + pw.print(" editorInfo:"); + if (entry.mEditorInfo != null) { + pw.print(" inputType=" + entry.mEditorInfo.inputType); + pw.print(" privateImeOptions=" + entry.mEditorInfo.privateImeOptions); + pw.println(" fieldId (viewId)=" + entry.mEditorInfo.fieldId); + } else { + pw.println(" null"); + } + + pw.print(prefix); + pw.println(" focusedWindowSoftInputMode=" + InputMethodDebug.softInputModeToString( + entry.mFocusedWindowSoftInputMode)); + } + } +} diff --git a/services/core/java/com/android/server/inputmethod/StartInputHistory.java b/services/core/java/com/android/server/inputmethod/StartInputHistory.java new file mode 100644 index 000000000000..3a39434f2d02 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/StartInputHistory.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2024 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.server.inputmethod; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.ActivityManager; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; + +import com.android.internal.inputmethod.InputMethodDebug; +import com.android.internal.inputmethod.StartInputReason; + +import java.io.PrintWriter; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +/** + * A ring buffer to store the history of {@link StartInputInfo}. + */ +final class StartInputHistory { + /** + * Entry size for non low-RAM devices. + * + * <p>TODO: Consider to follow what other system services have been doing to manage + * constants (e.g. {@link android.provider.Settings.Global#ACTIVITY_MANAGER_CONSTANTS}).</p> + */ + private static final int ENTRY_SIZE_FOR_HIGH_RAM_DEVICE = 32; + + /** + * Entry size for low-RAM devices. + * + * <p>TODO: Consider to follow what other system services have been doing to manage + * constants (e.g. {@link android.provider.Settings.Global#ACTIVITY_MANAGER_CONSTANTS}).</p> + */ + private static final int ENTRY_SIZE_FOR_LOW_RAM_DEVICE = 5; + + private static int getEntrySize() { + if (ActivityManager.isLowRamDeviceStatic()) { + return ENTRY_SIZE_FOR_LOW_RAM_DEVICE; + } else { + return ENTRY_SIZE_FOR_HIGH_RAM_DEVICE; + } + } + + /** + * Backing store for the ring buffer. + */ + private final Entry[] mEntries = new Entry[getEntrySize()]; + + /** + * An index of {@link #mEntries}, to which next + * {@link #addEntry(StartInputInfo)} should + * write. + */ + private int mNextIndex = 0; + + /** + * Recyclable entry to store the information in {@link StartInputInfo}. + */ + private static final class Entry { + int mSequenceNumber; + long mTimestamp; + long mWallTime; + @UserIdInt + int mImeUserId; + @NonNull + String mImeTokenString; + int mImeDisplayId; + @NonNull + String mImeId; + @StartInputReason + int mStartInputReason; + boolean mRestarting; + @UserIdInt + int mTargetUserId; + int mTargetDisplayId; + @NonNull + String mTargetWindowString; + @NonNull + EditorInfo mEditorInfo; + @WindowManager.LayoutParams.SoftInputModeFlags + int mTargetWindowSoftInputMode; + int mClientBindSequenceNumber; + + Entry(@NonNull StartInputInfo original) { + set(original); + } + + void set(@NonNull StartInputInfo original) { + mSequenceNumber = original.mSequenceNumber; + mTimestamp = original.mTimestamp; + mWallTime = original.mWallTime; + mImeUserId = original.mImeUserId; + // Intentionally convert to String so as not to keep a strong reference to a Binder + // object. + mImeTokenString = String.valueOf(original.mImeToken); + mImeDisplayId = original.mImeDisplayId; + mImeId = original.mImeId; + mStartInputReason = original.mStartInputReason; + mRestarting = original.mRestarting; + mTargetUserId = original.mTargetUserId; + mTargetDisplayId = original.mTargetDisplayId; + // Intentionally convert to String so as not to keep a strong reference to a Binder + // object. + mTargetWindowString = String.valueOf(original.mTargetWindow); + mEditorInfo = original.mEditorInfo; + mTargetWindowSoftInputMode = original.mTargetWindowSoftInputMode; + mClientBindSequenceNumber = original.mClientBindSequenceNumber; + } + } + + /** + * Add a new entry and discard the oldest entry as needed. + * + * @param info {@link StartInputInfo} to be added. + */ + void addEntry(@NonNull StartInputInfo info) { + final int index = mNextIndex; + if (mEntries[index] == null) { + mEntries[index] = new Entry(info); + } else { + mEntries[index].set(info); + } + mNextIndex = (mNextIndex + 1) % mEntries.length; + } + + void dump(@NonNull PrintWriter pw, @NonNull String prefix) { + final DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US) + .withZone(ZoneId.systemDefault()); + + for (int i = 0; i < mEntries.length; ++i) { + final Entry entry = mEntries[(i + mNextIndex) % mEntries.length]; + if (entry == null) { + continue; + } + pw.print(prefix); + pw.println("StartInput #" + entry.mSequenceNumber + ":"); + + pw.print(prefix); + pw.println(" time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime)) + + " (timestamp=" + entry.mTimestamp + ")" + + " reason=" + + InputMethodDebug.startInputReasonToString(entry.mStartInputReason) + + " restarting=" + entry.mRestarting); + + pw.print(prefix); + pw.print(" imeToken=" + entry.mImeTokenString + " [" + entry.mImeId + "]"); + pw.print(" imeUserId=" + entry.mImeUserId); + pw.println(" imeDisplayId=" + entry.mImeDisplayId); + + pw.print(prefix); + pw.println(" targetWin=" + entry.mTargetWindowString + + " [" + entry.mEditorInfo.packageName + "]" + + " targetUserId=" + entry.mTargetUserId + + " targetDisplayId=" + entry.mTargetDisplayId + + " clientBindSeq=" + entry.mClientBindSequenceNumber); + + pw.print(prefix); + pw.println(" softInputMode=" + InputMethodDebug.softInputModeToString( + entry.mTargetWindowSoftInputMode)); + + pw.print(prefix); + pw.println(" inputType=0x" + Integer.toHexString(entry.mEditorInfo.inputType) + + " imeOptions=0x" + Integer.toHexString(entry.mEditorInfo.imeOptions) + + " fieldId=0x" + Integer.toHexString(entry.mEditorInfo.fieldId) + + " fieldName=" + entry.mEditorInfo.fieldName + + " actionId=" + entry.mEditorInfo.actionId + + " actionLabel=" + entry.mEditorInfo.actionLabel); + } + } +} diff --git a/services/core/java/com/android/server/inputmethod/StartInputInfo.java b/services/core/java/com/android/server/inputmethod/StartInputInfo.java new file mode 100644 index 000000000000..1cff737d4a00 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/StartInputInfo.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 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.server.inputmethod; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.inputmethodservice.InputMethodService; +import android.os.IBinder; +import android.os.SystemClock; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + +import com.android.internal.inputmethod.IInputMethod; +import com.android.internal.inputmethod.StartInputReason; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Internal state snapshot when + * {@link IInputMethod#startInput(IInputMethod.StartInputParams)} is about to be called. + * + * <p>Calling that IPC endpoint basically means that + * {@link InputMethodService#doStartInput(InputConnection, EditorInfo, boolean)} will be called + * back in the current IME process shortly, which will also affect what the current IME starts + * receiving from {@link InputMethodService#getCurrentInputConnection()}. In other words, this + * snapshot will be taken every time when {@link InputMethodManagerService} is initiating a new + * logical input session between the client application and the current IME.</p> + * + * <p>Be careful to not keep strong references to this object forever, which can prevent + * {@link StartInputInfo#mImeToken} and {@link StartInputInfo#mTargetWindow} from being GC-ed. + * </p> + */ +final class StartInputInfo { + private static final AtomicInteger sSequenceNumber = new AtomicInteger(0); + + final int mSequenceNumber; + final long mTimestamp; + final long mWallTime; + @UserIdInt + final int mImeUserId; + @NonNull + final IBinder mImeToken; + final int mImeDisplayId; + @NonNull + final String mImeId; + @StartInputReason + final int mStartInputReason; + final boolean mRestarting; + @UserIdInt + final int mTargetUserId; + final int mTargetDisplayId; + @Nullable + final IBinder mTargetWindow; + @NonNull + final EditorInfo mEditorInfo; + @WindowManager.LayoutParams.SoftInputModeFlags + final int mTargetWindowSoftInputMode; + final int mClientBindSequenceNumber; + + StartInputInfo(@UserIdInt int imeUserId, @NonNull IBinder imeToken, int imeDisplayId, + @NonNull String imeId, @StartInputReason int startInputReason, boolean restarting, + @UserIdInt int targetUserId, int targetDisplayId, @Nullable IBinder targetWindow, + @NonNull EditorInfo editorInfo, + @WindowManager.LayoutParams.SoftInputModeFlags int targetWindowSoftInputMode, + int clientBindSequenceNumber) { + mSequenceNumber = sSequenceNumber.getAndIncrement(); + mTimestamp = SystemClock.uptimeMillis(); + mWallTime = System.currentTimeMillis(); + mImeUserId = imeUserId; + mImeToken = imeToken; + mImeDisplayId = imeDisplayId; + mImeId = imeId; + mStartInputReason = startInputReason; + mRestarting = restarting; + mTargetUserId = targetUserId; + mTargetDisplayId = targetDisplayId; + mTargetWindow = targetWindow; + mEditorInfo = editorInfo; + mTargetWindowSoftInputMode = targetWindowSoftInputMode; + mClientBindSequenceNumber = clientBindSequenceNumber; + } +} diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index e7455dbad949..c38fbda4f5fd 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -12054,10 +12054,17 @@ public class NotificationManagerService extends SystemService { @Override public void onServiceAdded(ManagedServiceInfo info) { if (lifetimeExtensionRefactor()) { - // We explicitly check the status bar permission for the uid in the info object. - // We can't use the calling uid here because it's probably always system server. - // Note that this will also be true for the shell. - info.isSystemUi = getContext().checkPermission( + // Generally, only System or System UI should have the permissions to call + // registerSystemService. + // isCallerSystemOrPhone tells us whether the caller is System. We negate this, + // to eliminate cases where the service was added by the system. This leaves + // services registered by system server. + // To identify system UI, we explicitly check the status bar permission for the + // uid in the info object. + // We can't use the calling uid here because it belongs to system server. + // Note that this will also return true for the shell, but we deem this + // acceptable, for the purposes of testing. + info.isSystemUi = !isCallerSystemOrPhone() && getContext().checkPermission( android.Manifest.permission.STATUS_BAR_SERVICE, -1, info.uid) == PERMISSION_GRANTED; } diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index c7ebb3c2667b..c6bb99eed7ee 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -2116,6 +2116,18 @@ public class LauncherAppsService extends SystemService { @RequiresPermission(READ_FRAME_BUFFER) @Override + public void saveViewCaptureData() { + int status = checkCallingOrSelfPermissionForPreflight(mContext, READ_FRAME_BUFFER); + if (PERMISSION_GRANTED == status) { + forEachViewCaptureWindow(this::dumpViewCaptureDataToWmTrace); + } else { + Log.w(TAG, "caller lacks permissions to save view capture data"); + } + } + + + @RequiresPermission(READ_FRAME_BUFFER) + @Override public void registerDumpCallback(@NonNull IDumpCallback cb) { int status = checkCallingOrSelfPermissionForPreflight(mContext, READ_FRAME_BUFFER); if (PERMISSION_GRANTED == status) { diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java index c2f74a8895cb..c9fd2610bfb7 100644 --- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java +++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java @@ -155,7 +155,8 @@ public class UserRestrictionsUtils { UserManager.DISALLOW_CONFIG_DEFAULT_APPS, UserManager.DISALLOW_NEAR_FIELD_COMMUNICATION_RADIO, UserManager.DISALLOW_SIM_GLOBALLY, - UserManager.DISALLOW_ASSIST_CONTENT + UserManager.DISALLOW_ASSIST_CONTENT, + UserManager.DISALLOW_THREAD_NETWORK }); public static final Set<String> DEPRECATED_USER_RESTRICTIONS = Sets.newArraySet( @@ -206,7 +207,8 @@ public class UserRestrictionsUtils { UserManager.DISALLOW_ADD_WIFI_CONFIG, UserManager.DISALLOW_CELLULAR_2G, UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO, - UserManager.DISALLOW_NEAR_FIELD_COMMUNICATION_RADIO + UserManager.DISALLOW_NEAR_FIELD_COMMUNICATION_RADIO, + UserManager.DISALLOW_THREAD_NETWORK ); /** @@ -252,7 +254,8 @@ public class UserRestrictionsUtils { UserManager.DISALLOW_ADD_WIFI_CONFIG, UserManager.DISALLOW_CELLULAR_2G, UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO, - UserManager.DISALLOW_NEAR_FIELD_COMMUNICATION_RADIO + UserManager.DISALLOW_NEAR_FIELD_COMMUNICATION_RADIO, + UserManager.DISALLOW_THREAD_NETWORK ); /** diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 5974ac8bd41b..266418fd5b4a 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -4315,6 +4315,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { boolean allowDuringSetup) { if (allowDuringSetup || isUserSetupComplete()) { mContext.startActivityAsUser(intent, bundle, handle); + dismissKeyboardShortcutsMenu(); } else { Slog.i(TAG, "Not starting activity because user setup is in progress: " + intent); } @@ -4365,6 +4366,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (statusbar != null) { statusbar.showRecentApps(triggeredFromAltTab); } + dismissKeyboardShortcutsMenu(); } private void toggleKeyboardShortcutsMenu(int deviceId) { diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index c3efcb14f223..885baf65013f 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -2691,6 +2691,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } float maxDimAmount = getHighestDimAmountFromMap(wallpaper.mUidToDimAmount); + if (wallpaper.mWallpaperDimAmount == maxDimAmount) return; wallpaper.mWallpaperDimAmount = maxDimAmount; // Also set the dim amount to the lock screen wallpaper if the lock and home screen // do not share the same wallpaper diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java index a914c0712319..b616d24cfebb 100644 --- a/services/core/java/com/android/server/wm/ContentRecorder.java +++ b/services/core/java/com/android/server/wm/ContentRecorder.java @@ -107,7 +107,9 @@ final class ContentRecorder implements WindowContainerListener { ContentRecorder(@NonNull DisplayContent displayContent) { this(displayContent, new RemoteMediaProjectionManagerWrapper(displayContent.mDisplayId), - new DisplayManagerFlags().isConnectedDisplayManagementEnabled()); + new DisplayManagerFlags().isConnectedDisplayManagementEnabled() + && !new DisplayManagerFlags() + .isPixelAnisotropyCorrectionInLogicalDisplayEnabled()); } @VisibleForTesting diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java index 877378cac06a..a29cb60ff545 100644 --- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java +++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java @@ -173,18 +173,25 @@ public class DeferredDisplayUpdater implements DisplayUpdater { mDisplayContent.mInitialDisplayHeight); final int fromRotation = mDisplayContent.getRotation(); - onStartCollect.run(); - - ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS, - "DeferredDisplayUpdater: applied DisplayInfo after deferring"); - - if (physicalDisplayUpdated) { - onDisplayUpdated(transition, fromRotation, startBounds); - } else { - final TransitionRequestInfo.DisplayChange displayChange = - getCurrentDisplayChange(fromRotation, startBounds); - mDisplayContent.mTransitionController.requestStartTransition(transition, - /* startTask= */ null, /* remoteTransition= */ null, displayChange); + mDisplayContent.mAtmService.deferWindowLayout(); + try { + onStartCollect.run(); + + ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS, + "DeferredDisplayUpdater: applied DisplayInfo after deferring"); + + if (physicalDisplayUpdated) { + onDisplayUpdated(transition, fromRotation, startBounds); + } else { + final TransitionRequestInfo.DisplayChange displayChange = + getCurrentDisplayChange(fromRotation, startBounds); + mDisplayContent.mTransitionController.requestStartTransition(transition, + /* startTask= */ null, /* remoteTransition= */ null, displayChange); + } + } finally { + // Run surface placement after requestStartTransition, so shell side can receive + // the transition request before handling task info changes. + mDisplayContent.mAtmService.continueWindowLayout(); } }); } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 282ecc77bd50..837d08b33756 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -6210,7 +6210,12 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp * @param onDisplayChangeApplied callback that is called when the changes are applied */ void requestDisplayUpdate(@NonNull Runnable onDisplayChangeApplied) { - mDisplayUpdater.updateDisplayInfo(onDisplayChangeApplied); + mAtmService.deferWindowLayout(); + try { + mDisplayUpdater.updateDisplayInfo(onDisplayChangeApplied); + } finally { + mAtmService.continueWindowLayout(); + } } void onDisplayInfoUpdated(@NonNull DisplayInfo newDisplayInfo) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index c37946b4d750..ee72db036bbf 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -3395,7 +3395,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (shouldMigrateV1ToDevicePolicyEngine()) { migrateV1PoliciesToDevicePolicyEngine(); } + maybeMigratePoliciesPostUpgradeToDevicePolicyEngineLocked(); migratePoliciesToPolicyEngineLocked(); + } maybeStartSecurityLogMonitorOnActivityManagerReady(); break; @@ -16877,6 +16879,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { private int checkDeviceOwnerProvisioningPreCondition(@UserIdInt int callingUserId) { synchronized (getLockObject()) { final int deviceOwnerUserId = mInjector.userManagerIsHeadlessSystemUserMode() + && (!Flags.headlessDeviceOwnerProvisioningFixEnabled() + || getHeadlessDeviceOwnerMode() == HEADLESS_DEVICE_OWNER_MODE_AFFILIATED) ? UserHandle.USER_SYSTEM : callingUserId; Slogf.i(LOG_TAG, "Calling user %d, device owner will be set on user %d", @@ -21549,10 +21553,21 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { setTimeAndTimezone(provisioningParams.getTimeZone(), provisioningParams.getLocalTime()); setLocale(provisioningParams.getLocale()); + + + boolean isSingleUserMode; + if (Flags.headlessDeviceOwnerProvisioningFixEnabled()) { + DeviceAdminInfo adminInfo = findAdmin( + deviceAdmin, caller.getUserId(), /* throwForMissingPermission= */ false); + isSingleUserMode = (adminInfo != null && adminInfo.getHeadlessDeviceOwnerMode() + == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER); + } else { + isSingleUserMode = + (getHeadlessDeviceOwnerMode() == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER); + } int deviceOwnerUserId = Flags.headlessDeviceOwnerSingleUserEnabled() - && getHeadlessDeviceOwnerMode() == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER - ? mUserManagerInternal.getMainUserId() - : UserHandle.USER_SYSTEM; + && isSingleUserMode + ? mUserManagerInternal.getMainUserId() : UserHandle.USER_SYSTEM; if (!removeNonRequiredAppsForManagedDevice( deviceOwnerUserId, @@ -23736,7 +23751,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (!canForceMigration && !shouldMigrateV1ToDevicePolicyEngine()) { return false; } - return migrateV1PoliciesToDevicePolicyEngine(); + boolean migrated = migrateV1PoliciesToDevicePolicyEngine(); + migrated &= migratePoliciesPostUpgradeToDevicePolicyEngineLocked(); + return migrated; }); } @@ -23765,6 +23782,30 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { /** * Migrates the initial set of policies to use policy engine. + * [b/318497672] Migrate policies that weren't migrated properly in the initial migration on + * update from Android T to Android U + */ + private void maybeMigratePoliciesPostUpgradeToDevicePolicyEngineLocked() { + if (!mOwners.isMigratedToPolicyEngine() || mOwners.isMigratedPostUpdate()) { + return; + } + migratePoliciesPostUpgradeToDevicePolicyEngineLocked(); + mOwners.markPostUpgradeMigration(); + } + + private boolean migratePoliciesPostUpgradeToDevicePolicyEngineLocked() { + try { + migrateScreenCapturePolicyLocked(); + migrateLockTaskPolicyLocked(); + return true; + } catch (Exception e) { + Slogf.e(LOG_TAG, e, "Error occurred during post upgrade migration to the device " + + "policy engine."); + return false; + } + } + + /** * @return {@code true} if policies were migrated successfully, {@code false} otherwise. */ private boolean migrateV1PoliciesToDevicePolicyEngine() { @@ -23777,7 +23818,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { migrateAutoTimezonePolicy(); migratePermissionGrantStatePolicies(); } - migrateScreenCapturePolicyLocked(); migratePermittedInputMethodsPolicyLocked(); migrateAccountManagementDisabledPolicyLocked(); migrateUserControlDisabledPackagesLocked(); @@ -23858,14 +23898,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { private void migrateScreenCapturePolicyLocked() { Binder.withCleanCallingIdentity(() -> { - if (mPolicyCache.getScreenCaptureDisallowedUser() == UserHandle.USER_NULL) { - return; - } ActiveAdmin admin = getDeviceOwnerOrProfileOwnerOfOrganizationOwnedDeviceLocked(); if (admin != null && ((isDeviceOwner(admin) && admin.disableScreenCapture) || (admin.getParentActiveAdmin() != null && admin.getParentActiveAdmin().disableScreenCapture))) { + EnforcingAdmin enforcingAdmin = EnforcingAdmin.createEnterpriseEnforcingAdmin( admin.info.getComponent(), admin.getUserHandle().getIdentifier(), @@ -23894,6 +23932,48 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { }); } + private void migrateLockTaskPolicyLocked() { + Binder.withCleanCallingIdentity(() -> { + ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked(); + if (deviceOwner != null) { + int doUserId = deviceOwner.getUserHandle().getIdentifier(); + DevicePolicyData policies = getUserData(doUserId); + List<String> packages = policies.mLockTaskPackages; + int features = policies.mLockTaskFeatures; + // TODO: find out about persistent preferred activities + if (!packages.isEmpty()) { + setLockTaskPolicyInPolicyEngine(deviceOwner, doUserId, packages, features); + } + } + + for (int userId : mUserManagerInternal.getUserIds()) { + ActiveAdmin profileOwner = getProfileOwnerLocked(userId); + if (profileOwner != null && canDPCManagedUserUseLockTaskLocked(userId)) { + DevicePolicyData policies = getUserData(userId); + List<String> packages = policies.mLockTaskPackages; + int features = policies.mLockTaskFeatures; + if (!packages.isEmpty()) { + setLockTaskPolicyInPolicyEngine(profileOwner, userId, packages, features); + } + } + } + }); + } + + private void setLockTaskPolicyInPolicyEngine( + ActiveAdmin admin, int userId, List<String> packages, int features) { + EnforcingAdmin enforcingAdmin = + EnforcingAdmin.createEnterpriseEnforcingAdmin( + admin.info.getComponent(), + userId, + admin); + mDevicePolicyEngine.setLocalPolicy( + PolicyDefinition.LOCK_TASK, + enforcingAdmin, + new LockTaskPolicy(new HashSet<>(packages), features), + userId); + } + private void migratePermittedInputMethodsPolicyLocked() { Binder.withCleanCallingIdentity(() -> { List<UserInfo> users = mUserManager.getUsers(); @@ -24256,4 +24336,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return mDevicePolicyEngine.getMaxPolicyStorageLimit(); } + + @Override + public int getHeadlessDeviceOwnerMode(String callerPackageName) { + final CallerIdentity caller = getCallerIdentity(callerPackageName); + enforcePermission(MANAGE_PROFILE_AND_DEVICE_OWNERS, caller.getPackageName(), + caller.getUserId()); + + return Binder.withCleanCallingIdentity(() -> getHeadlessDeviceOwnerMode()); + } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java index c5a98880ec84..7912cbce554f 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java @@ -623,12 +623,25 @@ class Owners { } } + void markPostUpgradeMigration() { + synchronized (mData) { + mData.mPoliciesMigratedPostUpdate = true; + mData.writeDeviceOwner(); + } + } + boolean isSecurityLoggingMigrated() { synchronized (mData) { return mData.mSecurityLoggingMigrated; } } + boolean isMigratedPostUpdate() { + synchronized (mData) { + return mData.mPoliciesMigratedPostUpdate; + } + } + @GuardedBy("mData") void pushToAppOpsLocked() { if (!mSystemReady) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java index 9d73ed0070c8..42ac998bf96c 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java @@ -89,6 +89,8 @@ class OwnersData { private static final String ATTR_MIGRATED_TO_POLICY_ENGINE = "migratedToPolicyEngine"; private static final String ATTR_SECURITY_LOG_MIGRATED = "securityLogMigrated"; + private static final String ATTR_MIGRATED_POST_UPGRADE = "migratedPostUpgrade"; + // Internal state for the device owner package. OwnerInfo mDeviceOwner; int mDeviceOwnerUserId = UserHandle.USER_NULL; @@ -117,6 +119,8 @@ class OwnersData { boolean mMigratedToPolicyEngine = false; boolean mSecurityLoggingMigrated = false; + boolean mPoliciesMigratedPostUpdate = false; + OwnersData(PolicyPathProvider pathProvider) { mPathProvider = pathProvider; } @@ -400,6 +404,7 @@ class OwnersData { out.startTag(null, TAG_POLICY_ENGINE_MIGRATION); out.attributeBoolean(null, ATTR_MIGRATED_TO_POLICY_ENGINE, mMigratedToPolicyEngine); + out.attributeBoolean(null, ATTR_MIGRATED_POST_UPGRADE, mPoliciesMigratedPostUpdate); if (Flags.securityLogV2Enabled()) { out.attributeBoolean(null, ATTR_SECURITY_LOG_MIGRATED, mSecurityLoggingMigrated); } @@ -463,8 +468,11 @@ class OwnersData { case TAG_POLICY_ENGINE_MIGRATION: mMigratedToPolicyEngine = parser.getAttributeBoolean( null, ATTR_MIGRATED_TO_POLICY_ENGINE, false); + mPoliciesMigratedPostUpdate = parser.getAttributeBoolean( + null, ATTR_MIGRATED_POST_UPGRADE, false); mSecurityLoggingMigrated = Flags.securityLogV2Enabled() && parser.getAttributeBoolean(null, ATTR_SECURITY_LOG_MIGRATED, false); + break; default: Slog.e(TAG, "Unexpected tag: " + tag); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java index 71facab99fce..e713a827fc76 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java @@ -506,6 +506,10 @@ final class PolicyDefinition<V> { UserManager.DISALLOW_SIM_GLOBALLY, POLICY_FLAG_GLOBAL_ONLY_POLICY); USER_RESTRICTION_FLAGS.put(UserManager.DISALLOW_ASSIST_CONTENT, /* flags= */ 0); + if (com.android.net.thread.platform.flags.Flags.threadUserRestrictionEnabled()) { + USER_RESTRICTION_FLAGS.put( + UserManager.DISALLOW_THREAD_NETWORK, POLICY_FLAG_GLOBAL_ONLY_POLICY); + } for (String key : USER_RESTRICTION_FLAGS.keySet()) { createAndAddUserRestrictionPolicyDefinition(key, USER_RESTRICTION_FLAGS.get(key)); diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 2112dae0f311..7c669f1aeb35 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1134,7 +1134,7 @@ public final class SystemServer implements Dumpable { ServiceManager.addService(Context.PLATFORM_COMPAT_SERVICE, platformCompat); ServiceManager.addService(Context.PLATFORM_COMPAT_NATIVE_SERVICE, new PlatformCompatNative(platformCompat)); - AppCompatCallbacks.install(new long[0]); + AppCompatCallbacks.install(new long[0], new long[0]); t.traceEnd(); // FileIntegrityService responds to requests from apps and the system. It needs to run after @@ -2166,14 +2166,16 @@ public final class SystemServer implements Dumpable { } t.traceEnd(); - t.traceBegin("StartVcnManagementService"); - try { - vcnManagement = VcnManagementService.create(context); - ServiceManager.addService(Context.VCN_MANAGEMENT_SERVICE, vcnManagement); - } catch (Throwable e) { - reportWtf("starting VCN Management Service", e); + if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) { + t.traceBegin("StartVcnManagementService"); + try { + vcnManagement = VcnManagementService.create(context); + ServiceManager.addService(Context.VCN_MANAGEMENT_SERVICE, vcnManagement); + } catch (Throwable e) { + reportWtf("starting VCN Management Service", e); + } + t.traceEnd(); } - t.traceEnd(); t.traceBegin("StartSystemUpdateManagerService"); try { diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java index a33e52f0dd75..e5d315358df6 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java @@ -91,8 +91,8 @@ public final class InputMethodManagerServiceTests { @Test public void testSoftInputShowHideHistoryDump_withNulls_doesntThrow() { var writer = new StringWriter(); - var history = new InputMethodManagerService.SoftInputShowHideHistory(); - history.addEntry(new InputMethodManagerService.SoftInputShowHideHistory.Entry( + var history = new SoftInputShowHideHistory(); + history.addEntry(new SoftInputShowHideHistory.Entry( null, null, null, diff --git a/services/tests/VpnTests/Android.bp b/services/tests/VpnTests/Android.bp index 64a9a3b4f119..a5011a8d8b00 100644 --- a/services/tests/VpnTests/Android.bp +++ b/services/tests/VpnTests/Android.bp @@ -17,8 +17,7 @@ android_test { "java/**/*.java", "java/**/*.kt", ], - - defaults: ["framework-connectivity-test-defaults"], + sdk_version: "core_platform", // tests can use @CorePlatformApi's test_suites: ["device-tests"], static_libs: [ "androidx.test.rules", @@ -32,6 +31,13 @@ android_test { "service-connectivity-tiramisu-pre-jarjar", ], libs: [ + // order matters: classes in framework-connectivity are resolved before framework, + // meaning @hide APIs in framework-connectivity are resolved before @SystemApi + // stubs in framework + "framework-connectivity.impl", + "framework-connectivity-t.impl", + "framework", + "framework-res", "android.test.runner", "android.test.base", "android.test.mock", diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java index dc6abf1981c0..1c71abc984df 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java @@ -52,7 +52,9 @@ public class DisplayDeviceTest { private static final int WIDTH = 500; private static final int HEIGHT = 900; private static final Point PORTRAIT_SIZE = new Point(WIDTH, HEIGHT); + private static final Point PORTRAIT_DOUBLE_WIDTH = new Point(2 * WIDTH, HEIGHT); private static final Point LANDSCAPE_SIZE = new Point(HEIGHT, WIDTH); + private static final Point LANDSCAPE_DOUBLE_HEIGHT = new Point(HEIGHT, 2 * WIDTH); @Mock private SurfaceControl.Transaction mMockTransaction; @@ -69,6 +71,16 @@ public class DisplayDeviceTest { } @Test + public void testGetDisplaySurfaceDefaultSizeLocked_notRotated_anisotropyCorrection() { + mDisplayDeviceInfo.xDpi = 0.5f; + mDisplayDeviceInfo.yDpi = 1.0f; + DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo, + mMockDisplayAdapter, /*isAnisotropyCorrectionEnabled=*/ true); + assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo( + PORTRAIT_DOUBLE_WIDTH); + } + + @Test public void testGetDisplaySurfaceDefaultSizeLocked_notRotated() { DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo, mMockDisplayAdapter); @@ -84,6 +96,17 @@ public class DisplayDeviceTest { } @Test + public void testGetDisplaySurfaceDefaultSizeLocked_rotation90_anisotropyCorrection() { + mDisplayDeviceInfo.xDpi = 0.5f; + mDisplayDeviceInfo.yDpi = 1.0f; + DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo, + mMockDisplayAdapter, /*isAnisotropyCorrectionEnabled=*/ true); + displayDevice.setProjectionLocked(mMockTransaction, ROTATION_90, new Rect(), new Rect()); + assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo( + LANDSCAPE_DOUBLE_HEIGHT); + } + + @Test public void testGetDisplaySurfaceDefaultSizeLocked_rotation90() { DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo, mMockDisplayAdapter); @@ -111,8 +134,14 @@ public class DisplayDeviceTest { private final DisplayDeviceInfo mDisplayDeviceInfo; FakeDisplayDevice(DisplayDeviceInfo displayDeviceInfo, DisplayAdapter displayAdapter) { + this(displayDeviceInfo, displayAdapter, /*isAnisotropyCorrectionEnabled=*/ false); + } + + FakeDisplayDevice(DisplayDeviceInfo displayDeviceInfo, DisplayAdapter displayAdapter, + boolean isAnisotropyCorrectionEnabled) { super(displayAdapter, /* displayToken= */ null, /* uniqueId= */ "", - InstrumentationRegistry.getInstrumentation().getContext()); + InstrumentationRegistry.getInstrumentation().getContext(), + isAnisotropyCorrectionEnabled); mDisplayDeviceInfo = displayDeviceInfo; } diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java index 1c43418f9276..549f0d74b67b 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java @@ -106,8 +106,184 @@ public class LogicalDisplayTest { } @Test + public void testLetterbox() { + mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice, + /*isAnisotropyCorrectionEnabled=*/ false); + mDisplayDeviceInfo.xDpi = 0.5f; + mDisplayDeviceInfo.yDpi = 1.0f; + + mLogicalDisplay.updateLocked(mDeviceRepo); + var originalDisplayInfo = mLogicalDisplay.getDisplayInfoLocked(); + assertEquals(DISPLAY_WIDTH, originalDisplayInfo.logicalWidth); + assertEquals(DISPLAY_HEIGHT, originalDisplayInfo.logicalHeight); + + SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); + mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false); + assertEquals(new Point(0, 0), mLogicalDisplay.getDisplayPosition()); + + /* + * Content is too wide, should become letterboxed + * ______DISPLAY_WIDTH________ + * | | + * |________________________| + * | | + * | CONTENT | + * | | + * |________________________| + * | | + * |________________________| + */ + // Make a wide application content, by reducing its height. + DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = DISPLAY_WIDTH; + displayInfo.logicalHeight = DISPLAY_HEIGHT / 2; + mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo); + + mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false); + assertEquals(new Point(0, DISPLAY_HEIGHT / 4), mLogicalDisplay.getDisplayPosition()); + } + + @Test + public void testNoLetterbox_anisotropyCorrection() { + mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice, + /*isAnisotropyCorrectionEnabled=*/ true); + + // In case of Anisotropy of pixels, then the content should be rescaled so it would adjust + // to using the whole screen. This is because display will rescale it back to fill the + // screen (in case the display menu setting is set to stretch the pixels across the display) + mDisplayDeviceInfo.xDpi = 0.5f; + mDisplayDeviceInfo.yDpi = 1.0f; + + mLogicalDisplay.updateLocked(mDeviceRepo); + var originalDisplayInfo = mLogicalDisplay.getDisplayInfoLocked(); + // Content width re-scaled + assertEquals(DISPLAY_WIDTH * 2, originalDisplayInfo.logicalWidth); + assertEquals(DISPLAY_HEIGHT, originalDisplayInfo.logicalHeight); + + SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); + mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false); + + // Applications need to think that they are shown on a display with square pixels. + // as applications can be displayed on multiple displays simultaneously (mirrored). + // Content is too wide, should have become letterboxed - but it won't because of anisotropy + // correction + assertEquals(new Point(0, 0), mLogicalDisplay.getDisplayPosition()); + } + + @Test + public void testLetterbox_anisotropyCorrectionYDpi() { + mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice, + /*isAnisotropyCorrectionEnabled=*/ true); + + DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = DISPLAY_WIDTH; + displayInfo.logicalHeight = DISPLAY_HEIGHT / 2; + mDisplayDeviceInfo.xDpi = 1.0f; + mDisplayDeviceInfo.yDpi = 0.5f; + mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo); + mLogicalDisplay.updateLocked(mDeviceRepo); + + SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); + mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false); + + assertEquals(new Point(0, 75), mLogicalDisplay.getDisplayPosition()); + } + + @Test + public void testPillarbox() { + mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice, + /*isAnisotropyCorrectionEnabled=*/ false); + mDisplayDeviceInfo.xDpi = 0.5f; + mDisplayDeviceInfo.yDpi = 1.0f; + + DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.rotation = Surface.ROTATION_90; + displayInfo.logicalWidth = DISPLAY_WIDTH; + displayInfo.logicalHeight = DISPLAY_HEIGHT; + mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT; + mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo); + mLogicalDisplay.updateLocked(mDeviceRepo); + + var updatedDisplayInfo = mLogicalDisplay.getDisplayInfoLocked(); + assertEquals(Surface.ROTATION_90, updatedDisplayInfo.rotation); + assertEquals(DISPLAY_WIDTH, updatedDisplayInfo.logicalWidth); + assertEquals(DISPLAY_HEIGHT, updatedDisplayInfo.logicalHeight); + + /* + * Content is too tall, should become pillarboxed + * ______DISPLAY_WIDTH________ + * | | | | + * | | | | + * | | | | + * | | CONTENT | | + * | | | | + * | | | | + * |____|________________|____| + */ + + SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); + mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false); + + assertEquals(new Point(75, 0), mLogicalDisplay.getDisplayPosition()); + } + + @Test + public void testPillarbox_anisotropyCorrection() { + mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice, + /*isAnisotropyCorrectionEnabled=*/ true); + + DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = DISPLAY_WIDTH; + displayInfo.logicalHeight = DISPLAY_HEIGHT; + displayInfo.rotation = Surface.ROTATION_90; + mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT; + // In case of Anisotropy of pixels, then the content should be rescaled so it would adjust + // to using the whole screen. This is because display will rescale it back to fill the + // screen (in case the display menu setting is set to stretch the pixels across the display) + mDisplayDeviceInfo.xDpi = 0.5f; + mDisplayDeviceInfo.yDpi = 1.0f; + mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo); + mLogicalDisplay.updateLocked(mDeviceRepo); + + SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); + mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false); + + // Applications need to think that they are shown on a display with square pixels. + // as applications can be displayed on multiple displays simultaneously (mirrored). + // Content is a bit wider than in #testPillarbox, due to content added stretching + assertEquals(new Point(50, 0), mLogicalDisplay.getDisplayPosition()); + } + + @Test + public void testNoPillarbox_anisotropyCorrectionYDpi() { + mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice, + /*isAnisotropyCorrectionEnabled=*/ true); + + // In case of Anisotropy of pixels, then the content should be rescaled so it would adjust + // to using the whole screen. This is because display will rescale it back to fill the + // screen (in case the display menu setting is set to stretch the pixels across the display) + mDisplayDeviceInfo.xDpi = 1.0f; + mDisplayDeviceInfo.yDpi = 0.5f; + + mLogicalDisplay.updateLocked(mDeviceRepo); + var originalDisplayInfo = mLogicalDisplay.getDisplayInfoLocked(); + // Content width re-scaled + assertEquals(DISPLAY_WIDTH, originalDisplayInfo.logicalWidth); + assertEquals(DISPLAY_HEIGHT * 2, originalDisplayInfo.logicalHeight); + + SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); + mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false); + + // Applications need to think that they are shown on a display with square pixels. + // as applications can be displayed on multiple displays simultaneously (mirrored). + // Content is too tall, should have occupy the whole screen - but it won't because of + // anisotropy correction + assertEquals(new Point(0, 0), mLogicalDisplay.getDisplayPosition()); + } + + @Test public void testGetDisplayPosition() { - Point expectedPosition = new Point(); + Point expectedPosition = new Point(0, 0); SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false); diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java index 64076e604414..3eced7fa025c 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java @@ -1631,12 +1631,25 @@ public class DisplayModeDirectorTest { director.start(sensorManager); director.injectSupportedModesByDisplay(supportedModesByDisplay); - setPeakRefreshRate(Float.POSITIVE_INFINITY); + // Disable Smooth Display + setPeakRefreshRate(RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE); Vote vote1 = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); Vote vote2 = director.getVote(DISPLAY_ID_2, Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); + assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0, + /* frameRateHigh= */ RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE); + assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0, + /* frameRateHigh= */ RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE); + + // Enable Smooth Display + setPeakRefreshRate(Float.POSITIVE_INFINITY); + + vote1 = director.getVote(DISPLAY_ID, + Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); + vote2 = director.getVote(DISPLAY_ID_2, + Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0, /* frameRateHigh= */ 130); assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0, /* frameRateHigh= */ 140); } @@ -1654,10 +1667,18 @@ public class DisplayModeDirectorTest { SensorManager sensorManager = createMockSensorManager(lightSensor); director.start(sensorManager); - setPeakRefreshRate(peakRefreshRate); + // Disable Smooth Display + setPeakRefreshRate(RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE); Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, + /* frameRateHigh= */ RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE); + + // Enable Smooth Display + setPeakRefreshRate(peakRefreshRate); + + vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); + assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, /* frameRateHigh= */ peakRefreshRate); } @@ -1759,11 +1780,23 @@ public class DisplayModeDirectorTest { director.start(sensorManager); director.injectSupportedModesByDisplay(supportedModesByDisplay); - setMinRefreshRate(Float.POSITIVE_INFINITY); + // Disable Force Peak Refresh Rate + setMinRefreshRate(0); Vote vote1 = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); Vote vote2 = director.getVote(DISPLAY_ID_2, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); + assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0, + /* frameRateHigh= */ Float.POSITIVE_INFINITY); + assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0, + /* frameRateHigh= */ Float.POSITIVE_INFINITY); + + // Enable Force Peak Refresh Rate + setMinRefreshRate(Float.POSITIVE_INFINITY); + + vote1 = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); + vote2 = director.getVote(DISPLAY_ID_2, + Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 130, /* frameRateHigh= */ Float.POSITIVE_INFINITY); assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 140, @@ -1783,9 +1816,17 @@ public class DisplayModeDirectorTest { SensorManager sensorManager = createMockSensorManager(lightSensor); director.start(sensorManager); - setMinRefreshRate(minRefreshRate); + // Disable Force Peak Refresh Rate + setMinRefreshRate(0); Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); + assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, + /* frameRateHigh= */ Float.POSITIVE_INFINITY); + + // Enable Force Peak Refresh Rate + setMinRefreshRate(minRefreshRate); + + vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ minRefreshRate, /* frameRateHigh= */ Float.POSITIVE_INFINITY); } @@ -1829,6 +1870,58 @@ public class DisplayModeDirectorTest { } @Test + public void testPeakAndMinRefreshRate_FlagEnabled_DisplayWithOneMode() { + when(mDisplayManagerFlags.isBackUpSmoothDisplayAndForcePeakRefreshRateEnabled()) + .thenReturn(true); + DisplayModeDirector director = + new DisplayModeDirector(mContext, mHandler, mInjector, mDisplayManagerFlags); + director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON); + + Display.Mode[] modes1 = new Display.Mode[] { + new Display.Mode(/* modeId= */ 1, /* width= */ 1280, /* height= */ 720, + /* refreshRate= */ 60), + new Display.Mode(/* modeId= */ 2, /* width= */ 1280, /* height= */ 720, + /* refreshRate= */ 130), + }; + Display.Mode[] modes2 = new Display.Mode[] { + new Display.Mode(/* modeId= */ 1, /* width= */ 1280, /* height= */ 720, + /* refreshRate= */ 60), + }; + SparseArray<Display.Mode[]> supportedModesByDisplay = new SparseArray<>(); + supportedModesByDisplay.put(DISPLAY_ID, modes1); + supportedModesByDisplay.put(DISPLAY_ID_2, modes2); + + Sensor lightSensor = createLightSensor(); + SensorManager sensorManager = createMockSensorManager(lightSensor); + director.start(sensorManager); + director.injectSupportedModesByDisplay(supportedModesByDisplay); + + // Disable Force Peak Refresh Rate and Smooth Display + setMinRefreshRate(0); + setPeakRefreshRate(RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE); + + // Even though the highest refresh rate of the second display == the current min refresh + // rate == 60, Force Peak Refresh Rate should remain disabled + Vote vote1 = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); + Vote vote2 = director.getVote(DISPLAY_ID_2, + Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); + assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0, + /* frameRateHigh= */ Float.POSITIVE_INFINITY); + assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0, + /* frameRateHigh= */ Float.POSITIVE_INFINITY); + + // Even though the highest refresh rate of the second display == the current peak refresh + // rate == 60, Smooth Display should remain disabled + vote1 = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); + vote2 = director.getVote(DISPLAY_ID_2, + Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); + assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0, + /* frameRateHigh= */ RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE); + assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0, + /* frameRateHigh= */ RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE); + } + + @Test public void testSensorRegistration() { // First, configure brightness zones or DMD won't register for sensor data. final FakeDeviceConfig config = mInjector.getDeviceConfig(); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java index caa08647628e..a8b792e30485 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java @@ -213,7 +213,7 @@ public class AsyncProcessStartTest { any(), any(), any(), any(), any(), any(), any(), - any(), + any(), any(), anyLong(), anyLong()); final ProcessRecord r = spy(new ProcessRecord(mAms, ai, ai.processName, ai.uid)); @@ -277,7 +277,7 @@ public class AsyncProcessStartTest { null, null, null, null, null, null, - null, null, + null, null, null, 0, 0); // Sleep until timeout should have triggered diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index a2756ffc9add..0ba74c62b9fe 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -1413,6 +1413,9 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { final BroadcastRecord userPresentRecord2 = makeBroadcastRecord(userPresent); mImpl.enqueueBroadcastLocked(userPresentRecord1); + // Wait for a few ms before sending another broadcast to allow comparing the + // enqueue timestamps of these broadcasts. + SystemClock.sleep(5); mImpl.enqueueBroadcastLocked(userPresentRecord2); final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN, diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java index fcf761fb6607..67be93b3b49f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java @@ -215,7 +215,7 @@ public class ProcessObserverTest { any(), any(), any(), any(), any(), any(), any(), - any(), + any(), any(), anyLong(), anyLong()); final ProcessRecord r = spy(new ProcessRecord(mAms, ai, ai.processName, ai.uid)); r.setPid(myPid()); @@ -263,7 +263,7 @@ public class ProcessObserverTest { null, null, null, null, null, null, - null, null, + null, null, null, 0, 0); return app; } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java index e168596b8eb2..16d05b157727 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java @@ -214,7 +214,7 @@ public class AbstractAccessibilityServiceConnectionTest { .thenReturn(mA11yWindowInfos.get(0)); when(mMockA11yWindowManager.findA11yWindowInfoByIdLocked(PIP_WINDOWID)) .thenReturn(mA11yWindowInfos.get(1)); - when(mMockA11yWindowManager.getDisplayIdByUserIdAndWindowIdLocked(USER_ID, + when(mMockA11yWindowManager.getDisplayIdByUserIdAndWindowId(USER_ID, WINDOWID_ONSECONDDISPLAY)).thenReturn(SECONDARY_DISPLAY_ID); when(mMockA11yWindowManager.getWindowListLocked(SECONDARY_DISPLAY_ID)) .thenReturn(mA11yWindowInfosOnSecondDisplay); diff --git a/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java b/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java index ef15f60101d4..36b163ec84b6 100644 --- a/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java @@ -173,6 +173,25 @@ public class CompatConfigTest { } @Test + public void testGetLoggableChanges() throws Exception { + final long disabledChangeId = 1234L; + final long enabledLatestChangeId = 2345L; + final long enabledOlderChangeId = 3456L; + CompatConfig compatConfig = CompatConfigBuilder.create(mBuildClassifier, mContext) + // Disabled changes should not be logged. + .addDisabledChangeWithId(disabledChangeId) + // A change targeting the latest sdk should be logged. + .addEnableSinceSdkChangeWithId(3, enabledLatestChangeId) + // A change targeting an old sdk should not be logged. + .addEnableSinceSdkChangeWithId(1, enabledOlderChangeId) + .build(); + + assertThat(compatConfig.getLoggableChanges( + ApplicationInfoBuilder.create().withTargetSdk(3).build())) + .asList().containsExactly(enabledLatestChangeId); + } + + @Test public void testPackageOverrideEnabled() throws Exception { CompatConfig compatConfig = CompatConfigBuilder.create(mBuildClassifier, mContext) .addDisabledChangeWithId(1234L) diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java index 1dd64ffa5dde..5582e13cbb4d 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java @@ -145,6 +145,7 @@ public class DevicePolicyManagerServiceMigrationTest extends DpmTestBase { @SmallTest @Test + @Ignore("b/277916462") public void testCompMigrationUnAffiliated_skipped() throws Exception { prepareAdmin1AsDo(); prepareAdminAnotherPackageAsPo(COPE_PROFILE_USER_ID); @@ -216,6 +217,7 @@ public class DevicePolicyManagerServiceMigrationTest extends DpmTestBase { @SmallTest @Test + @Ignore("b/277916462") public void testCompMigration_keepSuspendedAppsWhenDpcIsRPlus() throws Exception { prepareAdmin1AsDo(); prepareAdmin1AsPo(COPE_PROFILE_USER_ID, Build.VERSION_CODES.R); @@ -249,6 +251,7 @@ public class DevicePolicyManagerServiceMigrationTest extends DpmTestBase { @SmallTest @Test + @Ignore("b/277916462") public void testCompMigration_unsuspendAppsWhenDpcNotRPlus() throws Exception { prepareAdmin1AsDo(); prepareAdmin1AsPo(COPE_PROFILE_USER_ID, Build.VERSION_CODES.Q); diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index dc504cac2e12..a35a35acb6c1 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -153,7 +153,8 @@ public class UsageStatsService extends SystemService implements = SystemProperties.getBoolean("persist.debug.time_correction", true); private static final boolean USE_DEDICATED_HANDLER_THREAD = - SystemProperties.getBoolean("persist.debug.use_dedicated_handler_thread", false); + SystemProperties.getBoolean("persist.debug.use_dedicated_handler_thread", + Flags.useDedicatedHandlerThread()); static final boolean DEBUG = false; // Never submit with true static final boolean DEBUG_RESPONSE_STATS = DEBUG || Log.isLoggable(TAG, Log.DEBUG); diff --git a/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl b/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl index e69b60b3a37c..c3495996a231 100644 --- a/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl +++ b/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl @@ -26,5 +26,5 @@ oneway interface IQualifiedNetworksServiceCallback { void onQualifiedNetworkTypesChanged(int apnTypes, in int[] qualifiedNetworkTypes); void onNetworkValidationRequested(int networkCapability, IIntegerConsumer callback); - void onReconnectQualifedNetworkType(int apnTypes, int qualifiedNetworkType); + void onReconnectQualifiedNetworkType(int apnTypes, int qualifiedNetworkType); } diff --git a/telephony/java/android/telephony/data/QualifiedNetworksService.java b/telephony/java/android/telephony/data/QualifiedNetworksService.java index 7bfe04d025c8..f775de6ebef4 100644 --- a/telephony/java/android/telephony/data/QualifiedNetworksService.java +++ b/telephony/java/android/telephony/data/QualifiedNetworksService.java @@ -238,7 +238,7 @@ public abstract class QualifiedNetworksService extends Service { @AccessNetworkConstants.RadioAccessNetworkType int qualifiedNetworkType) { if (mCallback != null) { try { - mCallback.onReconnectQualifedNetworkType(apnTypes, qualifiedNetworkType); + mCallback.onReconnectQualifiedNetworkType(apnTypes, qualifiedNetworkType); } catch (RemoteException e) { loge("Failed to call onReconnectQualifiedNetworkType. " + e); } diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java index 755636aef7ed..75284c712bd2 100644 --- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java +++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java @@ -43,13 +43,13 @@ import android.os.SystemProperties; import android.os.test.TestLooper; import android.provider.DeviceConfig; import android.util.AtomicFile; -import android.util.LongArrayQueue; import android.util.Xml; +import android.utils.LongArrayQueue; +import android.utils.XmlUtils; import androidx.test.InstrumentationRegistry; import com.android.dx.mockito.inline.extended.ExtendedMockito; -import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.PackageWatchdog.HealthCheckState; |