diff options
323 files changed, 6925 insertions, 1873 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 0ee7ace5f661..e6e835b05295 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -19,6 +19,7 @@ aconfig_srcjars = [ ":android.app.flags-aconfig-java{.generated_srcjars}", ":android.app.smartspace.flags-aconfig-java{.generated_srcjars}", ":android.app.usage.flags-aconfig-java{.generated_srcjars}", + ":android.app.wearable.flags-aconfig-java{.generated_srcjars}", ":android.appwidget.flags-aconfig-java{.generated_srcjars}", ":android.chre.flags-aconfig-java{.generated_srcjars}", ":android.companion.flags-aconfig-java{.generated_srcjars}", @@ -1123,3 +1124,16 @@ java_aconfig_library { ], defaults: ["framework-minus-apex-aconfig-java-defaults"], } + +// Wearable Sensing +aconfig_declarations { + name: "android.app.wearable.flags-aconfig", + package: "android.app.wearable", + srcs: ["core/java/android/app/wearable/*.aconfig"], +} + +java_aconfig_library { + name: "android.app.wearable.flags-aconfig-java", + aconfig_declarations: "android.app.wearable.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} diff --git a/api/Android.bp b/api/Android.bp index b3b18b66e097..ef64a89a892d 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -115,6 +115,7 @@ combined_apis { "framework-pdf", "framework-permission", "framework-permission-s", + "framework-profiling", "framework-scheduling", "framework-sdkextensions", "framework-statsd", diff --git a/api/ApiDocs.bp b/api/ApiDocs.bp index 7ae3224e7500..7ee43191a80a 100644 --- a/api/ApiDocs.bp +++ b/api/ApiDocs.bp @@ -67,6 +67,7 @@ stubs_defaults { ":framework-ondevicepersonalization-sources", ":framework-permission-sources", ":framework-permission-s-sources", + ":framework-profiling-sources", ":framework-scheduling-sources", ":framework-sdkextensions-sources", ":framework-statsd-sources", diff --git a/boot/Android.bp b/boot/Android.bp index 228d060bf9cf..cdfa7c80bc93 100644 --- a/boot/Android.bp +++ b/boot/Android.bp @@ -122,6 +122,10 @@ custom_platform_bootclasspath { module: "com.android.permission-bootclasspath-fragment", }, { + apex: "com.android.profiling", + module: "com.android.profiling-bootclasspath-fragment", + }, + { apex: "com.android.scheduling", module: "com.android.scheduling-bootclasspath-fragment", }, diff --git a/core/api/current.txt b/core/api/current.txt index cf5a261f583d..b17e3343666e 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -13,6 +13,7 @@ package android { field public static final String ACCESS_CHECKIN_PROPERTIES = "android.permission.ACCESS_CHECKIN_PROPERTIES"; field public static final String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"; field public static final String ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION"; + field @FlaggedApi("android.multiuser.enable_permission_to_access_hidden_profiles") public static final String ACCESS_HIDDEN_PROFILES = "android.permission.ACCESS_HIDDEN_PROFILES"; field public static final String ACCESS_LOCATION_EXTRA_COMMANDS = "android.permission.ACCESS_LOCATION_EXTRA_COMMANDS"; field public static final String ACCESS_MEDIA_LOCATION = "android.permission.ACCESS_MEDIA_LOCATION"; field public static final String ACCESS_NETWORK_STATE = "android.permission.ACCESS_NETWORK_STATE"; @@ -89,6 +90,7 @@ package android { field public static final String DELETE_PACKAGES = "android.permission.DELETE_PACKAGES"; field public static final String DELIVER_COMPANION_MESSAGES = "android.permission.DELIVER_COMPANION_MESSAGES"; field public static final String DETECT_SCREEN_CAPTURE = "android.permission.DETECT_SCREEN_CAPTURE"; + field @FlaggedApi("com.android.window.flags.screen_recording_callbacks") public static final String DETECT_SCREEN_RECORDING = "android.permission.DETECT_SCREEN_RECORDING"; field public static final String DIAGNOSTIC = "android.permission.DIAGNOSTIC"; field public static final String DISABLE_KEYGUARD = "android.permission.DISABLE_KEYGUARD"; field public static final String DUMP = "android.permission.DUMP"; @@ -44544,6 +44546,7 @@ package android.telephony { method @Nullable public android.telephony.CellIdentity getCellIdentity(); method public int getDomain(); method @Nullable public String getRegisteredPlmn(); + method @FlaggedApi("com.android.internal.telephony.flags.network_registration_info_reject_cause") public int getRejectCause(); method public int getTransportType(); method public boolean isNetworkRegistered(); method public boolean isNetworkRoaming(); @@ -53782,6 +53785,7 @@ package android.view { method public default void addCrossWindowBlurEnabledListener(@NonNull java.util.function.Consumer<java.lang.Boolean>); method public default void addCrossWindowBlurEnabledListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>); method public default void addProposedRotationListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer); + method @FlaggedApi("com.android.window.flags.screen_recording_callbacks") @RequiresPermission(android.Manifest.permission.DETECT_SCREEN_RECORDING) public default int addScreenRecordingCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @NonNull public default android.view.WindowMetrics getCurrentWindowMetrics(); method @Deprecated public android.view.Display getDefaultDisplay(); method @NonNull public default android.view.WindowMetrics getMaximumWindowMetrics(); @@ -53791,6 +53795,7 @@ package android.view { method @FlaggedApi("com.android.window.flags.surface_control_input_receiver") public default void registerUnbatchedSurfaceControlInputReceiver(int, @NonNull android.os.IBinder, @NonNull android.view.SurfaceControl, @NonNull android.os.Looper, @NonNull android.view.SurfaceControlInputReceiver); method public default void removeCrossWindowBlurEnabledListener(@NonNull java.util.function.Consumer<java.lang.Boolean>); method public default void removeProposedRotationListener(@NonNull java.util.function.IntConsumer); + method @FlaggedApi("com.android.window.flags.screen_recording_callbacks") @RequiresPermission(android.Manifest.permission.DETECT_SCREEN_RECORDING) public default void removeScreenRecordingCallback(@NonNull java.util.function.Consumer<java.lang.Integer>); method public void removeViewImmediate(android.view.View); method @FlaggedApi("com.android.window.flags.surface_control_input_receiver") public default void unregisterSurfaceControlInputReceiver(@NonNull android.view.SurfaceControl); method @FlaggedApi("com.android.window.flags.trusted_presentation_listener_for_window") public default void unregisterTrustedPresentationListener(@NonNull java.util.function.Consumer<java.lang.Boolean>); @@ -53810,6 +53815,8 @@ package android.view { field public static final String PROPERTY_COMPAT_ENABLE_FAKE_FOCUS = "android.window.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS"; field public static final String PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION = "android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION"; field @FlaggedApi("com.android.window.flags.supports_multi_instance_system_ui") public static final String PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI = "android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI"; + field @FlaggedApi("com.android.window.flags.screen_recording_callbacks") public static final int SCREEN_RECORDING_STATE_NOT_VISIBLE = 0; // 0x0 + field @FlaggedApi("com.android.window.flags.screen_recording_callbacks") public static final int SCREEN_RECORDING_STATE_VISIBLE = 1; // 0x1 } public static class WindowManager.BadTokenException extends java.lang.RuntimeException { diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 55ed1f559f51..d33145529f2a 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -187,6 +187,7 @@ package android.media { method @NonNull public static android.media.BluetoothProfileConnectionInfo createA2dpInfo(boolean, int); method @NonNull public static android.media.BluetoothProfileConnectionInfo createA2dpSinkInfo(int); method @NonNull public static android.media.BluetoothProfileConnectionInfo createHearingAidInfo(boolean); + method @FlaggedApi("android.media.audio.sco_managed_by_audio") @NonNull public static android.media.BluetoothProfileConnectionInfo createHfpInfo(); method @NonNull public static android.media.BluetoothProfileConnectionInfo createLeAudioInfo(boolean, boolean); method @NonNull public static android.media.BluetoothProfileConnectionInfo createLeAudioOutputInfo(boolean, int); method public int describeContents(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 86f2b6281254..e6040f8cf68b 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -414,6 +414,7 @@ package android { field public static final String WRITE_OBB = "android.permission.WRITE_OBB"; field public static final String WRITE_SECURITY_LOG = "android.permission.WRITE_SECURITY_LOG"; field public static final String WRITE_SMS = "android.permission.WRITE_SMS"; + field @FlaggedApi("android.provider.user_keys") public static final String WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS = "android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS"; } public static final class Manifest.permission_group { @@ -3157,6 +3158,7 @@ package android.app.wearable { field public static final int STATUS_SUCCESS = 1; // 0x1 field public static final int STATUS_UNKNOWN = 0; // 0x0 field public static final int STATUS_UNSUPPORTED = 2; // 0x2 + field @FlaggedApi("android.app.wearable.enable_unsupported_operation_status_code") public static final int STATUS_UNSUPPORTED_OPERATION = 6; // 0x6 field public static final int STATUS_WEARABLE_UNAVAILABLE = 4; // 0x4 } @@ -11303,6 +11305,12 @@ package android.provider { field public static final int ERROR_UNKNOWN = 0; // 0x0 } + @FlaggedApi("android.provider.user_keys") public class ContactKeysManager { + method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateContactKeyLocalVerificationState(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int); + method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateContactKeyRemoteVerificationState(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int); + method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateSelfKeyRemoteVerificationState(@NonNull String, @NonNull String, @NonNull String, int); + } + @Deprecated public static final class ContactsContract.MetadataSync implements android.provider.BaseColumns android.provider.ContactsContract.MetadataSyncColumns { field @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact_metadata"; field @Deprecated public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact_metadata"; @@ -14128,7 +14136,6 @@ package android.telephony { method @Nullable public android.telephony.DataSpecificRegistrationInfo getDataSpecificInfo(); method public int getNetworkRegistrationState(); method @Deprecated public int getRegistrationState(); - method public int getRejectCause(); method public int getRoamingType(); method public boolean isEmergencyEnabled(); method public void writeToParcel(android.os.Parcel, int); diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 6285eb3b2096..084c71f47603 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -147,6 +147,7 @@ import java.util.function.Consumer; * </p> */ @SystemService(Context.ACTIVITY_SERVICE) +@android.ravenwood.annotation.RavenwoodKeepPartialClass public class ActivityManager { private static String TAG = "ActivityManager"; @@ -966,6 +967,7 @@ public class ActivityManager { * Print capability bits in human-readable form. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static void printCapabilitiesSummary(PrintWriter pw, @ProcessCapability int caps) { pw.print((caps & PROCESS_CAPABILITY_FOREGROUND_LOCATION) != 0 ? 'L' : '-'); pw.print((caps & PROCESS_CAPABILITY_FOREGROUND_CAMERA) != 0 ? 'C' : '-'); @@ -976,6 +978,7 @@ public class ActivityManager { } /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static void printCapabilitiesSummary(StringBuilder sb, @ProcessCapability int caps) { sb.append((caps & PROCESS_CAPABILITY_FOREGROUND_LOCATION) != 0 ? 'L' : '-'); sb.append((caps & PROCESS_CAPABILITY_FOREGROUND_CAMERA) != 0 ? 'C' : '-'); @@ -989,6 +992,7 @@ public class ActivityManager { * Print capability bits in human-readable form. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static void printCapabilitiesFull(PrintWriter pw, @ProcessCapability int caps) { printCapabilitiesSummary(pw, caps); final int remain = caps & ~PROCESS_CAPABILITY_ALL; @@ -999,6 +1003,7 @@ public class ActivityManager { } /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static String getCapabilitiesSummary(@ProcessCapability int caps) { final StringBuilder sb = new StringBuilder(); printCapabilitiesSummary(sb, caps); @@ -1018,6 +1023,7 @@ public class ActivityManager { * @return the value of the corresponding enums.proto ProcessStateEnum value. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static final int processStateAmToProto(int amInt) { switch (amInt) { case PROCESS_STATE_UNKNOWN: @@ -1078,16 +1084,19 @@ public class ActivityManager { public static final int MAX_PROCESS_STATE = PROCESS_STATE_NONEXISTENT; /** @hide Should this process state be considered a background state? */ + @android.ravenwood.annotation.RavenwoodKeep public static final boolean isProcStateBackground(int procState) { return procState >= PROCESS_STATE_TRANSIENT_BACKGROUND; } /** @hide Should this process state be considered in the cache? */ + @android.ravenwood.annotation.RavenwoodKeep public static final boolean isProcStateCached(int procState) { return procState >= PROCESS_STATE_CACHED_ACTIVITY; } /** @hide Is this a foreground service type? */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isForegroundService(int procState) { return procState == PROCESS_STATE_FOREGROUND_SERVICE; } @@ -1161,10 +1170,25 @@ public class ActivityManager { mContext = context; } + private static volatile int sCurrentUser$ravenwood = UserHandle.USER_NULL; + + /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep + public static void init$ravenwood(int currentUser) { + sCurrentUser$ravenwood = currentUser; + } + + /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep + public static void reset$ravenwood() { + sCurrentUser$ravenwood = UserHandle.USER_NULL; + } + /** * Returns whether the launch was successful. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static final boolean isStartResultSuccessful(int result) { return FIRST_START_SUCCESS_CODE <= result && result <= LAST_START_SUCCESS_CODE; } @@ -1173,6 +1197,7 @@ public class ActivityManager { * Returns whether the launch result was a fatal error. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static final boolean isStartResultFatalError(int result) { return FIRST_START_FATAL_ERROR_CODE <= result && result <= LAST_START_FATAL_ERROR_CODE; } @@ -1343,6 +1368,7 @@ public class ActivityManager { public @interface RestrictionLevel{} /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static String restrictionLevelToName(@RestrictionLevel int level) { switch (level) { case RESTRICTION_LEVEL_UNKNOWN: @@ -4779,6 +4805,7 @@ public class ActivityManager { * Returns "true" if the user interface is currently being messed with * by a monkey. */ + @android.ravenwood.annotation.RavenwoodReplace public static boolean isUserAMonkey() { try { return getService().isUserAMonkey(); @@ -4787,6 +4814,12 @@ public class ActivityManager { } } + /** @hide */ + public static boolean isUserAMonkey$ravenwood() { + // Ravenwood environment is never considered a "monkey" + return false; + } + /** * Returns "true" if device is running in a test harness. * @@ -4973,6 +5006,7 @@ public class ActivityManager { "android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS_FULL" }) + @android.ravenwood.annotation.RavenwoodReplace public static int getCurrentUser() { try { return getService().getCurrentUserId(); @@ -4981,6 +5015,11 @@ public class ActivityManager { } } + /** @hide */ + public static int getCurrentUser$ravenwood() { + return sCurrentUser$ravenwood; + } + /** * @param userid the user's id. Zero indicates the default user. * @hide @@ -5320,6 +5359,7 @@ public class ActivityManager { /** * @hide */ + @android.ravenwood.annotation.RavenwoodReplace public static boolean isSystemReady() { if (!sSystemReady) { if (ActivityThread.isSystem()) { @@ -5334,6 +5374,12 @@ public class ActivityManager { return sSystemReady; } + /** @hide */ + public static boolean isSystemReady$ravenwood() { + // Ravenwood environment is always considered as booted and ready + return true; + } + /** * @hide */ @@ -5661,11 +5707,13 @@ public class ActivityManager { } /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isProcStateConsideredInteraction(@ProcessState int procState) { return (procState <= PROCESS_STATE_TOP || procState == PROCESS_STATE_BOUND_TOP); } /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static String procStateToString(int procState) { final String procStateStr; switch (procState) { diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 5d2a26e13ee7..4c54b03bd6d6 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -303,7 +303,7 @@ public final class ActivityThread extends ClientTransactionHandler public static final boolean DEBUG_MEMORY_TRIM = false; private static final boolean DEBUG_PROVIDER = false; public static final boolean DEBUG_ORDER = false; - private static final boolean DEBUG_APP_INFO = true; + private static final boolean DEBUG_APP_INFO = false; private static final long MIN_TIME_BETWEEN_GCS = 5*1000; /** * The delay to release the provider when it has no more references. It reduces the number of diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 79070595e831..0dbce97c978b 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1550,7 +1550,7 @@ public class AppOpsManager { AppProtoEnums.APP_OP_READ_SYSTEM_GRAMMATICAL_GENDER; /** - * Allows an app whose primary use case is to backup or sync content to run longer jobs. + * Allows an app with a major use case of backing-up or syncing content to run longer jobs. * * @hide */ diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index d54074818b41..e2e2f1d3401e 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -225,7 +225,7 @@ interface IActivityTaskManager { boolean focused, boolean newSessionId); boolean requestAutofillData(in IAssistDataReceiver receiver, in Bundle receiverExtras, in IBinder activityToken, int flags); - boolean isAssistDataAllowedOnCurrentActivity(); + boolean isAssistDataAllowed(); boolean requestAssistDataForTask(in IAssistDataReceiver receiver, int taskId, in String callingPackageName, String callingAttributionTag); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index d705eeb706e8..88839071bf6c 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -5946,6 +5946,12 @@ public class Notification implements Parcelable // there is enough space to do so (and fall back to the left edge if not). big.setInt(R.id.actions, "setCollapsibleIndentDimen", R.dimen.call_notification_collapsible_indent); + if (CallStyle.USE_NEW_ACTION_LAYOUT) { + if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "setting evenly divided mode on action list"); + } + big.setBoolean(R.id.actions, "setEvenlyDividedMode", true); + } } big.setBoolean(R.id.actions, "setEmphasizedMode", emphasizedMode); if (numActions > 0 && !p.mHideActions) { @@ -6421,7 +6427,15 @@ public class Notification implements Parcelable // Remove full-length color spans and ensure text contrast with the button fill. title = ContrastColorUtil.ensureColorSpanContrast(title, buttonFillColor); } - button.setTextViewText(R.id.action0, ensureColorSpanContrast(title, p)); + final CharSequence label = ensureColorSpanContrast(title, p); + if (p.mCallStyleActions && CallStyle.USE_NEW_ACTION_LAYOUT) { + if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "new action layout enabled, gluing instead of setting text"); + } + button.setCharSequence(R.id.action0, "glueLabel", label); + } else { + button.setTextViewText(R.id.action0, label); + } int textColor = ContrastColorUtil.resolvePrimaryColor(mContext, buttonFillColor, mInNightMode); if (tombstone) { @@ -6438,7 +6452,14 @@ public class Notification implements Parcelable button.setColorStateList(R.id.action0, "setButtonBackground", ColorStateList.valueOf(buttonFillColor)); if (p.mCallStyleActions) { - button.setImageViewIcon(R.id.action0, action.getIcon()); + if (CallStyle.USE_NEW_ACTION_LAYOUT) { + if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "new action layout enabled, gluing instead of setting icon"); + } + button.setIcon(R.id.action0, "glueIcon", action.getIcon()); + } else { + button.setImageViewIcon(R.id.action0, action.getIcon()); + } boolean priority = action.getExtras().getBoolean(CallStyle.KEY_ACTION_PRIORITY); button.setBoolean(R.id.action0, "setIsPriority", priority); int minWidthDimen = @@ -9565,6 +9586,15 @@ public class Notification implements Parcelable * </pre> */ public static class CallStyle extends Style { + /** + * @hide + */ + public static final boolean USE_NEW_ACTION_LAYOUT = false; + + /** + * @hide + */ + public static final boolean DEBUG_NEW_ACTION_LAYOUT = true; /** * @hide diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java index a27132872521..a045eae9e108 100644 --- a/core/java/android/app/UiModeManager.java +++ b/core/java/android/app/UiModeManager.java @@ -16,6 +16,8 @@ package android.app; +import static android.app.Flags.enableNightModeCache; + import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.FloatRange; @@ -31,6 +33,7 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.res.Configuration; import android.os.Binder; +import android.os.IpcDataCache; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; @@ -874,6 +877,51 @@ public class UiModeManager { } } + private Integer getNightModeFromServer() { + try { + if (sGlobals != null) { + return sGlobals.mService.getNightMode(); + } + return -1; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** + * Retrieve the night mode for the user. + */ + private final IpcDataCache.QueryHandler<Void, Integer> mNightModeQuery = + new IpcDataCache.QueryHandler<>() { + + @Override + @NonNull + public Integer apply(Void query) { + return getNightModeFromServer(); + } + }; + + private static final String NIGHT_MODE_API = "getNightMode"; + + /** + * Cache the night mode for a user. + */ + private final IpcDataCache<Void, Integer> mNightModeCache = + new IpcDataCache<>(1, IpcDataCache.MODULE_SYSTEM, + NIGHT_MODE_API, /* cacheName= */ "NightModeCache", mNightModeQuery); + + /** + * Invalidate the night mode cache. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_ENABLE_NIGHT_MODE_CACHE) + public static void invalidateNightModeCache() { + IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM, + NIGHT_MODE_API); + } + /** * Returns the currently configured night mode. * <p> @@ -890,14 +938,11 @@ public class UiModeManager { * @see #setNightMode(int) */ public @NightMode int getNightMode() { - if (sGlobals != null) { - try { - return sGlobals.mService.getNightMode(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + if (enableNightModeCache()) { + return mNightModeCache.query(null); + } else { + return getNightModeFromServer(); } - return -1; } /** diff --git a/core/java/android/app/ui_mode_manager.aconfig b/core/java/android/app/ui_mode_manager.aconfig new file mode 100644 index 000000000000..1ae5264a7e8e --- /dev/null +++ b/core/java/android/app/ui_mode_manager.aconfig @@ -0,0 +1,8 @@ +package: "android.app" + +flag { + namespace: "system_performance" + name: "enable_night_mode_cache" + description: "Enables the use of binder caching for system night mode." + bug: "255999432" +}
\ No newline at end of file diff --git a/core/java/android/app/wearable/WearableSensingManager.java b/core/java/android/app/wearable/WearableSensingManager.java index f1ca0864a6dd..eca0039c20f4 100644 --- a/core/java/android/app/wearable/WearableSensingManager.java +++ b/core/java/android/app/wearable/WearableSensingManager.java @@ -18,6 +18,7 @@ package android.app.wearable; import android.Manifest; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -99,6 +100,13 @@ public class WearableSensingManager { */ public static final int STATUS_ACCESS_DENIED = 5; + /** + * The value of the status code that indicates the method called is not supported by the + * implementation of {@link WearableSensingService}. + */ + @FlaggedApi(Flags.FLAG_ENABLE_UNSUPPORTED_OPERATION_STATUS_CODE) + public static final int STATUS_UNSUPPORTED_OPERATION = 6; + /** @hide */ @IntDef(prefix = { "STATUS_" }, value = { STATUS_UNKNOWN, @@ -106,7 +114,8 @@ public class WearableSensingManager { STATUS_UNSUPPORTED, STATUS_SERVICE_UNAVAILABLE, STATUS_WEARABLE_UNAVAILABLE, - STATUS_ACCESS_DENIED + STATUS_ACCESS_DENIED, + STATUS_UNSUPPORTED_OPERATION }) @Retention(RetentionPolicy.SOURCE) public @interface StatusCode {} diff --git a/core/java/android/app/wearable/flags.aconfig b/core/java/android/app/wearable/flags.aconfig new file mode 100644 index 000000000000..074ce9bb0e82 --- /dev/null +++ b/core/java/android/app/wearable/flags.aconfig @@ -0,0 +1,8 @@ +package: "android.app.wearable" + +flag { + name: "enable_unsupported_operation_status_code" + namespace: "machine_learning" + description: "This flag enables the WearableSensingManager#STATUS_UNSUPPORTED_OPERATION status code API." + bug: "301427767" +}
\ No newline at end of file diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java index bde562dbf95b..af1301140358 100644 --- a/core/java/android/content/AttributionSource.java +++ b/core/java/android/content/AttributionSource.java @@ -409,9 +409,10 @@ public final class AttributionSource implements Parcelable { "packageName = " + mAttributionSourceState.packageName + ", " + "attributionTag = " + mAttributionSourceState.attributionTag + ", " + "token = " + mAttributionSourceState.token + ", " + + "deviceId = " + mAttributionSourceState.deviceId + ", " + "next = " + (mAttributionSourceState.next != null - && mAttributionSourceState.next.length > 0 - ? mAttributionSourceState.next[0] : null) + + && mAttributionSourceState.next.length > 0 + ? new AttributionSource(mAttributionSourceState.next[0]).toString() : null) + " }"; } return super.toString(); diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index caff4576c9c3..19bce0bb3abd 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -183,3 +183,19 @@ flag { description: "Feature flag to enable permission EMERGENCY_INSTALL_PACKAGES" bug: "321080601" } + +flag { + name: "asl_in_apk_app_metadata_source" + namespace: "package_manager_service" + description: "Feature flag to allow to know if the Android Safety Label (ASL) of an app is provided by the app's APK itself, or provided by an installer." + bug: "287487923" + is_fixed_read_only: true +} + +flag { + name: "force_multi_arch_native_libs_match" + namespace: "package_manager_service" + description: "Feature flag to force an multiArch app's native libraries to match with the natively supported ABIs of the device" + bug: "282783453" + is_fixed_read_only: true +} diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index efb8607f75f7..d7e64b66763e 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -108,3 +108,17 @@ flag { bug: "316362775" is_fixed_read_only: true } + +flag { + name: "enable_permission_to_access_hidden_profiles" + namespace: "profile_experiences" + description: "Add permission to access API hidden users data via system APIs" + bug: "321988638" +} + +flag { + name: "handle_interleaved_settings_for_private_space" + namespace: "profile_experiences" + description: "Handle listing of private space apps in settings pages with interleaved content" + bug: "323212460" +} diff --git a/core/java/android/content/pm/overlay/OverlayPaths.java b/core/java/android/content/pm/overlay/OverlayPaths.java index bd74b0b9293c..a4db733af013 100644 --- a/core/java/android/content/pm/overlay/OverlayPaths.java +++ b/core/java/android/content/pm/overlay/OverlayPaths.java @@ -49,13 +49,6 @@ public class OverlayPaths { public static class Builder { final OverlayPaths mPaths = new OverlayPaths(); - public Builder() {} - - public Builder(@NonNull OverlayPaths base) { - mPaths.mResourceDirs.addAll(base.getResourceDirs()); - mPaths.mOverlayPaths.addAll(base.getOverlayPaths()); - } - /** * Adds a non-APK path to the contents of {@link OverlayPaths#getOverlayPaths()}. */ diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index c7790bd96c62..5e442b819774 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -273,14 +273,27 @@ public class ResourcesImpl { throw new NotFoundException("String resource name " + name); } + private static boolean isIntLike(@NonNull String s) { + if (s.isEmpty() || s.length() > 10) return false; + for (int i = 0, size = s.length(); i < size; i++) { + final char c = s.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + return true; + } + int getIdentifier(String name, String defType, String defPackage) { if (name == null) { throw new NullPointerException("name is null"); } - try { - return Integer.parseInt(name); - } catch (Exception e) { - // Ignore + if (isIntLike(name)) { + try { + return Integer.parseInt(name); + } catch (Exception e) { + // Ignore + } } return mAssets.getResourceIdentifier(name, defType, defPackage); } diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig index 90cd4718fcee..1ca11e698d5d 100644 --- a/core/java/android/credentials/flags.aconfig +++ b/core/java/android/credentials/flags.aconfig @@ -47,4 +47,11 @@ flag { name: "configurable_selector_ui_enabled" description: "Enables OEM configurable Credential Selector UI" bug: "319448437" -}
\ No newline at end of file +} + +flag { + namespace: "credential_manager" + name: "credman_biometric_api_enabled" + description: "Enables Credential Manager to work with the Biometric Authenticate API" + bug: "323211850" +} diff --git a/core/java/android/ddm/DdmHandleViewDebug.java b/core/java/android/ddm/DdmHandleViewDebug.java index 0f66fcbdbec9..5cbf24f3ba54 100644 --- a/core/java/android/ddm/DdmHandleViewDebug.java +++ b/core/java/android/ddm/DdmHandleViewDebug.java @@ -16,16 +16,12 @@ package android.ddm; -import static com.android.internal.util.Preconditions.checkArgument; - import android.util.Log; import android.view.View; import android.view.ViewDebug; import android.view.ViewRootImpl; import android.view.WindowManagerGlobal; -import com.android.internal.annotations.VisibleForTesting; - import org.apache.harmony.dalvik.ddmc.Chunk; import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; @@ -35,10 +31,8 @@ import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; -import java.lang.reflect.Method; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; /** * Handle various requests related to profiling / debugging of the view system. @@ -352,48 +346,17 @@ public class DdmHandleViewDebug extends DdmHandle { * * The return value is encoded the same way as a single parameter (type + value) */ - private Chunk invokeViewMethod(final View rootView, final View targetView, ByteBuffer in) { + private Chunk invokeViewMethod(View rootView, final View targetView, ByteBuffer in) { int l = in.getInt(); String methodName = getString(in, l); - Class<?>[] argTypes; - Object[] args; - if (!in.hasRemaining()) { - argTypes = new Class<?>[0]; - args = new Object[0]; - } else { - int nArgs = in.getInt(); - argTypes = new Class<?>[nArgs]; - args = new Object[nArgs]; - - try { - deserializeMethodParameters(args, argTypes, in); - } catch (ViewMethodInvocationSerializationException e) { - return createFailChunk(ERR_INVALID_PARAM, e.getMessage()); - } - } - - Method method; - try { - method = targetView.getClass().getMethod(methodName, argTypes); - } catch (NoSuchMethodException e) { - Log.e(TAG, "No such method: " + e.getMessage()); - return createFailChunk(ERR_INVALID_PARAM, - "No such method: " + e.getMessage()); - } - try { - Object result = ViewDebug.invokeViewMethod(targetView, method, args); - Class<?> returnType = method.getReturnType(); - byte[] returnValue = serializeReturnValue(returnType, returnType.cast(result)); + byte[] returnValue = ViewDebug.invokeViewMethod(targetView, methodName, in); return new Chunk(CHUNK_VUOP, returnValue, 0, returnValue.length); + } catch (ViewDebug.ViewMethodInvocationSerializationException e) { + return createFailChunk(ERR_INVALID_PARAM, e.getMessage()); } catch (Exception e) { - Log.e(TAG, "Exception while invoking method: " + e.getCause().getMessage()); - String msg = e.getCause().getMessage(); - if (msg == null) { - msg = e.getCause().toString(); - } - return createFailChunk(ERR_EXCEPTION, msg); + return createFailChunk(ERR_EXCEPTION, e.getMessage()); } } @@ -431,175 +394,4 @@ public class DdmHandleViewDebug extends DdmHandle { byte[] data = b.toByteArray(); return new Chunk(CHUNK_VUOP, data, 0, data.length); } - - /** - * Deserializes parameters according to the VUOP_INVOKE_VIEW_METHOD protocol the {@code in} - * buffer. - * - * The length of {@code args} determines how many arguments are read. The {@code argTypes} must - * be the same length, and will be set to the argument types of the data read. - * - * @hide - */ - @VisibleForTesting - public static void deserializeMethodParameters( - Object[] args, Class<?>[] argTypes, ByteBuffer in) throws - ViewMethodInvocationSerializationException { - checkArgument(args.length == argTypes.length); - - for (int i = 0; i < args.length; i++) { - char typeSignature = in.getChar(); - boolean isArray = typeSignature == SIG_ARRAY; - if (isArray) { - char arrayType = in.getChar(); - if (arrayType != SIG_BYTE) { - // This implementation only supports byte-arrays for now. - throw new ViewMethodInvocationSerializationException( - "Unsupported array parameter type (" + typeSignature - + ") to invoke view method @argument " + i); - } - - int arrayLength = in.getInt(); - if (arrayLength > in.remaining()) { - // The sender did not actually sent the specified amount of bytes. This - // avoids a malformed packet to trigger an out-of-memory error. - throw new BufferUnderflowException(); - } - - byte[] byteArray = new byte[arrayLength]; - in.get(byteArray); - - argTypes[i] = byte[].class; - args[i] = byteArray; - } else { - switch (typeSignature) { - case SIG_BOOLEAN: - argTypes[i] = boolean.class; - args[i] = in.get() != 0; - break; - case SIG_BYTE: - argTypes[i] = byte.class; - args[i] = in.get(); - break; - case SIG_CHAR: - argTypes[i] = char.class; - args[i] = in.getChar(); - break; - case SIG_SHORT: - argTypes[i] = short.class; - args[i] = in.getShort(); - break; - case SIG_INT: - argTypes[i] = int.class; - args[i] = in.getInt(); - break; - case SIG_LONG: - argTypes[i] = long.class; - args[i] = in.getLong(); - break; - case SIG_FLOAT: - argTypes[i] = float.class; - args[i] = in.getFloat(); - break; - case SIG_DOUBLE: - argTypes[i] = double.class; - args[i] = in.getDouble(); - break; - case SIG_STRING: { - argTypes[i] = String.class; - int stringUtf8ByteCount = Short.toUnsignedInt(in.getShort()); - byte[] rawStringBuffer = new byte[stringUtf8ByteCount]; - in.get(rawStringBuffer); - args[i] = new String(rawStringBuffer, StandardCharsets.UTF_8); - break; - } - default: - Log.e(TAG, "arg " + i + ", unrecognized type: " + typeSignature); - throw new ViewMethodInvocationSerializationException( - "Unsupported parameter type (" + typeSignature - + ") to invoke view method."); - } - } - - } - } - - /** - * Serializes {@code value} to the wire protocol of VUOP_INVOKE_VIEW_METHOD. - * @hide - */ - @VisibleForTesting - public static byte[] serializeReturnValue(Class<?> type, Object value) - throws ViewMethodInvocationSerializationException, IOException { - ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(1024); - DataOutputStream dos = new DataOutputStream(byteOutStream); - - if (type.isArray()) { - if (!type.equals(byte[].class)) { - // Only byte arrays are supported currently. - throw new ViewMethodInvocationSerializationException( - "Unsupported array return type (" + type + ")"); - } - byte[] byteArray = (byte[]) value; - dos.writeChar(SIG_ARRAY); - dos.writeChar(SIG_BYTE); - dos.writeInt(byteArray.length); - dos.write(byteArray); - } else if (boolean.class.equals(type)) { - dos.writeChar(SIG_BOOLEAN); - dos.write((boolean) value ? 1 : 0); - } else if (byte.class.equals(type)) { - dos.writeChar(SIG_BYTE); - dos.writeByte((byte) value); - } else if (char.class.equals(type)) { - dos.writeChar(SIG_CHAR); - dos.writeChar((char) value); - } else if (short.class.equals(type)) { - dos.writeChar(SIG_SHORT); - dos.writeShort((short) value); - } else if (int.class.equals(type)) { - dos.writeChar(SIG_INT); - dos.writeInt((int) value); - } else if (long.class.equals(type)) { - dos.writeChar(SIG_LONG); - dos.writeLong((long) value); - } else if (double.class.equals(type)) { - dos.writeChar(SIG_DOUBLE); - dos.writeDouble((double) value); - } else if (float.class.equals(type)) { - dos.writeChar(SIG_FLOAT); - dos.writeFloat((float) value); - } else if (String.class.equals(type)) { - dos.writeChar(SIG_STRING); - dos.writeUTF(value != null ? (String) value : ""); - } else { - dos.writeChar(SIG_VOID); - } - - return byteOutStream.toByteArray(); - } - - // Prefixes for simple primitives. These match the JNI definitions. - private static final char SIG_ARRAY = '['; - private static final char SIG_BOOLEAN = 'Z'; - private static final char SIG_BYTE = 'B'; - private static final char SIG_SHORT = 'S'; - private static final char SIG_CHAR = 'C'; - private static final char SIG_INT = 'I'; - private static final char SIG_LONG = 'J'; - private static final char SIG_FLOAT = 'F'; - private static final char SIG_DOUBLE = 'D'; - private static final char SIG_VOID = 'V'; - // Prefixes for some commonly used objects - private static final char SIG_STRING = 'R'; - - /** - * @hide - */ - @VisibleForTesting - public static class ViewMethodInvocationSerializationException extends Exception { - ViewMethodInvocationSerializationException(String message) { - super(message); - } - } } diff --git a/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl b/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl index d51e62e709c2..1488cff3a35e 100644 --- a/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl +++ b/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl @@ -15,6 +15,8 @@ */ package android.hardware.biometrics; +import android.hardware.biometrics.BiometricSourceType; + /** * Low-level callback interface between <Biometric>Manager and <Auth>Service. Allows core system * services (e.g. SystemUI) to register a listener for updates about the current state of biometric @@ -49,4 +51,15 @@ oneway interface AuthenticationStateListener { * @param userId The user Id for the requested authentication */ void onAuthenticationFailed(int requestReason, int userId); + + /** + * Defines behavior in response to biometric being acquired. + * @param biometricSourceType identifies [BiometricSourceType] biometric was acquired for + * @param requestReason reason from [BiometricRequestConstants.RequestReason] for authentication + * @param acquiredInfo [BiometricFingerprintConstants.FingerprintAcquired] int corresponding to + * a known acquired message. + */ + void onAuthenticationAcquired( + in BiometricSourceType biometricSourceType, int requestReason, int acquiredInfo + ); } diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index 64a62a9299b8..f18a0b758bc5 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -202,8 +202,11 @@ public abstract class DisplayManagerInternal { /** * Called by the window manager to perform traversals while holding a * surface flinger transaction. + * @param t The default transaction. + * @param displayTransactions The transactions mapped by display id. */ - public abstract void performTraversal(Transaction t); + public abstract void performTraversal(Transaction t, + SparseArray<SurfaceControl.Transaction> displayTransactions); /** * Tells the display manager about properties of the display that depend on the windows on it. diff --git a/core/java/android/os/VibrationAttributes.java b/core/java/android/os/VibrationAttributes.java index 5078dc351f6f..46705a31f395 100644 --- a/core/java/android/os/VibrationAttributes.java +++ b/core/java/android/os/VibrationAttributes.java @@ -29,6 +29,7 @@ import java.util.Objects; /** * Encapsulates a collection of attributes describing information about a vibration. */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public final class VibrationAttributes implements Parcelable { private static final String TAG = "VibrationAttributes"; @@ -463,6 +464,7 @@ public final class VibrationAttributes implements Parcelable { * Builder class for {@link VibrationAttributes} objects. * By default, all information is set to UNKNOWN. */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public static final class Builder { private int mUsage = USAGE_UNKNOWN; private int mOriginalAudioUsage = AudioAttributes.USAGE_UNKNOWN; diff --git a/core/java/android/provider/ContactKeysManager.java b/core/java/android/provider/ContactKeysManager.java index ecde69917820..bef645698b4a 100644 --- a/core/java/android/provider/ContactKeysManager.java +++ b/core/java/android/provider/ContactKeysManager.java @@ -21,6 +21,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SystemApi; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; @@ -247,6 +248,44 @@ public class ContactKeysManager { } /** + * Updates a contact key entry's local verification state that belongs to the app identified + * by ownerPackageName. + * + * @param lookupKey the value that references the contact + * @param deviceId an app-specified identifier for the device + * @param accountId an app-specified identifier for the account + * @param ownerPackageName the package name of the app that owns the key + * @param localVerificationState the new local verification state + * + * @return true if the entry was updated, false otherwise. + * + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, + android.Manifest.permission.WRITE_CONTACTS}) + public boolean updateContactKeyLocalVerificationState(@NonNull String lookupKey, + @NonNull String deviceId, + @NonNull String accountId, + @NonNull String ownerPackageName, + @VerificationState int localVerificationState) { + validateVerificationState(localVerificationState); + + final Bundle extras = new Bundle(); + extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey)); + extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); + extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); + extras.putString(ContactKeys.OWNER_PACKAGE_NAME, Objects.requireNonNull(ownerPackageName)); + extras.putInt(ContactKeys.LOCAL_VERIFICATION_STATE, localVerificationState); + + final Bundle response = nullSafeCall(mContentResolver, + ContactKeys.UPDATE_CONTACT_KEY_LOCAL_VERIFICATION_STATE_METHOD, extras); + + return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); + } + + /** * Updates a contact key entry's remote verification state that belongs to the caller app. * * @param lookupKey the value that references the contact @@ -275,6 +314,45 @@ public class ContactKeysManager { return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); } + /** + * Updates a contact key entry's remote verification state that belongs to the app identified + * by ownerPackageName. + * + * @param lookupKey the value that references the contact + * @param deviceId an app-specified identifier for the device + * @param accountId an app-specified identifier for the account + * @param ownerPackageName the package name of the app that owns the key + * @param remoteVerificationState the new remote verification state + * + * @return true if the entry was updated, false otherwise. + * + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, + android.Manifest.permission.WRITE_CONTACTS}) + public boolean updateContactKeyRemoteVerificationState(@NonNull String lookupKey, + @NonNull String deviceId, + @NonNull String accountId, + @NonNull String ownerPackageName, + @VerificationState int remoteVerificationState) { + validateVerificationState(remoteVerificationState); + + final Bundle extras = new Bundle(); + extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey)); + extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); + extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); + extras.putString(ContactKeys.OWNER_PACKAGE_NAME, Objects.requireNonNull(ownerPackageName)); + extras.putInt(ContactKeys.REMOTE_VERIFICATION_STATE, remoteVerificationState); + + final Bundle response = nullSafeCall(mContentResolver, + ContactKeys.UPDATE_CONTACT_KEY_REMOTE_VERIFICATION_STATE_METHOD, extras); + + return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); + } + + private static void validateVerificationState(int verificationState) { if (verificationState != UNVERIFIED && verificationState != VERIFICATION_FAILED @@ -297,12 +375,12 @@ public class ContactKeysManager { public boolean removeContactKey(@NonNull String lookupKey, @NonNull String deviceId, @NonNull String accountId) { - Bundle extras = new Bundle(); + final Bundle extras = new Bundle(); extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey)); extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); - Bundle response = nullSafeCall(mContentResolver, + final Bundle response = nullSafeCall(mContentResolver, ContactKeys.REMOVE_CONTACT_KEY_METHOD, extras); return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); @@ -369,6 +447,41 @@ public class ContactKeysManager { } /** + * Updates a self key entry's remote verification state that belongs to the app identified + * by ownerPackageName. + * + * @param deviceId an app-specified identifier for the device + * @param accountId an app-specified identifier for the account + * @param ownerPackageName the package name of the app that owns the key + * @param remoteVerificationState the new remote verification state + * + * @return true if the entry was updated, false otherwise. + * + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, + android.Manifest.permission.WRITE_CONTACTS}) + public boolean updateSelfKeyRemoteVerificationState(@NonNull String deviceId, + @NonNull String accountId, + @NonNull String ownerPackageName, + @VerificationState int remoteVerificationState) { + validateVerificationState(remoteVerificationState); + + Bundle extras = new Bundle(); + extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); + extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); + extras.putString(ContactKeys.OWNER_PACKAGE_NAME, Objects.requireNonNull(ownerPackageName)); + extras.putInt(ContactKeys.REMOTE_VERIFICATION_STATE, remoteVerificationState); + + Bundle response = nullSafeCall(mContentResolver, + ContactKeys.UPDATE_SELF_KEY_REMOTE_VERIFICATION_STATE_METHOD, extras); + + return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); + } + + /** * Maximum size of a contact key. */ public static int getMaxKeySizeBytes() { diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java index 064bc6947fc4..35b137a322e3 100644 --- a/core/java/android/view/FocusFinder.java +++ b/core/java/android/view/FocusFinder.java @@ -314,13 +314,20 @@ public class FocusFinder { if (count < 2) { return null; } + View next = null; + final boolean[] looped = new boolean[1]; switch (direction) { case View.FOCUS_FORWARD: - return getNextFocusable(focused, focusables, count); + next = getNextFocusable(focused, focusables, count, looped); + break; case View.FOCUS_BACKWARD: - return getPreviousFocusable(focused, focusables, count); + next = getPreviousFocusable(focused, focusables, count, looped); + break; } - return focusables.get(count - 1); + if (root != null && root.mAttachInfo != null && root == root.getRootView()) { + root.mAttachInfo.mNextFocusLooped = looped[0]; + } + return next != null ? next : focusables.get(count - 1); } private void setFocusBottomRight(ViewGroup root, Rect focusedRect) { @@ -375,7 +382,8 @@ public class FocusFinder { return closest; } - private static View getNextFocusable(View focused, ArrayList<View> focusables, int count) { + private static View getNextFocusable(View focused, ArrayList<View> focusables, int count, + boolean[] outLooped) { if (count < 2) { return null; } @@ -385,10 +393,12 @@ public class FocusFinder { return focusables.get(position + 1); } } + outLooped[0] = true; return focusables.get(0); } - private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count) { + private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count, + boolean[] outLooped) { if (count < 2) { return null; } @@ -398,6 +408,7 @@ public class FocusFinder { return focusables.get(position - 1); } } + outLooped[0] = true; return focusables.get(count - 1); } diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 7903050e41d4..99863d013970 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -1085,7 +1085,9 @@ interface IWindowManager void unregisterTrustedPresentationListener(in ITrustedPresentationListener listener, int id); + @EnforcePermission("DETECT_SCREEN_RECORDING") boolean registerScreenRecordingCallback(IScreenRecordingCallback callback); + @EnforcePermission("DETECT_SCREEN_RECORDING") void unregisterScreenRecordingCallback(IScreenRecordingCallback callback); } diff --git a/core/java/android/view/ScreenRecordingCallbacks.java b/core/java/android/view/ScreenRecordingCallbacks.java new file mode 100644 index 000000000000..ee55737cc27e --- /dev/null +++ b/core/java/android/view/ScreenRecordingCallbacks.java @@ -0,0 +1,146 @@ +/* + * 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 android.view; + +import static android.Manifest.permission.DETECT_SCREEN_RECORDING; +import static android.view.WindowManager.SCREEN_RECORDING_STATE_NOT_VISIBLE; +import static android.view.WindowManager.SCREEN_RECORDING_STATE_VISIBLE; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.os.Binder; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.view.WindowManager.ScreenRecordingState; +import android.window.IScreenRecordingCallback; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * This class is responsible for calling app-registered screen recording callbacks. This class + * registers a single screen recording callback with WindowManagerService and calls the + * app-registered callbacks whenever that WindowManagerService callback is called. + * + * @hide + */ +public final class ScreenRecordingCallbacks { + + private static ScreenRecordingCallbacks sInstance; + private static final Object sLock = new Object(); + + private final ArrayMap<Consumer<@ScreenRecordingState Integer>, Executor> mCallbacks = + new ArrayMap<>(); + + private IScreenRecordingCallback mCallbackNotifier; + private @ScreenRecordingState int mState = SCREEN_RECORDING_STATE_NOT_VISIBLE; + + private ScreenRecordingCallbacks() {} + + private static @NonNull IWindowManager getWindowManagerService() { + return Objects.requireNonNull(WindowManagerGlobal.getWindowManagerService()); + } + + static ScreenRecordingCallbacks getInstance() { + synchronized (sLock) { + if (sInstance == null) { + sInstance = new ScreenRecordingCallbacks(); + } + return sInstance; + } + } + + @RequiresPermission(DETECT_SCREEN_RECORDING) + @ScreenRecordingState + int addCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<@ScreenRecordingState Integer> callback) { + synchronized (sLock) { + if (mCallbackNotifier == null) { + mCallbackNotifier = + new IScreenRecordingCallback.Stub() { + @Override + public void onScreenRecordingStateChanged( + boolean visibleInScreenRecording) { + int state = + visibleInScreenRecording + ? SCREEN_RECORDING_STATE_VISIBLE + : SCREEN_RECORDING_STATE_NOT_VISIBLE; + notifyCallbacks(state); + } + }; + try { + boolean visibleInScreenRecording = + getWindowManagerService() + .registerScreenRecordingCallback(mCallbackNotifier); + mState = + visibleInScreenRecording + ? SCREEN_RECORDING_STATE_VISIBLE + : SCREEN_RECORDING_STATE_NOT_VISIBLE; + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + mCallbacks.put(callback, executor); + return mState; + } + } + + @RequiresPermission(DETECT_SCREEN_RECORDING) + void removeCallback(@NonNull Consumer<@ScreenRecordingState Integer> callback) { + synchronized (sLock) { + mCallbacks.remove(callback); + if (mCallbacks.isEmpty()) { + try { + getWindowManagerService().unregisterScreenRecordingCallback(mCallbackNotifier); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + mCallbackNotifier = null; + } + } + } + + private void notifyCallbacks(@ScreenRecordingState int state) { + List<Runnable> callbacks; + synchronized (sLock) { + mState = state; + if (mCallbacks.isEmpty()) { + return; + } + + callbacks = new ArrayList<>(); + for (int i = 0; i < mCallbacks.size(); i++) { + Consumer<Integer> callback = mCallbacks.keyAt(i); + Executor executor = mCallbacks.valueAt(i); + callbacks.add(() -> executor.execute(() -> callback.accept(state))); + } + } + final long token = Binder.clearCallingIdentity(); + try { + for (int i = 0; i < callbacks.size(); i++) { + callbacks.get(i).run(); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } +} diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 1b22fda9c31a..2366ff77692b 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -5537,10 +5537,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private static final float FRAME_RATE_SIZE_PERCENTAGE_THRESHOLD = 0.07f; + + private static final long INFREQUENT_UPDATE_INTERVAL_MILLIS = 100; + private static final int INFREQUENT_UPDATE_COUNTS = 2; + // The preferred frame rate of the view that is mainly used for // touch boosting, view velocity handling, and TextureView. private float mPreferredFrameRate = REQUESTED_FRAME_RATE_CATEGORY_DEFAULT; + private int mInfrequentUpdateCount = 0; + private long mLastUpdateTimeMillis = 0; + private long mMinusOneFrameIntervalMillis = 0; + private long mMinusTwoFrameIntervalMillis = 0; + private int mLastFrameRateCategory = FRAME_RATE_CATEGORY_HIGH; + @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public static final float REQUESTED_FRAME_RATE_CATEGORY_DEFAULT = 0; @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) @@ -20253,7 +20263,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } // For VRR to vote the preferred frame rate - votePreferredFrameRate(); + if (sToolkitSetFrameRateReadOnlyFlagValue) { + updateInfrequentCount(); + votePreferredFrameRate(); + } // Reset content capture caches mPrivateFlags4 &= ~PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK; @@ -20358,7 +20371,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, protected void damageInParent() { if (mParent != null && mAttachInfo != null) { // For VRR to vote the preferred frame rate - votePreferredFrameRate(); + if (sToolkitSetFrameRateReadOnlyFlagValue) { + updateInfrequentCount(); + votePreferredFrameRate(); + } mParent.onDescendantInvalidated(this, this); } } @@ -31336,6 +31352,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, final ArrayList<View> mTempArrayList = new ArrayList<View>(24); /** + * Indicates if the next focus will be looped back to the first focusable view of the entire + * hierarchy when finding in the direction of {@link #FOCUS_FORWARD} or to the last + * focusable view when finding in the direction of {@link #FOCUS_BACKWARD}. + */ + boolean mNextFocusLooped = false; + + /** * The id of the window for accessibility purposes. */ int mAccessibilityWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; @@ -33124,11 +33147,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } private int calculateFrameRateCategory(float sizePercentage) { - if (sizePercentage <= FRAME_RATE_SIZE_PERCENTAGE_THRESHOLD) { - return FRAME_RATE_CATEGORY_LOW; - } else { + if (mMinusTwoFrameIntervalMillis + mMinusOneFrameIntervalMillis + < INFREQUENT_UPDATE_INTERVAL_MILLIS) { + if (sizePercentage <= FRAME_RATE_SIZE_PERCENTAGE_THRESHOLD) { + return FRAME_RATE_CATEGORY_NORMAL; + } else { + return FRAME_RATE_CATEGORY_HIGH; + } + } + + if (mInfrequentUpdateCount == INFREQUENT_UPDATE_COUNTS) { return FRAME_RATE_CATEGORY_NORMAL; } + + return mLastFrameRateCategory; } private void votePreferredFrameRate() { @@ -33137,22 +33169,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback, float sizePercentage = getSizePercentage(); int frameRateCateogry = calculateFrameRateCategory(sizePercentage); if (viewRootImpl != null && sizePercentage > 0) { - if (sToolkitSetFrameRateReadOnlyFlagValue) { - if (mPreferredFrameRate < 0) { - if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE) { - frameRateCateogry = FRAME_RATE_CATEGORY_NO_PREFERENCE; - } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_LOW) { - frameRateCateogry = FRAME_RATE_CATEGORY_LOW; - } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NORMAL) { - frameRateCateogry = FRAME_RATE_CATEGORY_NORMAL; - } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_HIGH) { - frameRateCateogry = FRAME_RATE_CATEGORY_HIGH; - } - } else { - viewRootImpl.votePreferredFrameRate(mPreferredFrameRate); + if (mPreferredFrameRate < 0) { + if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE) { + frameRateCateogry = FRAME_RATE_CATEGORY_NO_PREFERENCE; + } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_LOW) { + frameRateCateogry = FRAME_RATE_CATEGORY_LOW; + } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NORMAL) { + frameRateCateogry = FRAME_RATE_CATEGORY_NORMAL; + } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_HIGH) { + frameRateCateogry = FRAME_RATE_CATEGORY_HIGH; } - viewRootImpl.votePreferredFrameRateCategory(frameRateCateogry); + } else { + viewRootImpl.votePreferredFrameRate(mPreferredFrameRate); } + viewRootImpl.votePreferredFrameRateCategory(frameRateCateogry); + mLastFrameRateCategory = frameRateCateogry; + if (sToolkitMetricsForFrameRateDecisionFlagValue) { viewRootImpl.recordViewPercentage(sizePercentage); } @@ -33231,4 +33263,27 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } return 0; } + + /** + * This function is mainly used for migrating infrequent layer lagic + * from SurfaceFlinger to Toolkit. + * The infrequent layter logic includes: + * - NORMAL for infrequent update: FT2-FT1 > 100 && FT3-FT2 > 100. + * - HIGH/NORMAL based on size for frequent update: (FT3-FT2) + (FT2 - FT1) < 100. + * - otherwise, use the previous category value. + */ + private void updateInfrequentCount() { + long currentTimeMillis = AnimationUtils.currentAnimationTimeMillis(); + long timeIntervalMillis = currentTimeMillis - mLastUpdateTimeMillis; + mMinusTwoFrameIntervalMillis = mMinusOneFrameIntervalMillis; + mMinusOneFrameIntervalMillis = timeIntervalMillis; + + mLastUpdateTimeMillis = currentTimeMillis; + if (timeIntervalMillis >= INFREQUENT_UPDATE_INTERVAL_MILLIS) { + mInfrequentUpdateCount = mInfrequentUpdateCount == INFREQUENT_UPDATE_COUNTS + ? mInfrequentUpdateCount : mInfrequentUpdateCount + 1; + } else { + mInfrequentUpdateCount = 0; + } + } } diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index 25e0eca12bf3..4f1fb40ab214 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -7,7 +7,7 @@ * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software + * Unless required by applicable law or agreed to in writing, softwareViewDebug * 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 @@ -16,6 +16,8 @@ package android.view; +import static com.android.internal.util.Preconditions.checkArgument; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; @@ -34,10 +36,13 @@ import android.os.Debug; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.util.Base64; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; +import com.android.internal.annotations.VisibleForTesting; + import libcore.util.HexEncoding; import java.io.BufferedOutputStream; @@ -54,9 +59,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayDeque; import java.util.Arrays; import java.util.HashMap; @@ -67,7 +74,6 @@ import java.util.concurrent.Executor; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; import java.util.stream.Stream; @@ -76,6 +82,9 @@ import java.util.stream.Stream; * Various debugging/tracing tools related to {@link View} and the view hierarchy. */ public class ViewDebug { + + private static final String TAG = "ViewDebug"; + /** * @deprecated This flag is now unused */ @@ -425,6 +434,7 @@ public class ViewDebug { private static final String REMOTE_PROFILE = "PROFILE"; private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS"; private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST"; + private static final String REMOTE_COMMAND_INVOKE_METHOD = "INVOKE_METHOD"; private static HashMap<Class<?>, PropertyInfo<ExportedProperty, ?>[]> sExportProperties; private static HashMap<Class<?>, PropertyInfo<CapturedViewProperty, ?>[]> @@ -555,6 +565,8 @@ public class ViewDebug { requestLayout(view, params[0]); } else if (REMOTE_PROFILE.equalsIgnoreCase(command)) { profile(view, clientStream, params[0]); + } else if (REMOTE_COMMAND_INVOKE_METHOD.equals(command)) { + invokeViewMethod(view, clientStream, params); } } } @@ -1825,46 +1837,84 @@ public class ViewDebug { Log.d(tag, sb.toString()); } + private static void invokeViewMethod(View root, OutputStream clientStream, String[] params) + throws IOException { + BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024); + try { + if (params.length < 2) { + throw new IllegalArgumentException("Missing parameter"); + } + View targetView = findView(root, params[0]); + if (targetView == null) { + throw new IllegalArgumentException("View not found: " + params[0]); + } + String method = params[1]; + ByteBuffer args = ByteBuffer.wrap(params.length < 2 + ? new byte[0] + : Base64.decode(params[2], Base64.NO_WRAP)); + byte[] result = invokeViewMethod(targetView, method, args); + out.write("1"); + out.newLine(); + out.write(Base64.encodeToString(result, Base64.NO_WRAP)); + out.newLine(); + } catch (Exception e) { + out.write("-1"); + out.newLine(); + out.write(e.getMessage()); + out.newLine(); + } finally { + out.close(); + } + } + /** * Invoke a particular method on given view. * The given method is always invoked on the UI thread. The caller thread will stall until the * method invocation is complete. Returns an object equal to the result of the method * invocation, null if the method is declared to return void + * @param params all the method parameters encoded in a byteArray * @throws Exception if the method invocation caused any exception * @hide */ - public static Object invokeViewMethod(final View view, final Method method, - final Object[] args) { - final CountDownLatch latch = new CountDownLatch(1); - final AtomicReference<Object> result = new AtomicReference<Object>(); - final AtomicReference<Throwable> exception = new AtomicReference<Throwable>(); - - view.post(new Runnable() { - @Override - public void run() { - try { - result.set(method.invoke(view, args)); - } catch (InvocationTargetException e) { - exception.set(e.getCause()); - } catch (Exception e) { - exception.set(e); - } + public static byte[] invokeViewMethod(View targetView, String methodName, ByteBuffer params) + throws ViewMethodInvocationSerializationException { + Class<?>[] argTypes; + Object[] args; + if (!params.hasRemaining()) { + argTypes = new Class<?>[0]; + args = new Object[0]; + } else { + int nArgs = params.getInt(); + argTypes = new Class<?>[nArgs]; + args = new Object[nArgs]; - latch.countDown(); - } - }); + deserializeMethodParameters(args, argTypes, params); + } + Method method; try { - latch.await(); - } catch (InterruptedException e) { - throw new RuntimeException(e); + method = targetView.getClass().getMethod(methodName, argTypes); + } catch (NoSuchMethodException e) { + Log.e(TAG, "No such method: " + e.getMessage()); + throw new ViewMethodInvocationSerializationException( + "No such method: " + e.getMessage()); } - if (exception.get() != null) { - throw new RuntimeException(exception.get()); + try { + // Invoke the method on Views handler + FutureTask<Object> task = new FutureTask<>(() -> method.invoke(targetView, args)); + targetView.post(task); + Object result = task.get(); + Class<?> returnType = method.getReturnType(); + return serializeReturnValue(returnType, returnType.cast(result)); + } catch (Exception e) { + Log.e(TAG, "Exception while invoking method: " + e.getCause().getMessage()); + String msg = e.getCause().getMessage(); + if (msg == null) { + msg = e.getCause().toString(); + } + throw new RuntimeException(msg); } - - return result.get(); } /** @@ -1961,4 +2011,175 @@ public class ViewDebug { */ Bitmap createBitmap(); } + + /** + * Deserializes parameters according to the VUOP_INVOKE_VIEW_METHOD protocol the {@code in} + * buffer. + * + * The length of {@code args} determines how many arguments are read. The {@code argTypes} must + * be the same length, and will be set to the argument types of the data read. + * + * @hide + */ + @VisibleForTesting + public static void deserializeMethodParameters( + Object[] args, Class<?>[] argTypes, ByteBuffer in) throws + ViewMethodInvocationSerializationException { + checkArgument(args.length == argTypes.length); + + for (int i = 0; i < args.length; i++) { + char typeSignature = in.getChar(); + boolean isArray = typeSignature == SIG_ARRAY; + if (isArray) { + char arrayType = in.getChar(); + if (arrayType != SIG_BYTE) { + // This implementation only supports byte-arrays for now. + throw new ViewMethodInvocationSerializationException( + "Unsupported array parameter type (" + typeSignature + + ") to invoke view method @argument " + i); + } + + int arrayLength = in.getInt(); + if (arrayLength > in.remaining()) { + // The sender did not actually sent the specified amount of bytes. This + // avoids a malformed packet to trigger an out-of-memory error. + throw new BufferUnderflowException(); + } + + byte[] byteArray = new byte[arrayLength]; + in.get(byteArray); + + argTypes[i] = byte[].class; + args[i] = byteArray; + } else { + switch (typeSignature) { + case SIG_BOOLEAN: + argTypes[i] = boolean.class; + args[i] = in.get() != 0; + break; + case SIG_BYTE: + argTypes[i] = byte.class; + args[i] = in.get(); + break; + case SIG_CHAR: + argTypes[i] = char.class; + args[i] = in.getChar(); + break; + case SIG_SHORT: + argTypes[i] = short.class; + args[i] = in.getShort(); + break; + case SIG_INT: + argTypes[i] = int.class; + args[i] = in.getInt(); + break; + case SIG_LONG: + argTypes[i] = long.class; + args[i] = in.getLong(); + break; + case SIG_FLOAT: + argTypes[i] = float.class; + args[i] = in.getFloat(); + break; + case SIG_DOUBLE: + argTypes[i] = double.class; + args[i] = in.getDouble(); + break; + case SIG_STRING: { + argTypes[i] = String.class; + int stringUtf8ByteCount = Short.toUnsignedInt(in.getShort()); + byte[] rawStringBuffer = new byte[stringUtf8ByteCount]; + in.get(rawStringBuffer); + args[i] = new String(rawStringBuffer, StandardCharsets.UTF_8); + break; + } + default: + Log.e(TAG, "arg " + i + ", unrecognized type: " + typeSignature); + throw new ViewMethodInvocationSerializationException( + "Unsupported parameter type (" + typeSignature + + ") to invoke view method."); + } + } + + } + } + + /** + * Serializes {@code value} to the wire protocol of VUOP_INVOKE_VIEW_METHOD. + * @hide + */ + @VisibleForTesting + public static byte[] serializeReturnValue(Class<?> type, Object value) + throws ViewMethodInvocationSerializationException, IOException { + ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(1024); + DataOutputStream dos = new DataOutputStream(byteOutStream); + + if (type.isArray()) { + if (!type.equals(byte[].class)) { + // Only byte arrays are supported currently. + throw new ViewMethodInvocationSerializationException( + "Unsupported array return type (" + type + ")"); + } + byte[] byteArray = (byte[]) value; + dos.writeChar(SIG_ARRAY); + dos.writeChar(SIG_BYTE); + dos.writeInt(byteArray.length); + dos.write(byteArray); + } else if (boolean.class.equals(type)) { + dos.writeChar(SIG_BOOLEAN); + dos.write((boolean) value ? 1 : 0); + } else if (byte.class.equals(type)) { + dos.writeChar(SIG_BYTE); + dos.writeByte((byte) value); + } else if (char.class.equals(type)) { + dos.writeChar(SIG_CHAR); + dos.writeChar((char) value); + } else if (short.class.equals(type)) { + dos.writeChar(SIG_SHORT); + dos.writeShort((short) value); + } else if (int.class.equals(type)) { + dos.writeChar(SIG_INT); + dos.writeInt((int) value); + } else if (long.class.equals(type)) { + dos.writeChar(SIG_LONG); + dos.writeLong((long) value); + } else if (double.class.equals(type)) { + dos.writeChar(SIG_DOUBLE); + dos.writeDouble((double) value); + } else if (float.class.equals(type)) { + dos.writeChar(SIG_FLOAT); + dos.writeFloat((float) value); + } else if (String.class.equals(type)) { + dos.writeChar(SIG_STRING); + dos.writeUTF(value != null ? (String) value : ""); + } else { + dos.writeChar(SIG_VOID); + } + + return byteOutStream.toByteArray(); + } + + // Prefixes for simple primitives. These match the JNI definitions. + private static final char SIG_ARRAY = '['; + private static final char SIG_BOOLEAN = 'Z'; + private static final char SIG_BYTE = 'B'; + private static final char SIG_SHORT = 'S'; + private static final char SIG_CHAR = 'C'; + private static final char SIG_INT = 'I'; + private static final char SIG_LONG = 'J'; + private static final char SIG_FLOAT = 'F'; + private static final char SIG_DOUBLE = 'D'; + private static final char SIG_VOID = 'V'; + // Prefixes for some commonly used objects + private static final char SIG_STRING = 'R'; + + /** + * @hide + */ + @VisibleForTesting + public static class ViewMethodInvocationSerializationException extends Exception { + ViewMethodInvocationSerializationException(String message) { + super(message); + } + } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index c18aeee52460..c27b2b174587 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -1025,6 +1025,13 @@ public final class ViewRootImpl implements ViewParent, // time for revaluating the idle status before lowering the frame rate. private static final int FRAME_RATE_IDLENESS_REEVALUATE_TIME = 500; + /* + * the variables below are used to determine whther a dVRR feature should be enabled + */ + + // Used to determine whether to suppress boost on typing + private boolean mShouldSuppressBoostOnTyping = false; + /** * A temporary object used so relayoutWindow can return the latest SyncSeqId * system. The SyncSeqId system was designed to work without synchronous relayout @@ -7283,8 +7290,18 @@ public final class ViewRootImpl implements ViewParent, if (direction != 0) { View focused = mView.findFocus(); if (focused != null) { + mAttachInfo.mNextFocusLooped = false; View v = focused.focusSearch(direction); if (v != null && v != focused) { + if (mAttachInfo.mNextFocusLooped) { + // The next focus is looped. Let's try to move the focus to the adjacent + // window. Note: we still need to move the focus in this window + // regardless of what moveFocusToAdjacentWindow returns, so the focus + // can be looped back from the focus in the adjacent window to next + // focus of this window. + moveFocusToAdjacentWindow(direction); + } + // do the math the get the interesting rect // of previous focused into the coord system of // newly focused view @@ -12275,7 +12292,7 @@ public final class ViewRootImpl implements ViewParent, boolean desiredAction = motionEventAction == MotionEvent.ACTION_DOWN || motionEventAction == MotionEvent.ACTION_MOVE || motionEventAction == MotionEvent.ACTION_UP; - boolean undesiredType = windowType == TYPE_INPUT_METHOD; + boolean undesiredType = windowType == TYPE_INPUT_METHOD && mShouldSuppressBoostOnTyping; // use toolkitSetFrameRate flag to gate the change return desiredAction && !undesiredType && sToolkitSetFrameRateReadOnlyFlagValue && getFrameRateBoostOnTouchEnabled(); diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index c78826116426..38cf4907cbe7 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -128,8 +128,10 @@ import android.window.TrustedPresentationThresholds; import com.android.window.flags.Flags; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -6117,4 +6119,65 @@ public interface WindowManager extends ViewManager { throw new UnsupportedOperationException( "getDefaultToken is not implemented"); } + + /** @hide */ + @Target(ElementType.TYPE_USE) + @IntDef( + prefix = {"SCREEN_RECORDING_STATE"}, + value = {SCREEN_RECORDING_STATE_NOT_VISIBLE, SCREEN_RECORDING_STATE_VISIBLE}) + @Retention(RetentionPolicy.SOURCE) + @interface ScreenRecordingState {} + + /** Indicates the app that registered the callback is not visible in screen recording. */ + @FlaggedApi(com.android.window.flags.Flags.FLAG_SCREEN_RECORDING_CALLBACKS) + int SCREEN_RECORDING_STATE_NOT_VISIBLE = 0; + + /** Indicates the app that registered the callback is visible in screen recording. */ + @FlaggedApi(com.android.window.flags.Flags.FLAG_SCREEN_RECORDING_CALLBACKS) + int SCREEN_RECORDING_STATE_VISIBLE = 1; + + /** + * Adds a screen recording callback. The callback will be invoked whenever the app becomes + * visible in screen recording or was visible in screen recording and becomes invisible in + * screen recording. + * + * <p>An app is considered visible in screen recording if any activities owned by the + * registering process's UID are being recorded. + * + * <p>Example: + * + * <pre> + * windowManager.addScreenRecordingCallback(state -> { + * // handle change in screen recording state + * }); + * </pre> + * + * @param executor The executor on which callback method will be invoked. + * @param callback The callback that will be invoked when screen recording visibility changes. + * @return the current screen recording state. + * @see #SCREEN_RECORDING_STATE_NOT_VISIBLE + * @see #SCREEN_RECORDING_STATE_VISIBLE + */ + @SuppressLint("AndroidFrameworkRequiresPermission") + @RequiresPermission(permission.DETECT_SCREEN_RECORDING) + @FlaggedApi(com.android.window.flags.Flags.FLAG_SCREEN_RECORDING_CALLBACKS) + default @ScreenRecordingState int addScreenRecordingCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<@ScreenRecordingState Integer> callback) { + throw new UnsupportedOperationException(); + } + + /** + * Removes a screen recording callback. + * + * @param callback The callback to remove. + * @see #addScreenRecordingCallback(Executor, Consumer) + */ + @SuppressLint("AndroidFrameworkRequiresPermission") + @RequiresPermission(permission.DETECT_SCREEN_RECORDING) + @FlaggedApi(com.android.window.flags.Flags.FLAG_SCREEN_RECORDING_CALLBACKS) + default void removeScreenRecordingCallback( + @NonNull Consumer<@ScreenRecordingState Integer> callback) { + throw new UnsupportedOperationException(); + } } diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index 5072ad755cba..eaf45c488634 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -20,6 +20,8 @@ import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW; import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW; import static android.window.WindowProviderService.isWindowProviderService; +import static com.android.window.flags.Flags.screenRecordingCallbacks; + import android.annotation.CallbackExecutor; import android.annotation.IntRange; import android.annotation.NonNull; @@ -551,4 +553,25 @@ public final class WindowManagerImpl implements WindowManager { public IBinder getSurfaceControlInputClientToken(@NonNull SurfaceControl surfaceControl) { return mGlobal.getSurfaceControlInputClientToken(surfaceControl); } + + @Override + public @ScreenRecordingState int addScreenRecordingCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<@ScreenRecordingState Integer> callback) { + if (screenRecordingCallbacks()) { + Objects.requireNonNull(executor, "executor must not be null"); + Objects.requireNonNull(callback, "callback must not be null"); + return ScreenRecordingCallbacks.getInstance().addCallback(executor, callback); + } + return SCREEN_RECORDING_STATE_NOT_VISIBLE; + } + + @Override + public void removeScreenRecordingCallback( + @NonNull Consumer<@ScreenRecordingState Integer> callback) { + if (screenRecordingCallbacks()) { + Objects.requireNonNull(callback, "callback must not be null"); + ScreenRecordingCallbacks.getInstance().removeCallback(callback); + } + } } diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index ef1bf5a5c548..146b576a8d1f 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -2424,7 +2424,7 @@ public final class AccessibilityManager { } } try { - service.attachAccessibilityOverlayToDisplay_enforcePermission( + service.attachAccessibilityOverlayToDisplay( displayId, surfaceControl); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 1c5d29e0ff1e..eca15869f581 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -139,5 +139,5 @@ interface IAccessibilityManager { WindowTransformationSpec getWindowTransformationSpec(int windowId); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)") - void attachAccessibilityOverlayToDisplay_enforcePermission(int displayId, in SurfaceControl surfaceControl); + void attachAccessibilityOverlayToDisplay(int displayId, in SurfaceControl surfaceControl); } diff --git a/core/java/android/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig index 0aa516e08697..9d613bcae29a 100644 --- a/core/java/android/view/flags/refresh_rate_flags.aconfig +++ b/core/java/android/view/flags/refresh_rate_flags.aconfig @@ -50,4 +50,28 @@ flag { description: "Feature flag for toolkit metrics collecting for frame rate decision" bug: "301343249" is_fixed_read_only: true +} + +flag { + name: "toolkit_frame_rate_default_normal_read_only" + namespace: "toolkit" + description: "Feature flag for setting frame rate category as NORMAL for default" + bug: "239979904" + is_fixed_read_only: true +} + +flag { + name: "toolkit_frame_rate_by_size_read_only" + namespace: "toolkit" + description: "Feature flag for setting frame rate category based on size" + bug: "239979904" + is_fixed_read_only: true +} + +flag { + name: "toolkit_frame_rate_velocity_mapping_read_only" + namespace: "toolkit" + description: "Feature flag for setting frame rate based on velocity" + bug: "239979904" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java index c6271d27cb37..2f765ae6de38 100644 --- a/core/java/android/webkit/URLUtil.java +++ b/core/java/android/webkit/URLUtil.java @@ -16,7 +16,6 @@ package android.webkit; -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.Compatibility; @@ -47,8 +46,7 @@ public final class URLUtil { */ @ChangeId @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) - @FlaggedApi(android.os.Flags.FLAG_ANDROID_OS_BUILD_VANILLA_ICE_CREAM) - public static final long PARSE_CONTENT_DISPOSITION_USING_RFC_6266 = 319400769L; + static final long PARSE_CONTENT_DISPOSITION_USING_RFC_6266 = 319400769L; private static final String LOGTAG = "webkit"; private static final boolean TRACE = false; diff --git a/core/java/android/widget/flags/notification_widget_flags.aconfig b/core/java/android/widget/flags/notification_widget_flags.aconfig new file mode 100644 index 000000000000..9f0b7c3dfb02 --- /dev/null +++ b/core/java/android/widget/flags/notification_widget_flags.aconfig @@ -0,0 +1,8 @@ +package: "android.widget.flags" + +flag { + name: "notif_linearlayout_optimized" + namespace: "systemui" + description: "Enables notification specific LinearLayout optimization" + bug: "316110233" +}
\ No newline at end of file diff --git a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java index ce6af49eef0b..5cda3f2b2bc0 100644 --- a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java +++ b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java @@ -16,16 +16,30 @@ package com.android.internal.widget; +import static android.app.Notification.CallStyle.DEBUG_NEW_ACTION_LAYOUT; +import static android.app.Notification.CallStyle.USE_NEW_ACTION_LAYOUT; +import static android.text.style.DynamicDrawableSpan.ALIGN_CENTER; + +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.res.ColorStateList; +import android.content.res.TypedArray; import android.graphics.BlendMode; +import android.graphics.Canvas; +import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.graphics.drawable.DrawableWrapper; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.Icon; import android.graphics.drawable.RippleDrawable; +import android.text.SpannableStringBuilder; +import android.text.TextPaint; +import android.text.style.ImageSpan; +import android.text.style.MetricAffectingSpan; +import android.text.style.ReplacementSpan; import android.util.AttributeSet; +import android.util.Log; import android.view.RemotableViewMethod; import android.widget.Button; import android.widget.RemoteViews; @@ -43,6 +57,14 @@ public class EmphasizedNotificationButton extends Button { private final GradientDrawable mBackground; private boolean mPriority; + private int mInitialDrawablePadding; + private int mIconSize; + + private Drawable mIconToGlue; + private CharSequence mLabelToGlue; + private int mGluedLayoutDirection = LAYOUT_DIRECTION_UNDEFINED; + private boolean mGluePending; + public EmphasizedNotificationButton(Context context) { this(context, null); } @@ -58,10 +80,25 @@ public class EmphasizedNotificationButton extends Button { public EmphasizedNotificationButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); + mRipple = (RippleDrawable) getBackground(); mRipple.mutate(); DrawableWrapper inset = (DrawableWrapper) mRipple.getDrawable(0); mBackground = (GradientDrawable) inset.getDrawable(); + + mIconSize = mContext.getResources().getDimensionPixelSize( + R.dimen.notification_actions_icon_drawable_size); + + try (TypedArray typedArray = context.obtainStyledAttributes( + attrs, android.R.styleable.TextView, defStyleAttr, defStyleRes)) { + mInitialDrawablePadding = typedArray.getDimensionPixelSize( + android.R.styleable.TextView_drawablePadding, 0); + } + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "iconSize = " + mIconSize + "px, " + + "initialDrawablePadding = " + mInitialDrawablePadding + "px"); + } } @RemotableViewMethod @@ -95,19 +132,248 @@ public class EmphasizedNotificationButton extends Button { return () -> setImageDrawable(drawable); } - private void setImageDrawable(Drawable drawable) { + private void setImageDrawable(@Nullable Drawable drawable) { if (drawable != null) { - drawable.mutate(); - drawable.setTintList(getTextColors()); - drawable.setTintBlendMode(BlendMode.SRC_IN); - int iconSize = mContext.getResources().getDimensionPixelSize( - R.dimen.notification_actions_icon_drawable_size); - drawable.setBounds(0, 0, iconSize, iconSize); + prepareIcon(drawable); } setCompoundDrawablesRelative(drawable, null, null, null); } /** + * Sets an icon to be 'glued' to the label when this button is displayed, so the icon will stay + * with the text if the button is wider than needed and the text isn't start-aligned. + * + * As with {@link #setImageIcon(Icon)}, the Icon will have its size constrained and will be set + * to the same color as the text, and this must be called after {@link #setTextColor(int)} for + * the latter to work. + * + * This must be called along with {@link #glueLabel(CharSequence)}, in any order, before the + * button is displayed. + */ + @RemotableViewMethod(asyncImpl = "glueIconAsync") + public void glueIcon(@Nullable Icon icon) { + final Drawable drawable = icon == null ? null : icon.loadDrawable(mContext); + setIconToGlue(drawable); + } + + /** + * @hide + */ + @RemotableViewMethod + public Runnable glueIconAsync(@Nullable Icon icon) { + final Drawable drawable = icon == null ? null : icon.loadDrawable(mContext); + return () -> setIconToGlue(drawable); + } + + private void setIconToGlue(@Nullable Drawable icon) { + if (!USE_NEW_ACTION_LAYOUT) { + Log.e(TAG, "glueIcon: new action layout disabled; doing nothing"); + return; + } + + prepareIcon(icon); + + mIconToGlue = icon; + mGluePending = true; + + glueIconAndLabelIfNeeded(); + } + + private void prepareIcon(@NonNull Drawable drawable) { + drawable.mutate(); + drawable.setTintList(getTextColors()); + drawable.setTintBlendMode(BlendMode.SRC_IN); + drawable.setBounds(0, 0, mIconSize, mIconSize); + } + + /** + * Sets a label to be 'glued' to the icon when this button is displayed, so the icon will stay + * with the text if the button is wider than needed and the text isn't start-aligned. + * + * This must be called along with {@link #glueIcon(Icon)}, in any order, before the button is + * displayed. + */ + @RemotableViewMethod(asyncImpl = "glueLabelAsync") + public void glueLabel(@Nullable CharSequence label) { + setLabelToGlue(label); + } + + /** + * @hide + */ + @RemotableViewMethod + public Runnable glueLabelAsync(@Nullable CharSequence label) { + return () -> setLabelToGlue(label); + } + + private void setLabelToGlue(@Nullable CharSequence label) { + if (!USE_NEW_ACTION_LAYOUT) { + Log.e(TAG, "glueLabel: new action layout disabled; doing nothing"); + return; + } + + mLabelToGlue = label; + mGluePending = true; + + glueIconAndLabelIfNeeded(); + } + + @Override + public void onRtlPropertiesChanged(int layoutDirection) { + super.onRtlPropertiesChanged(layoutDirection); + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "onRtlPropertiesChanged: layoutDirection = " + layoutDirection + ", " + + "gluedLayoutDirection = " + mGluedLayoutDirection); + } + + if (layoutDirection != mGluedLayoutDirection) { + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "onRtlPropertiesChanged: layout direction changed; regluing"); + } + mGluePending = true; + } + + glueIconAndLabelIfNeeded(); + } + + private void glueIconAndLabelIfNeeded() { + // Don't need to glue: + + if (!mGluePending) { + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "glueIconAndLabelIfNeeded: glue not pending; doing nothing"); + } + return; + } + + if (mIconToGlue == null && mLabelToGlue == null) { + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "glueIconAndLabelIfNeeded: no icon or label to glue; doing nothing"); + } + mGluePending = false; + return; + } + + if (!USE_NEW_ACTION_LAYOUT) { + Log.e(TAG, "glueIconAndLabelIfNeeded: new action layout disabled; doing nothing"); + return; + } + + // Not ready to glue yet: + + if (!isLayoutDirectionResolved()) { + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "glueIconAndLabelIfNeeded: " + + "layout direction not resolved; doing nothing"); + } + return; + } + + // Ready to glue but don't have an icon *and* a label: + // + // (Note that this will *not* happen while the button is being initialized, since we won't + // be ready to glue. This can only happen if the button is initialized and displayed and + // *then* someone calls glueIcon or glueLabel. + + if (mIconToGlue == null) { + Log.w(TAG, "glueIconAndLabelIfNeeded: label glued without icon; doing nothing"); + return; + } + + if (mLabelToGlue == null) { + Log.w(TAG, "glueIconAndLabelIfNeeded: icon glued without label; doing nothing"); + return; + } + + // Can't glue: + + final int layoutDirection = getLayoutDirection(); + if (layoutDirection != LAYOUT_DIRECTION_LTR && layoutDirection != LAYOUT_DIRECTION_RTL) { + Log.e(TAG, "glueIconAndLabelIfNeeded: " + + "resolved layout direction neither LTR nor RTL; " + + "doing nothing"); + return; + } + + // No excuses left, let's glue it! + + glueIconAndLabel(layoutDirection); + + mGluePending = false; + mGluedLayoutDirection = layoutDirection; + } + + // Unicode replacement character + private static final String IMAGE_SPAN_TEXT = "\ufffd"; + + // Unicode no-break space + private static final String SPACER_SPAN_TEXT = "\u00a0"; + + private static final String LEFT_TO_RIGHT_ISOLATE = "\u2066"; + private static final String RIGHT_TO_LEFT_ISOLATE = "\u2067"; + private static final String FIRST_STRONG_ISOLATE = "\u2068"; + private static final String POP_DIRECTIONAL_ISOLATE = "\u2069"; + + private void glueIconAndLabel(int layoutDirection) { + final boolean rtlLayout = layoutDirection == LAYOUT_DIRECTION_RTL; + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "glueIconAndLabel: " + + "icon = " + mIconToGlue + ", " + + "iconSize = " + mIconSize + "px, " + + "initialDrawablePadding = " + mInitialDrawablePadding + "px, " + + "labelToGlue.length = " + mLabelToGlue.length() + ", " + + "rtlLayout = " + rtlLayout); + } + + logIfTextDirectionNotFirstStrong(); + + final SpannableStringBuilder builder = new SpannableStringBuilder(); + + // The text direction of the label might not match the layout direction of the button, so + // wrap the entire string in a LEFT-TO-RIGHT ISOLATE or RIGHT-TO-LEFT ISOLATE to match the + // layout direction. This puts the icon, padding, and label in the right order. + builder.append(rtlLayout ? RIGHT_TO_LEFT_ISOLATE : LEFT_TO_RIGHT_ISOLATE); + + appendSpan(builder, IMAGE_SPAN_TEXT, new ImageSpan(mIconToGlue, ALIGN_CENTER)); + appendSpan(builder, SPACER_SPAN_TEXT, new SpacerSpan(mInitialDrawablePadding)); + + // If the text and layout directions are different, we would end up with the *label* in the + // wrong direction, so wrap the label in a FIRST STRONG ISOLATE. This triggers the same + // automatic text direction heuristic that Android uses by default. + builder.append(FIRST_STRONG_ISOLATE); + + appendSpan(builder, mLabelToGlue, new CenterBesideImageSpan(mIconSize)); + + builder.append(POP_DIRECTIONAL_ISOLATE); + builder.append(POP_DIRECTIONAL_ISOLATE); + + setText(builder); + } + + private void logIfTextDirectionNotFirstStrong() { + if (!isTextDirectionResolved()) { + Log.e(TAG, "glueIconAndLabel: text direction not resolved; " + + "letting View assume FIRST STRONG"); + } + final int textDirection = getTextDirection(); + if (textDirection != TEXT_DIRECTION_FIRST_STRONG) { + Log.w(TAG, "glueIconAndLabel: " + + "expected text direction TEXT_DIRECTION_FIRST_STRONG " + + "but found " + textDirection + "; " + + "will use a FIRST STRONG ISOLATE regardless"); + } + } + + private void appendSpan(SpannableStringBuilder builder, CharSequence text, Object span) { + final int spanStart = builder.length(); + builder.append(text); + final int spanEnd = builder.length(); + builder.setSpan(span, spanStart, spanEnd, 0); + } + + /** * Sets whether this view is a priority over its peers (which affects width). * Specifically, this is used by {@link NotificationActionListLayout} to give this view width * priority ahead of user-defined buttons when allocating horizontal space. @@ -123,4 +389,104 @@ public class EmphasizedNotificationButton extends Button { public boolean isPriority() { return mPriority; } + + private static class SpacerSpan extends ReplacementSpan { + private int mWidth; + + SpacerSpan(int width) { + mWidth = width; + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "width = " + mWidth + "px"); + } + } + + + @Override + public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, + @Nullable Paint.FontMetricsInt fontMetrics) { + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "getSize returning " + mWidth + "px"); + } + + return mWidth; + } + + @Override + public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, + float x, int top, int y, int bottom, @NonNull Paint paint) { + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "drawing nothing"); + } + + // Draw nothing, it's a spacer. + } + + private static final String TAG = "SpacerSpan"; + } + + private static class CenterBesideImageSpan extends MetricAffectingSpan { + private int mImageHeight; + + private boolean mMeasured; + private int mBaselineShiftOffset; + + CenterBesideImageSpan(int imageHeight) { + mImageHeight = imageHeight; + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "imageHeight = " + mImageHeight + "px"); + } + } + + @Override + public void updateMeasureState(@NonNull TextPaint textPaint) { + final int textHeight = (int) -textPaint.ascent(); + + /* + * We only need to shift the text *up* if the text is shorter than the image; ImageSpan + * with ALIGN_CENTER will shift the *image* up if the text is taller than the image. + */ + if (textHeight < mImageHeight) { + mBaselineShiftOffset = -(mImageHeight - textHeight) / 2; + } else { + mBaselineShiftOffset = 0; + } + + mMeasured = true; + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "updateMeasureState: " + + "imageHeight = " + mImageHeight + "px, " + + "textHeight = " + textHeight + "px, " + + "baselineShiftOffset = " + mBaselineShiftOffset + "px"); + } + + textPaint.baselineShift += mBaselineShiftOffset; + } + + @Override + public void updateDrawState(TextPaint textPaint) { + if (textPaint == null) { + Log.e(TAG, "updateDrawState: textPaint is null; doing nothing"); + return; + } + + if (!mMeasured) { + Log.e(TAG, "updateDrawState: called without measure; doing nothing"); + return; + } + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "updateDrawState: " + + "baselineShiftOffset = " + mBaselineShiftOffset + "px"); + } + + textPaint.baselineShift += mBaselineShiftOffset; + } + + private static final String TAG = "CenterBesideImageSpan"; + } + + private static final String TAG = "EmphasizedNotificationButton"; } diff --git a/core/java/com/android/internal/widget/NotificationActionListLayout.java b/core/java/com/android/internal/widget/NotificationActionListLayout.java index a7a69c9e43fb..69d254499ef4 100644 --- a/core/java/com/android/internal/widget/NotificationActionListLayout.java +++ b/core/java/com/android/internal/widget/NotificationActionListLayout.java @@ -16,12 +16,16 @@ package com.android.internal.widget; +import static android.app.Notification.CallStyle.DEBUG_NEW_ACTION_LAYOUT; +import static android.app.Notification.CallStyle.USE_NEW_ACTION_LAYOUT; + import android.annotation.DimenRes; import android.app.Notification; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.RippleDrawable; import android.util.AttributeSet; +import android.util.Log; import android.view.Gravity; import android.view.RemotableViewMethod; import android.view.View; @@ -41,13 +45,13 @@ import java.util.Comparator; */ @RemoteViews.RemoteView public class NotificationActionListLayout extends LinearLayout { - private final int mGravity; private int mTotalWidth = 0; private int mExtraStartPadding = 0; private ArrayList<TextViewInfo> mMeasureOrderTextViews = new ArrayList<>(); private ArrayList<View> mMeasureOrderOther = new ArrayList<>(); private boolean mEmphasizedMode; + private boolean mEvenlyDividedMode; private int mDefaultPaddingBottom; private int mDefaultPaddingTop; private int mEmphasizedPaddingTop; @@ -124,6 +128,42 @@ public class NotificationActionListLayout extends LinearLayout { } } + private int measureAndReturnEvenlyDividedWidth(int heightMeasureSpec, int innerWidth) { + final int numChildren = getChildCount(); + int childMarginSum = 0; + for (int i = 0; i < numChildren; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + childMarginSum += lp.leftMargin + lp.rightMargin; + } + } + + final int innerWidthMinusChildMargins = innerWidth - childMarginSum; + final int childWidth = innerWidthMinusChildMargins / mNumNotGoneChildren; + final int childWidthMeasureSpec = + MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY); + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "measuring evenly divided width: " + + "numChildren = " + numChildren + ", " + + "innerWidth = " + innerWidth + "px, " + + "childMarginSum = " + childMarginSum + "px, " + + "innerWidthMinusChildMargins = " + innerWidthMinusChildMargins + "px, " + + "childWidth = " + childWidth + "px, " + + "childWidthMeasureSpec = " + MeasureSpec.toString(childWidthMeasureSpec)); + } + + for (int i = 0; i < numChildren; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + child.measure(childWidthMeasureSpec, heightMeasureSpec); + } + } + + return innerWidth; + } + private int measureAndGetUsedWidth(int widthMeasureSpec, int heightMeasureSpec, int innerWidth, boolean collapsePriorityActions) { final int numChildren = getChildCount(); @@ -208,11 +248,16 @@ public class NotificationActionListLayout extends LinearLayout { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { countAndRebuildMeasureOrder(); final int innerWidth = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight; - int usedWidth = measureAndGetUsedWidth(widthMeasureSpec, heightMeasureSpec, innerWidth, - false /* collapsePriorityButtons */); - if (mNumPriorityChildren != 0 && usedWidth >= innerWidth) { + int usedWidth; + if (mEvenlyDividedMode) { + usedWidth = measureAndReturnEvenlyDividedWidth(heightMeasureSpec, innerWidth); + } else { usedWidth = measureAndGetUsedWidth(widthMeasureSpec, heightMeasureSpec, innerWidth, - true /* collapsePriorityButtons */); + false /* collapsePriorityButtons */); + if (mNumPriorityChildren != 0 && usedWidth >= innerWidth) { + usedWidth = measureAndGetUsedWidth(widthMeasureSpec, heightMeasureSpec, innerWidth, + true /* collapsePriorityButtons */); + } } mTotalWidth = usedWidth + mPaddingRight + mPaddingLeft + mExtraStartPadding; @@ -352,6 +397,38 @@ public class NotificationActionListLayout extends LinearLayout { } /** + * Sets whether the available width should be distributed evenly among the action buttons. + * + * When enabled, the available width (after subtracting this layout's padding and all of the + * buttons' margins) is divided by the number of (not-GONE) buttons, and each button is forced + * to that exact width, even if it is less <em>or more</em> width than they need. + * + * When disabled, the available width is allocated as buttons need; if that exceeds the + * available width, priority buttons are collapsed to just their icon to save space. + * + * @param evenlyDividedMode whether to enable evenly divided mode + */ + @RemotableViewMethod + public void setEvenlyDividedMode(boolean evenlyDividedMode) { + if (evenlyDividedMode && !USE_NEW_ACTION_LAYOUT) { + Log.e(TAG, "setEvenlyDividedMode(true) called with new action layout disabled; " + + "leaving evenly divided mode disabled"); + return; + } + + if (evenlyDividedMode == mEvenlyDividedMode) { + return; + } + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "evenlyDividedMode changed to " + evenlyDividedMode + "; " + + "requesting layout"); + } + mEvenlyDividedMode = evenlyDividedMode; + requestLayout(); + } + + /** * Set whether the list is in a mode where some actions are emphasized. This will trigger an * equal measuring where all actions are full height and change a few parameters like * the padding. @@ -410,4 +487,5 @@ public class NotificationActionListLayout extends LinearLayout { } } + private static final String TAG = "NotificationActionListLayout"; } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 0171f584a838..6be1be4a765e 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -881,6 +881,19 @@ android:description="@string/permdesc_writeContacts" android:protectionLevel="dangerous" /> + <!-- Allows an app to update the verification status of E2EE contact keys owned by other apps. + <p>This permission is only granted to system apps. + <p>Protection level: signature|privileged + @SystemApi + @hide + @FlaggedApi("android.provider.user_keys") + --> + <permission android:name="android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS" + android:permissionGroup="android.permission-group.UNDEFINED" + android:label="@string/permlab_writeVerificationStateE2eeContactKeys" + android:description="@string/permdesc_writeVerificationStateE2eeContactKeys" + android:protectionLevel="signature|privileged" /> + <!-- Allows an application to set default account for new contacts. <p> This permission is only granted to system applications fulfilling the Contacts app role. <p>Protection level: internal|role @@ -2632,6 +2645,13 @@ android:description="@string/permdesc_detectScreenCapture" android:protectionLevel="normal" /> + <!-- Allows an application to get notified when it is being recorded. + <p>Protection level: normal + @FlaggedApi("com.android.window.flags.screen_recording_callbacks") + --> + <permission android:name="android.permission.DETECT_SCREEN_RECORDING" + android:protectionLevel="normal" /> + <!-- ======================================== --> <!-- Permissions for factory reset protection --> <!-- ======================================== --> @@ -3188,6 +3208,14 @@ <permission android:name="android.permission.INTERACT_ACROSS_PROFILES" android:protectionLevel="signature|appop" /> + <!-- Allows applications to access profiles with ACCESS_HIDDEN_PROFILES user property + <p>Protection level: normal + @FlaggedApi("android.multiuser.enable_permission_to_access_hidden_profiles") --> + <permission android:name="android.permission.ACCESS_HIDDEN_PROFILES" + android:label="@string/permlab_accessHiddenProfile" + android:description="@string/permdesc_accessHiddenProfile" + android:protectionLevel="normal" /> + <!-- @SystemApi @hide Allows starting activities across profiles in the same profile group. --> <permission android:name="android.permission.START_CROSS_PROFILE_ACTIVITIES" android:protectionLevel="signature|role" /> @@ -7828,7 +7856,7 @@ android:protectionLevel="normal"/> <!-- @FlaggedApi("android.app.job.backup_jobs_exemption") - Gives applications whose <b>primary use case</b> is to backup or sync content increased + Gives applications with a <b>major use case</b> of backing-up or syncing content increased job execution allowance in order to complete the related work. The jobs must have a valid content URI trigger and network constraint set. <p>This is a special access permission that can be revoked by the system or the user. diff --git a/core/res/res/drawable/ic_satellite_alt_24px.xml b/core/res/res/drawable/ic_satellite_alt_24px.xml new file mode 100644 index 000000000000..f9ca7dc66798 --- /dev/null +++ b/core/res/res/drawable/ic_satellite_alt_24px.xml @@ -0,0 +1,25 @@ +<!-- + 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?android:attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M560,928L560,848Q677,848 758.5,766.5Q840,685 840,568L920,568Q920,643 891.5,708.5Q863,774 814.5,822.5Q766,871 700.5,899.5Q635,928 560,928ZM560,768L560,688Q610,688 645,653Q680,618 680,568L760,568Q760,651 701.5,709.5Q643,768 560,768ZM222,903Q207,903 192,897Q177,891 165,880L23,738Q12,726 6,711Q0,696 0,681Q0,665 6,650.5Q12,636 23,625L150,498Q173,475 207,474.5Q241,474 264,497L314,547L342,519L292,469Q269,446 269,413Q269,380 292,357L349,300Q372,277 405.5,277Q439,277 462,300L512,350L540,322L490,272Q467,249 467,215.5Q467,182 490,159L617,32Q629,20 644,14Q659,8 674,8Q689,8 703.5,14Q718,20 730,32L872,174Q884,185 889.5,199.5Q895,214 895,230Q895,245 889.5,260Q884,275 872,287L745,414Q722,437 688.5,437Q655,437 632,414L582,364L554,392L604,442Q627,465 626.5,498.5Q626,532 603,555L547,611Q524,634 490.5,634Q457,634 434,611L384,561L356,589L406,639Q429,662 428.5,696Q428,730 405,753L278,880Q267,891 252.5,897Q238,903 222,903ZM222,824Q222,824 222,824Q222,824 222,824L264,782L122,640L80,682Q80,682 80,682Q80,682 80,682L222,824ZM307,739L349,697Q349,697 349,697Q349,697 349,697L207,555Q207,555 207,555Q207,555 207,555L165,597L307,739ZM491,555Q491,555 491,555Q491,555 491,555L547,499Q547,499 547,499Q547,499 547,499L405,357Q405,357 405,357Q405,357 405,357L349,413Q349,413 349,413Q349,413 349,413L491,555ZM689,357Q689,357 689,357Q689,357 689,357L731,315L589,173L547,215Q547,215 547,215Q547,215 547,215L689,357ZM774,272L816,230Q816,230 816,230Q816,230 816,230L674,88Q674,88 674,88Q674,88 674,88L632,130L774,272ZM448,456L448,456Q448,456 448,456Q448,456 448,456L448,456Q448,456 448,456Q448,456 448,456L448,456Q448,456 448,456Q448,456 448,456L448,456Q448,456 448,456Q448,456 448,456Z"/> +</vector> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 558bae727415..be96cc2d6362 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -396,6 +396,27 @@ <!-- Displayed when the call forwarding query was set but forwarding is not enabled. --> <string name="cfTemplateRegisteredTime"><xliff:g id="bearer_service_code">{0}</xliff:g>: Not forwarded</string> + <!-- Title of the cellular network security safety center source's status. --> + <string name="scCellularNetworkSecurityTitle">Cellular network security</string> + <!-- Summary of the cellular network security safety center source's status. --> + <string name="scCellularNetworkSecuritySummary">Review settings</string> + <!-- Title of the safety center issue and notification when the phone's identifier is shared over the network. --> + <string name="scIdentifierDisclosureIssueTitle">Device identifier accessed</string> + <!-- Summary of the safety center issue and notification when the phone's identifier is shared over the network. --> + <string name="scIdentifierDisclosureIssueSummary">A network on the <xliff:g id="disclosure_network">%4$s</xliff:g> connection recorded your device\'s unique identifier (IMSI) <xliff:g id="disclosure_count">%1$d</xliff:g> times in the period between <xliff:g id="disclosure_window_start_time">%2$tr</xliff:g> and <xliff:g id="disclosure_window_end_time">%3$tr</xliff:g>.</string> + <!-- Title of the safety center issue and notification when the phone restores an encrypted connection to the network. --> + <string name="scNullCipherIssueEncryptedTitle">Encrypted connection to <xliff:g id="network_name">%1$s</xliff:g></string> + <!-- Summary of the safety center issue and notification when the phone restores an encrypted connection to the network. --> + <string name="scNullCipherIssueEncryptedSummary">You\'re now connected to a more secure cellular network.</string> + <!-- Title of the safety center issue and notification when a connected network is not using encryption. --> + <string name="scNullCipherIssueNonEncryptedTitle">Non-encrypted connection to <xliff:g id="network_name">%1$s</xliff:g></string> + <!-- Summary of the safety center issue and notification when a connected network is not using encryption. --> + <string name="scNullCipherIssueNonEncryptedSummary">You\'re connected to a non-encrypted cellular network. Your calls, messages, and data are vulnerable to interception.</string> + <!-- Label for the button that links to the cellular network security settings. --> + <string name="scNullCipherIssueActionSettings">Cellular security settings</string> + <!-- Label for the button that link to education resourcess about cellular network security settings. --> + <string name="scNullCipherIssueActionLearnMore">Learn more</string> + <!-- android.net.http Error strings --> <skip /> <!-- Displayed when a feature code (non-phone number) is dialed and completes successfully. --> <string name="fcComplete">Feature code complete.</string> @@ -1572,6 +1593,11 @@ <string name="permdesc_setWallpaper">Allows the app to set the system wallpaper.</string> <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_accessHiddenProfile">Access hidden profiles</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_accessHiddenProfile">Allows the app to access hidden profiles.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permlab_setWallpaperHints">adjust your wallpaper size</string> <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permdesc_setWallpaperHints">Allows the app to set the system wallpaper size hints.</string> @@ -2222,6 +2248,11 @@ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this.[CHAR_LIMIT=NONE] --> <string name="permdesc_updatePackagesWithoutUserAction">Allows the holder to update the app it previously installed without user action</string> + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. [CHAR_LIMIT=NONE] --> + <string name="permlab_writeVerificationStateE2eeContactKeys">update the verification states of E2EE contact keys owned by other apps</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this.[CHAR_LIMIT=NONE] --> + <string name="permdesc_writeVerificationStateE2eeContactKeys">Allows the app to update the verification states of E2EE contact keys owned by other apps</string> + <!-- Policy administration --> <!-- Title of policy access to limiting the user's password choices --> @@ -6386,4 +6417,14 @@ ul.</string> <string name="redacted_notification_message"></string> <!-- Notification action title used instead of a notification's normal title sensitive [CHAR_LIMIT=NOTIF_BODY] --> <string name="redacted_notification_action_title"></string> + + <!-- Satellite related messages --> + <!-- Notification title when satellite service is connected. --> + <string name="satellite_notification_title">Auto connected to satellite</string> + <!-- Notification summary when satellite service is connected. [CHAR LIMIT=NONE] --> + <string name="satellite_notification_summary">You can send and receive messages without a mobile or Wi-Fi network</string> + <!-- Invoke "What to expect" dialog of messaging application --> + <string name="satellite_notification_open_message">Open Messages</string> + <!-- Invoke Satellite setting activity of Settings --> + <string name="satellite_notification_how_it_works">How it works</string> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 8b74cbfce5e3..603b9026aeb7 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -954,6 +954,16 @@ <java-symbol type="string" name="roamingText8" /> <java-symbol type="string" name="roamingText9" /> <java-symbol type="string" name="roamingTextSearching" /> + <java-symbol type="string" name="scCellularNetworkSecuritySummary" /> + <java-symbol type="string" name="scCellularNetworkSecurityTitle" /> + <java-symbol type="string" name="scIdentifierDisclosureIssueSummary" /> + <java-symbol type="string" name="scIdentifierDisclosureIssueTitle" /> + <java-symbol type="string" name="scNullCipherIssueActionLearnMore" /> + <java-symbol type="string" name="scNullCipherIssueActionSettings" /> + <java-symbol type="string" name="scNullCipherIssueEncryptedSummary" /> + <java-symbol type="string" name="scNullCipherIssueEncryptedTitle" /> + <java-symbol type="string" name="scNullCipherIssueNonEncryptedSummary" /> + <java-symbol type="string" name="scNullCipherIssueNonEncryptedTitle" /> <java-symbol type="string" name="selected" /> <java-symbol type="string" name="sendText" /> <java-symbol type="string" name="sending" /> @@ -5329,4 +5339,11 @@ <java-symbol type="bool" name="config_showPercentageTextDuringRebootToUpdate" /> <java-symbol type="integer" name="config_minMillisBetweenInputUserActivityEvents" /> + + <!-- System notification for satellite service --> + <java-symbol type="string" name="satellite_notification_title" /> + <java-symbol type="string" name="satellite_notification_summary" /> + <java-symbol type="string" name="satellite_notification_open_message" /> + <java-symbol type="string" name="satellite_notification_how_it_works" /> + <java-symbol type="drawable" name="ic_satellite_alt_24px" /> </resources> diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index f47679991418..1b25d7fa32a4 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -203,12 +203,12 @@ android_ravenwood_test { "androidx.test.uiautomator_uiautomator", "compatibility-device-util-axt", "flag-junit", - "mockito_ravenwood", "platform-test-annotations", "flag-junit", "testng", ], srcs: [ + "src/android/app/ActivityManagerTest.java", "src/android/content/pm/PackageManagerTest.java", "src/android/content/pm/UserInfoTest.java", "src/android/database/CursorWindowTest.java", diff --git a/core/tests/coretests/src/android/app/ActivityManagerTest.java b/core/tests/coretests/src/android/app/ActivityManagerTest.java new file mode 100644 index 000000000000..d930e4d79a3a --- /dev/null +++ b/core/tests/coretests/src/android/app/ActivityManagerTest.java @@ -0,0 +1,83 @@ +/* + * 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.app; + +import static android.app.ActivityManager.PROCESS_STATE_SERVICE; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.os.UserHandle; +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; + +@RunWith(AndroidJUnit4.class) +public class ActivityManagerTest { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Test + public void testSimple() throws Exception { + assertTrue(ActivityManager.isSystemReady()); + assertFalse(ActivityManager.isUserAMonkey()); + assertNotEquals(UserHandle.USER_NULL, ActivityManager.getCurrentUser()); + } + + @Test + public void testCapabilities() throws Exception { + // For the moment mostly want to confirm we don't crash + assertNotNull(ActivityManager.getCapabilitiesSummary(~0)); + ActivityManager.printCapabilitiesFull(new PrintWriter(new ByteArrayOutputStream()), ~0); + ActivityManager.printCapabilitiesSummary(new PrintWriter(new ByteArrayOutputStream()), ~0); + ActivityManager.printCapabilitiesSummary(new StringBuilder(), ~0); + } + + @Test + public void testProcState() throws Exception { + // For the moment mostly want to confirm we don't crash + assertNotNull(ActivityManager.procStateToString(PROCESS_STATE_SERVICE)); + assertNotNull(ActivityManager.processStateAmToProto(PROCESS_STATE_SERVICE)); + assertTrue(ActivityManager.isProcStateBackground(PROCESS_STATE_SERVICE)); + assertFalse(ActivityManager.isProcStateCached(PROCESS_STATE_SERVICE)); + assertFalse(ActivityManager.isForegroundService(PROCESS_STATE_SERVICE)); + assertFalse(ActivityManager.isProcStateConsideredInteraction(PROCESS_STATE_SERVICE)); + } + + @Test + public void testStartResult() throws Exception { + // For the moment mostly want to confirm we don't crash + assertTrue(ActivityManager.isStartResultSuccessful(50)); + assertTrue(ActivityManager.isStartResultFatalError(-50)); + } + + @Test + public void testRestrictionLevel() throws Exception { + // For the moment mostly want to confirm we don't crash + assertNotNull(ActivityManager.restrictionLevelToName( + ActivityManager.RESTRICTION_LEVEL_HIBERNATION)); + } +} diff --git a/core/tests/coretests/src/android/ddm/OWNERS b/core/tests/coretests/src/android/ddm/OWNERS deleted file mode 100644 index c8be1919fb93..000000000000 --- a/core/tests/coretests/src/android/ddm/OWNERS +++ /dev/null @@ -1 +0,0 @@ -michschn@google.com diff --git a/core/tests/coretests/src/android/os/HandlerThreadTest.java b/core/tests/coretests/src/android/os/HandlerThreadTest.java index 0bac1c728f3b..1ad71da7c2d1 100644 --- a/core/tests/coretests/src/android/os/HandlerThreadTest.java +++ b/core/tests/coretests/src/android/os/HandlerThreadTest.java @@ -28,15 +28,20 @@ import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.MediumTest; import androidx.test.runner.AndroidJUnit4; +import org.junit.Assume; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class HandlerThreadTest { private static final int TEST_WHAT = 1; - @Rule + @Rule(order = 1) + public ExpectedException mThrown = ExpectedException.none(); + + @Rule(order = 2) public final RavenwoodRule mRavenwood = new RavenwoodRule(); private boolean mGotMessage = false; @@ -112,4 +117,28 @@ public class HandlerThreadTest { assertTrue(mGotMessage); assertEquals(TEST_WHAT, mGotMessageWhat); } + + /** + * Confirm that a background handler thread throwing an exception during a test results in a + * test failure being reported. + */ + @Test + public void testUncaughtExceptionFails() throws Exception { + // For the moment we can only test Ravenwood; on a physical device uncaught exceptions + // are detected, but reported as test failures at a higher level where we can't inspect + Assume.assumeTrue(RavenwoodRule.isOnRavenwood()); + mThrown.expect(IllegalStateException.class); + + final HandlerThread thread = new HandlerThread("HandlerThreadTest"); + thread.start(); + thread.getThreadHandler().post(() -> { + throw new IllegalStateException(); + }); + + // Wait until we've drained past the message above, then terminate test without throwing + // directly; the test harness should notice and report the uncaught exception + while (!thread.getThreadHandler().getLooper().getQueue().isIdle()) { + SystemClock.sleep(10); + } + } } diff --git a/core/tests/coretests/src/android/os/VibrationAttributesTest.java b/core/tests/coretests/src/android/os/VibrationAttributesTest.java new file mode 100644 index 000000000000..f5a81c582b28 --- /dev/null +++ b/core/tests/coretests/src/android/os/VibrationAttributesTest.java @@ -0,0 +1,38 @@ +/* + * 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.os; + +import static org.junit.Assert.assertEquals; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class VibrationAttributesTest { + @Test + public void testSimple() throws Exception { + final VibrationAttributes attr = new VibrationAttributes.Builder() + .setCategory(VibrationAttributes.CATEGORY_KEYBOARD) + .setUsage(VibrationAttributes.USAGE_ALARM) + .build(); + + assertEquals(VibrationAttributes.CATEGORY_KEYBOARD, attr.getCategory()); + assertEquals(VibrationAttributes.USAGE_ALARM, attr.getUsage()); + } +} diff --git a/core/tests/coretests/src/android/ddm/DdmHandleViewDebugTest.java b/core/tests/coretests/src/android/view/ViewDebugTest.java index 7248983c741c..45228422b97b 100644 --- a/core/tests/coretests/src/android/ddm/DdmHandleViewDebugTest.java +++ b/core/tests/coretests/src/android/view/ViewDebugTest.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package android.ddm; +package android.view; -import static android.ddm.DdmHandleViewDebug.deserializeMethodParameters; -import static android.ddm.DdmHandleViewDebug.serializeReturnValue; +import static android.view.ViewDebug.deserializeMethodParameters; +import static android.view.ViewDebug.serializeReturnValue; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; -import android.ddm.DdmHandleViewDebug.ViewMethodInvocationSerializationException; import android.platform.test.annotations.Presubmit; +import android.view.ViewDebug.ViewMethodInvocationSerializationException; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -39,7 +39,7 @@ import java.util.Arrays; @RunWith(AndroidJUnit4.class) @SmallTest @Presubmit -public final class DdmHandleViewDebugTest { +public final class ViewDebugTest { // true private static final byte[] SERIALIZED_BOOLEAN_TRUE = {0x00, 0x5A, 1}; diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index cf3eb12498ca..60769c7acd45 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -18,6 +18,7 @@ package android.view; import static android.view.accessibility.Flags.FLAG_FORCE_INVERT_COLOR; import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY; +import static android.view.flags.Flags.FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY; import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; @@ -475,8 +476,9 @@ public class ViewRootImplTest { * Also, mIsFrameRateBoosting should be true when the visibility becomes visible */ @Test - @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) - public void votePreferredFrameRate_voteFrameRateCategory_visibility() { + @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, + FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY}) + public void votePreferredFrameRate_voteFrameRateCategory_visibility_bySize() { View view = new View(sContext); attachViewToWindow(view); ViewRootImpl viewRootImpl = view.getViewRootImpl(); @@ -507,8 +509,9 @@ public class ViewRootImplTest { * <7%: FRAME_RATE_CATEGORY_LOW */ @Test - @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) - public void votePreferredFrameRate_voteFrameRateCategory_smallSize() { + @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, + FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY}) + public void votePreferredFrameRate_voteFrameRateCategory_smallSize_bySize() { View view = new View(sContext); WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY); wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check @@ -534,8 +537,9 @@ public class ViewRootImplTest { * >=7% : FRAME_RATE_CATEGORY_NORMAL */ @Test - @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) - public void votePreferredFrameRate_voteFrameRateCategory_normalSize() { + @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, + FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY}) + public void votePreferredFrameRate_voteFrameRateCategory_normalSize_bySize() { View view = new View(sContext); WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY); wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check @@ -559,6 +563,96 @@ public class ViewRootImplTest { } /** + * Test the value of the frame rate cateogry based on the visibility of a view + * Invsible: FRAME_RATE_CATEGORY_NO_PREFERENCE + * Visible: FRAME_RATE_CATEGORY_HIGH + * Also, mIsFrameRateBoosting should be true when the visibility becomes visible + */ + @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) + public void votePreferredFrameRate_voteFrameRateCategory_visibility_defaultHigh() { + View view = new View(sContext); + attachViewToWindow(view); + ViewRootImpl viewRootImpl = view.getViewRootImpl(); + sInstrumentation.runOnMainSync(() -> { + view.setVisibility(View.INVISIBLE); + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), + FRAME_RATE_CATEGORY_NO_PREFERENCE); + }); + sInstrumentation.waitForIdleSync(); + + sInstrumentation.runOnMainSync(() -> { + view.setVisibility(View.VISIBLE); + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), + FRAME_RATE_CATEGORY_HIGH); + }); + sInstrumentation.waitForIdleSync(); + + sInstrumentation.runOnMainSync(() -> { + assertEquals(viewRootImpl.getIsFrameRateBoosting(), true); + }); + } + + /** + * Test the value of the frame rate cateogry based on the size of a view. + * The current threshold value is 7% of the screen size + * <7%: FRAME_RATE_CATEGORY_NORMAL + */ + @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) + public void votePreferredFrameRate_voteFrameRateCategory_smallSize_defaultHigh() { + View view = new View(sContext); + WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY); + wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check + wmlp.width = 1; + wmlp.height = 1; + + sInstrumentation.runOnMainSync(() -> { + WindowManager wm = sContext.getSystemService(WindowManager.class); + wm.addView(view, wmlp); + }); + sInstrumentation.waitForIdleSync(); + + ViewRootImpl viewRootImpl = view.getViewRootImpl(); + sInstrumentation.runOnMainSync(() -> { + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NORMAL); + }); + } + + /** + * Test the value of the frame rate cateogry based on the size of a view. + * The current threshold value is 7% of the screen size + * >=7% : FRAME_RATE_CATEGORY_HIGH + */ + @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) + public void votePreferredFrameRate_voteFrameRateCategory_normalSize_defaultHigh() { + View view = new View(sContext); + WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY); + wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check + + sInstrumentation.runOnMainSync(() -> { + WindowManager wm = sContext.getSystemService(WindowManager.class); + Display display = wm.getDefaultDisplay(); + DisplayMetrics metrics = new DisplayMetrics(); + display.getMetrics(metrics); + wmlp.width = (int) (metrics.widthPixels * 0.9); + wmlp.height = (int) (metrics.heightPixels * 0.9); + wm.addView(view, wmlp); + }); + sInstrumentation.waitForIdleSync(); + + ViewRootImpl viewRootImpl = view.getViewRootImpl(); + sInstrumentation.runOnMainSync(() -> { + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); + }); + } + + /** * Test how values of the frame rate cateogry are aggregated. * It should take the max value among all of the voted categories per frame. */ @@ -701,6 +795,61 @@ public class ViewRootImplTest { }); } + /** + * Test the logic of infrequent layer: + * - NORMAL for infrequent update: FT2-FT1 > 100 && FT3-FT2 > 100. + * - HIGH/NORMAL based on size for frequent update: (FT3-FT2) + (FT2 - FT1) < 100. + * - otherwise, use the previous category value. + */ + @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) + public void votePreferredFrameRate_infrequentLayer_defaultHigh() throws InterruptedException { + final long delay = 200L; + + View view = new View(sContext); + WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY); + wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check + + sInstrumentation.runOnMainSync(() -> { + WindowManager wm = sContext.getSystemService(WindowManager.class); + Display display = wm.getDefaultDisplay(); + DisplayMetrics metrics = new DisplayMetrics(); + display.getMetrics(metrics); + wmlp.width = (int) (metrics.widthPixels * 0.9); + wmlp.height = (int) (metrics.heightPixels * 0.9); + wm.addView(view, wmlp); + }); + sInstrumentation.waitForIdleSync(); + + ViewRootImpl viewRootImpl = view.getViewRootImpl(); + + // Frequent update + sInstrumentation.runOnMainSync(() -> { + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), + FRAME_RATE_CATEGORY_NO_PREFERENCE); + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); + }); + + // In transistion from frequent update to infrequent update + Thread.sleep(delay); + sInstrumentation.runOnMainSync(() -> { + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); + }); + + // Infrequent update + Thread.sleep(delay); + sInstrumentation.runOnMainSync(() -> { + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NORMAL); + }); + } + @Test public void forceInvertOffDarkThemeOff_forceDarkModeDisabled() { mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR); diff --git a/core/tests/utiltests/Android.bp b/core/tests/utiltests/Android.bp index 2ccee71c8ff7..f5563a710563 100644 --- a/core/tests/utiltests/Android.bp +++ b/core/tests/utiltests/Android.bp @@ -60,7 +60,6 @@ android_ravenwood_test { static_libs: [ "androidx.annotation_annotation", "androidx.test.rules", - "mockito_ravenwood", "frameworks-base-testutils", "servicestests-utils", ], diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 28734283e9b7..91e620cd4b83 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -185,6 +185,7 @@ applications that come with the platform <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG"/> <permission name="android.permission.UWB_PRIVILEGED"/> <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> + <permission name="android.permission.SEND_SAFETY_CENTER_UPDATE" /> </privapp-permissions> <privapp-permissions package="com.android.providers.calendar"> @@ -571,6 +572,8 @@ applications that come with the platform <!-- Permission required for BinaryTransparencyService shell API and host test --> <permission name="android.permission.GET_BACKGROUND_INSTALLED_PACKAGES" /> <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> + <!-- Permissions required for CTS test - CtsContactKeysProviderPrivilegedApp --> + <permission name="android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS"/> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 2b95f30a5e8a..9a66c0fa9eb9 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -57,3 +57,10 @@ flag { description: "Enables new UMO experience for PiP menu" bug: "307998712" } + +flag { + name: "enable_bubble_bar" + namespace: "multitasking" + description: "Enables the new bubble bar UI for tablets" + bug: "286246694" +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index f32f030ff06e..50a58daa6363 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -465,7 +465,7 @@ public class Bubble implements BubbleViewProvider { /** * Call when all the views should be removed/cleaned up. */ - void cleanupViews() { + public void cleanupViews() { cleanupViews(true); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index aea3ca1576e2..d0db7080a099 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -730,7 +730,7 @@ public class BubbleController implements ConfigurationChangeListener, // window to show this in, but we use a separate code path. // TODO(b/273312602): consider foldables where we do need a stack view when folded if (mLayerView == null) { - mLayerView = new BubbleBarLayerView(mContext, this); + mLayerView = new BubbleBarLayerView(mContext, this, mBubbleData); mLayerView.setUnBubbleConversationCallback(mSysuiProxy::onUnbubbleConversation); } } else { @@ -1714,8 +1714,7 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void removeBubble(Bubble removedBubble) { if (mLayerView != null) { - // TODO: need to check if there's something that needs to happen here, e.g. if - // the currently selected & expanded bubble is removed? + mLayerView.removeBubble(removedBubble); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 127c7e8d9378..dbfa2606ea06 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -15,7 +15,6 @@ */ package com.android.wm.shell.bubbles; -import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE; import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA; @@ -41,6 +40,7 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubbles.DismissReason; +import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import com.android.wm.shell.common.bubbles.RemovedBubble; @@ -427,7 +427,7 @@ public class BubbleData { /** * When this method is called it is expected that all info in the bubble has completed loading. * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleController, BubbleStackView, - * BubbleIconFactory, boolean) + * BubbleBarLayerView, BubbleIconFactory, boolean) */ void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { if (DEBUG_BUBBLE_DATA) { @@ -1069,7 +1069,6 @@ public class BubbleData { /** * The set of bubbles in row. */ - @VisibleForTesting(visibility = PACKAGE) public List<Bubble> getBubbles() { return Collections.unmodifiableList(mBubbles); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index 5fc67d7b5f00..dc271331c8f9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -31,6 +31,7 @@ import android.content.Intent; import android.graphics.Rect; import android.util.Log; import android.view.View; +import android.view.ViewGroup; import androidx.annotation.Nullable; @@ -186,6 +187,7 @@ public class BubbleTaskViewHelper { } if (mTaskView != null) { mTaskView.release(); + ((ViewGroup) mParentView).removeView(mTaskView); mTaskView = null; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 00d683e861e0..73a9cf456a6e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -266,13 +266,8 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView mListener.onBackPressed(); } - /** Cleans up task view, should be called when the bubble is no longer active. */ + /** Cleans up the expanded view, should be called when the bubble is no longer active. */ public void cleanUpExpandedState() { - if (mBubbleTaskViewHelper != null) { - if (mTaskView != null) { - removeView(mTaskView); - } - } mMenuViewController.hideMenu(false /* animated */); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index bd8ce803c591..b95d258da6fd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -34,7 +34,9 @@ import android.view.WindowManager; import android.widget.FrameLayout; import com.android.wm.shell.R; +import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleController; +import com.android.wm.shell.bubbles.BubbleData; import com.android.wm.shell.bubbles.BubbleOverflow; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleViewProvider; @@ -61,6 +63,7 @@ public class BubbleBarLayerView extends FrameLayout private static final float SCRIM_ALPHA = 0.2f; private final BubbleController mBubbleController; + private final BubbleData mBubbleData; private final BubblePositioner mPositioner; private final BubbleBarAnimationHelper mAnimationHelper; private final BubbleEducationViewController mEducationViewController; @@ -85,9 +88,10 @@ public class BubbleBarLayerView extends FrameLayout private TouchDelegate mHandleTouchDelegate; private final Rect mHandleTouchBounds = new Rect(); - public BubbleBarLayerView(Context context, BubbleController controller) { + public BubbleBarLayerView(Context context, BubbleController controller, BubbleData bubbleData) { super(context); mBubbleController = controller; + mBubbleData = bubbleData; mPositioner = mBubbleController.getPositioner(); mAnimationHelper = new BubbleBarAnimationHelper(context, @@ -236,15 +240,44 @@ public class BubbleBarLayerView extends FrameLayout showScrim(true); } + /** Removes the given {@code bubble}. */ + public void removeBubble(Bubble bubble) { + if (mBubbleData.getBubbles().isEmpty()) { + // we're removing the last bubble. collapse the expanded view and cleanup bubble views + // at the end. + collapse(bubble::cleanupViews); + } else { + bubble.cleanupViews(); + } + } + /** Collapses any showing expanded view */ public void collapse() { + collapse(/* endAction= */ null); + } + + /** + * Collapses any showing expanded view. + * + * @param endAction an action to run and the end of the collapse animation. + */ + public void collapse(@Nullable Runnable endAction) { + if (!mIsExpanded) { + return; + } mIsExpanded = false; final BubbleBarExpandedView viewToRemove = mExpandedView; mEducationViewController.hideEducation(/* animated = */ true); + Runnable runnable = () -> { + removeView(viewToRemove); + if (endAction != null) { + endAction.run(); + } + }; if (mDragController != null && mDragController.isStuckToDismiss()) { - mAnimationHelper.animateDismiss(() -> removeView(viewToRemove)); + mAnimationHelper.animateDismiss(runnable); } else { - mAnimationHelper.animateCollapse(() -> removeView(viewToRemove)); + mAnimationHelper.animateCollapse(runnable); } mBubbleController.getSysuiProxy().onStackExpandChanged(false); mExpandedView = null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt new file mode 100644 index 000000000000..fd91ac0affc5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.window.WindowContainerTransaction +import com.android.wm.shell.sysui.ShellCommandHandler +import java.io.PrintWriter + +/** + * Handles the shell commands for the DesktopTasksController. + */ +class DesktopModeShellCommandHandler(private val controller: DesktopTasksController) : + ShellCommandHandler.ShellCommandActionHandler { + + override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean { + return when (args[0]) { + "moveToDesktop" -> { + if (!runMoveToDesktop(args, pw)) { + pw.println("Task not found. Please enter a valid taskId.") + false + } else { + true + } + } + + else -> { + pw.println("Invalid command: ${args[0]}") + false + } + } + } + + private fun runMoveToDesktop(args: Array<String>, pw: PrintWriter): Boolean { + if (args.size < 2) { + // First argument is the action name. + pw.println("Error: task id should be provided as arguments") + return false + } + + val taskId = try { + args[1].toInt() + } catch (e: NumberFormatException) { + pw.println("Error: task id should be an integer") + return false + } + + return controller.moveToDesktopWithoutDecor(taskId, WindowContainerTransaction()) + } + + override fun printShellCommandHelp(pw: PrintWriter, prefix: String) { + pw.println("$prefix moveToDesktop <taskId> ") + pw.println("$prefix Move a task with given id to desktop mode.") + } +}
\ 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 a089e81ff6dd..e8728498ad64 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 @@ -100,6 +100,9 @@ class DesktopTasksController( private val desktopMode: DesktopModeImpl private var visualIndicator: DesktopModeVisualIndicator? = null + private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler = + DesktopModeShellCommandHandler(this) + private val mOnAnimationFinishedCallback = Consumer<SurfaceControl.Transaction> { t: SurfaceControl.Transaction -> visualIndicator?.releaseVisualIndicator(t) @@ -148,6 +151,8 @@ class DesktopTasksController( private fun onInit() { KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController") shellCommandHandler.addDumpCallback(this::dump, this) + shellCommandHandler.addCommandCallback("desktopmode", desktopModeShellCommandHandler, + this) shellController.addExternalInterface( ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE, { createExternalInterface() }, @@ -240,6 +245,40 @@ class DesktopTasksController( } } + /** Move a task with given `taskId` to desktop without decor */ + fun moveToDesktopWithoutDecor( + taskId: Int, + wct: WindowContainerTransaction + ): Boolean { + val task = shellTaskOrganizer.getRunningTaskInfo(taskId) ?: return false + moveToDesktopWithoutDecor(task, wct) + return true + } + + /** + * Move a task to desktop without decor + */ + private fun moveToDesktopWithoutDecor( + task: RunningTaskInfo, + wct: WindowContainerTransaction + ) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: moveToDesktopWithoutDecor taskId=%d", + task.taskId + ) + exitSplitIfApplicable(wct, task) + // Bring other apps to front first + bringDesktopAppsToFront(task.displayId, wct) + addMoveToDesktopChanges(wct, task) + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + /** * Move a task to desktop */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md index 73a7348d5aca..3fad28ad232f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md @@ -15,4 +15,4 @@ particular order): Todo - Per-feature docs - Feature flagging -- Best practices
\ No newline at end of file +- Best practices & patterns
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md index fbf326eadcd5..9aa5f4ffcd78 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md @@ -102,5 +102,5 @@ AIDL interfaces and constants. Currently, all AIDL files, and classes under the Launcher uses. If the new code doesn't fall into those categories, they can be added explicitly in the Shell's -[Android.bp](frameworks/base/libs/WindowManager/Shell/Android.bp) file under the +[Android.bp](/libs/WindowManager/Shell/Android.bp) file under the `wm_shell_util-sources` filegroup.
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md index 6c01d962adc9..7070dead9957 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md @@ -21,16 +21,16 @@ developers to jump into a few select files and understand how different componen (especially as products override components). The module dependency tree looks a bit like: -- [WMShellConcurrencyModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java) +- [WMShellConcurrencyModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java) (provides threading-related components) - - [WMShellBaseModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java) + - [WMShellBaseModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java) (provides components that are likely common to all products, ie. DisplayController, Transactions, etc.) - - [WMShellModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java) + - [WMShellModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java) (phone/tablet specific components only) - - [TvPipModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java) + - [TvPipModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java) (PIP specific components for TV) - - [TvWMShellModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java) + - [TvWMShellModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java) (TV specific components only) - etc. @@ -43,7 +43,7 @@ In some rare cases, there are base components that can change behavior depending product it runs on. If there are hooks that can be added to the component, that is the preferable approach. -The alternative is to use the [@DynamicOverride](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java) +The alternative is to use the [@DynamicOverride](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java) annotation to allow the product module to provide an implementation that the base module can reference. This is most useful if the existence of the entire component is controlled by the product and the override implementation is optional (there is a default implementation). More diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md index f9ea1d4e2a07..438aa768165e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md @@ -29,30 +29,78 @@ building to check the log state (is enabled) before printing the print format st ### Kotlin Protolog tool does not yet have support for Kotlin code (see [b/168581922](https://b.corp.google.com/issues/168581922)). -For logging in Kotlin, use the [KtProtoLog](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt) +For logging in Kotlin, use the [KtProtoLog](/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt) class which has a similar API to the Java ProtoLog class. ### Enabling ProtoLog command line logging -Run these commands to enable protologs for both WM Core and WM Shell to print to logcat. +Run these commands to enable protologs (in logcat) for WM Core ([list of all core tags](/core/java/com/android/internal/protolog/ProtoLogGroup.java)): ```shell -adb shell wm logging enable-text NEW_FEATURE -adb shell wm logging disable-text NEW_FEATURE +adb shell wm logging enable-text TAG +adb shell wm logging disable-text TAG +``` + +And these commands to enable protologs (in logcat) for WM Shell ([list of all shell tags](/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java)): +```shell +adb shell dumpsys activity service SystemUIService WMShell protolog enable-text TAG +adb shell dumpsys activity service SystemUIService WMShell protolog enable-text TAG ``` ## Winscope Tracing The Winscope tool is extremely useful in determining what is happening on-screen in both WindowManager and SurfaceFlinger. Follow [go/winscope](http://go/winscope-help) to learn how to -use the tool. +use the tool. This trace will contain all the information about the windows/activities/surfaces on +screen. + +## WindowManager/SurfaceFlinger hierarchy dump + +A quick way to view the WindowManager hierarchy without a winscope trace is via the wm dumps: +```shell +adb shell dumpsys activity containers +``` + +Likewise, the SurfaceFlinger hierarchy can be dumped for inspection by running: +```shell +adb shell dumpsys SurfaceFlinger +# Search output for "Layer Hierarchy" +``` + +## Tracing global SurfaceControl transaction updates -In addition, there is limited preliminary support for Winscope tracing componetns in the Shell, -which involves adding trace fields to [wm_shell_trace.proto](frameworks/base/libs/WindowManager/Shell/proto/wm_shell_trace.proto) -file and ensure it is updated as a part of `WMShell#writeToProto`. +While Winscope traces are very useful, it sometimes doesn't give you enough information about which +part of the code is initiating the transaction updates. In such cases, it can be helpful to get +stack traces when specific surface transaction calls are made, which is possible by enabling the +following system properties for example: +```shell +# Enabling +adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha # matches the name of the SurfaceControlTransaction method +adb shell setprop persist.wm.debug.sc.tx.log_match_name com.android.systemui # matches the name of the surface +adb reboot +adb logcat -s "SurfaceControlRegistry" + +# Disabling logging +adb shell setprop persist.wm.debug.sc.tx.log_match_call \"\" +adb shell setprop persist.wm.debug.sc.tx.log_match_name \"\" +adb reboot +``` + +It is not necessary to set both `log_match_call` and `log_match_name`, but note logs can be quite +noisy if unfiltered. -Tracing can be started via the shell command (to be added to the Winscope tool as needed): +## Tracing activity starts in the app process + +It's sometimes useful to know when to see a stack trace of when an activity starts in the app code +(ie. if you are repro'ing a bug related to activity starts). You can enable this system property to +get this trace: ```shell -adb shell cmd statusbar tracing start -adb shell cmd statusbar tracing stop +# Enabling +adb shell setprop persist.wm.debug.start_activity true +adb reboot +adb logcat -s "Instrumentation" + +# Disabling +adb shell setprop persist.wm.debug.start_activity \"\" +adb reboot ``` ## Dumps @@ -69,6 +117,21 @@ If information should be added to the dump, either: - Update `WMShell` if you are dumping SysUI state - Inject `ShellCommandHandler` into your Shell class, and add a dump callback +## Shell commands + +It can be useful to add additional shell commands to drive and test specific interactions. + +To add a new command for your feature, inject a `ShellCommandHandler` into your class and add a +shell command handler in your controller. + +```shell +# List all available commands +adb shell dumpsys activity service SystemUIService WMShell help + +# Run a specific command +adb shell dumpsys activity service SystemUIService WMShell <cmd> <args> ... +``` + ## Debugging in Android Studio If you are using the [go/sysui-studio](http://go/sysui-studio) project, then you can debug Shell diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md index a88ef6aea2ec..b489fe8ea1a9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md @@ -19,25 +19,24 @@ Currently, the WMShell library is used to drive the windowing experience on hand ## Where does the code live -The core WMShell library code is currently located in the [frameworks/base/libs/WindowManager/Shell](frameworks/base/libs/WindowManager/Shell) +The core WMShell library code is currently located in the [frameworks/base/libs/WindowManager/Shell](/libs/WindowManager/Shell) directory and is included as a part dependency of the host SystemUI apk. ## How do I build the Shell library -The library can be built directly by running (using [go/makepush](http://go/makepush)): +The library can be built directly by running: ```shell -mp :WindowManager-Shell +m WindowManager-Shell ``` But this is mainly useful for inspecting the contents of the library or verifying it builds. The -various targets can be found in the Shell library's [Android.bp](frameworks/base/libs/WindowManager/Shell/Android.bp) +various targets can be found in the Shell library's [Android.bp](/libs/WindowManager/Shell/Android.bp) file. Normally, you would build it as a part of the host SystemUI, for example via commandline: ```shell # Phone SystemUI variant -mp sysuig -# Building Shell & SysUI changes along w/ framework changes -mp core services sysuig +m SystemUI +adevice update ``` Or preferably, if you are making WMShell/SysUI only changes (no other framework changes), then diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/patterns/TEMPLATE.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/patterns/TEMPLATE.md new file mode 100644 index 000000000000..0e20a8ec275c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/patterns/TEMPLATE.md @@ -0,0 +1,30 @@ +# Pattern (one line description) + +### What this pattern solves + +Give detailed information about the problem that this pattern addresses, include when it +should/should not be used. + +### How it works + +Give a high level overview of how this pattern works technically with sufficient links for the +relevant dependencies for folks to read more. + +### Code examples + +Explain how this pattern is used in code. + +File.kt: +```kotlin +fun someFunction() { + // Use this code +} +``` + +### Relevant links + +Add relevant links to other files that implement this pattern, design docs, or other important +info. + +Link 1: [More info](some_example_link) \ +Link 2: [More info](some_example_link)
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md index d6302e640ba7..30ff6691f503 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md @@ -80,4 +80,6 @@ adb shell dumpsys activity service SystemUIService WMShell # Run a specific command adb shell dumpsys activity service SystemUIService WMShell help adb shell dumpsys activity service SystemUIService WMShell <cmd> <args> ... -```
\ No newline at end of file +``` + +More detail can be found in [Debugging in the Shell](debugging.md) section.
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md index 8a80333facc4..98af930c4486 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md @@ -5,7 +5,7 @@ ## Unit tests New WM Shell unit tests can be added to the -[Shell/tests/unittest](frameworks/base/libs/WindowManager/Shell/tests/unittest) directory, and can +[Shell/tests/unittest](/libs/WindowManager/Shell/tests/unittest) directory, and can be run via command line using `atest`: ```shell atest WMShellUnitTests @@ -25,10 +25,24 @@ Flicker tests are tests that perform actions and make assertions on the state in and SurfaceFlinger traces captured during the run. New WM Shell Flicker tests can be added to the -[Shell/tests/flicker](frameworks/base/libs/WindowManager/Shell/tests/flicker) directory, and can -be run via command line using `atest`: +[Shell/tests/flicker](/libs/WindowManager/Shell/tests/flicker) directory, and can be run via command line using `atest`: ```shell -atest WMShellFlickerTests +# Bubbles +atest WMShellFlickerTestsBubbles + +# PIP +atest WMShellFlickerTestsPip1 +atest WMShellFlickerTestsPip2 +atest WMShellFlickerTestsPip3 +atest WMShellFlickerTestsPipApps +atest WMShellFlickerTestsPipAppsCSuite + +# Splitscreen +atest WMShellFlickerTestsSplitScreenGroup1 +atest WMShellFlickerTestsSplitScreenGroup2 + +# Other +atest WMShellFlickerTestsOther ``` **Note**: Currently Flicker tests can only be run from the commandline and not via SysUI Studio diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md index eac748894432..9d015357b60b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md @@ -43,7 +43,7 @@ the product. ## Dagger setup -The threading-related components are provided by the [WMShellConcurrencyModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java), +The threading-related components are provided by the [WMShellConcurrencyModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java), for example, the Executors and Handlers for the various threads that are used. You can request an executor of the necessary type by using the appropriate annotation for each of the threads (ie. `@ShellMainThread Executor`) when injecting into your Shell component. @@ -76,7 +76,7 @@ To get the SysUI main thread, you can use the `@Main` annotation. want to dedupe multiple messages - In such cases inject `@ShellMainThread Handler` or use view.getHandler() which should be OK assuming that the view root was initialized on the main Shell thread -- **Never use Looper.getMainLooper()** +- <u>**Never</u> use Looper.getMainLooper()** - It's likely going to be wrong, you can inject `@Main ShellExecutor` to get the SysUI main thread ### Testing diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 238e6b5bf2af..c5a01025dcdd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -797,14 +797,20 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsAlgorithm.getMovementBounds(postChangeBounds), mPipBoundsState.getStashedState()); - updateDisplayLayout.run(); + // Scale PiP on density dpi change, so it appears to be the same size physically. + final boolean densityDpiChanged = + mPipDisplayLayoutState.getDisplayLayout().densityDpi() != 0 + && (mPipDisplayLayoutState.getDisplayLayout().densityDpi() + != layout.densityDpi()); + if (densityDpiChanged) { + final float scale = (float) layout.densityDpi() + / mPipDisplayLayoutState.getDisplayLayout().densityDpi(); + postChangeBounds.set(0, 0, + (int) (postChangeBounds.width() * scale), + (int) (postChangeBounds.height() * scale)); + } - // Resize the PiP bounds to be at the same scale relative to the new size spec. For - // example, if PiP was resized to 90% of the maximum size on the previous layout, - // make sure it is 90% of the new maximum size spec. - postChangeBounds.set(0, 0, - (int) (mPipBoundsState.getMaxSize().x * mPipBoundsState.getBoundsScale()), - (int) (mPipBoundsState.getMaxSize().y * mPipBoundsState.getBoundsScale())); + updateDisplayLayout.run(); // Calculate the PiP bounds in the new orientation based on same fraction along the // rotated movement bounds. @@ -821,10 +827,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsState.setHasUserResizedPip(true); mTouchHandler.setUserResizeBounds(postChangeBounds); - final boolean densityDpiChanged = - mPipDisplayLayoutState.getDisplayLayout().densityDpi() != 0 - && (mPipDisplayLayoutState.getDisplayLayout().densityDpi() - != layout.densityDpi()); if (densityDpiChanged) { // Using PipMotionHelper#movePip directly here may cause race condition since // the app content in PiP mode may or may not be updated for the new density dpi. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java index 99df6a31beac..0c25f27854bd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java @@ -18,6 +18,7 @@ package com.android.wm.shell.transition.tracing; import android.internal.perfetto.protos.PerfettoTrace; import android.os.SystemClock; +import android.os.Trace; import android.tracing.perfetto.DataSourceInstance; import android.tracing.perfetto.DataSourceParams; import android.tracing.perfetto.InitArguments; @@ -58,6 +59,15 @@ public class PerfettoTransitionTracer implements TransitionTracer { return; } + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logDispatched"); + try { + doLogDispatched(transitionId, handler); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + } + + private void doLogDispatched(int transitionId, Transitions.TransitionHandler handler) { mDataSource.trace(ctx -> { final int handlerId = getHandlerId(handler, ctx); @@ -97,6 +107,15 @@ public class PerfettoTransitionTracer implements TransitionTracer { return; } + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logMergeRequested"); + try { + doLogMergeRequested(mergeRequestedTransitionId, playingTransitionId); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + } + + private void doLogMergeRequested(int mergeRequestedTransitionId, int playingTransitionId) { mDataSource.trace(ctx -> { final ProtoOutputStream os = ctx.newTracePacket(); final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); @@ -120,10 +139,19 @@ public class PerfettoTransitionTracer implements TransitionTracer { return; } + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logMerged"); + try { + doLogMerged(mergedTransitionId, playingTransitionId); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + } + + private void doLogMerged(int mergeRequestedTransitionId, int playingTransitionId) { mDataSource.trace(ctx -> { final ProtoOutputStream os = ctx.newTracePacket(); final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); - os.write(PerfettoTrace.ShellTransition.ID, mergedTransitionId); + os.write(PerfettoTrace.ShellTransition.ID, mergeRequestedTransitionId); os.write(PerfettoTrace.ShellTransition.MERGE_TIME_NS, SystemClock.elapsedRealtimeNanos()); os.write(PerfettoTrace.ShellTransition.MERGE_TARGET, playingTransitionId); @@ -142,6 +170,15 @@ public class PerfettoTransitionTracer implements TransitionTracer { return; } + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logAborted"); + try { + doLogAborted(transitionId); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + } + + private void doLogAborted(int transitionId) { mDataSource.trace(ctx -> { final ProtoOutputStream os = ctx.newTracePacket(); final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); @@ -157,6 +194,15 @@ public class PerfettoTransitionTracer implements TransitionTracer { } private void onFlush() { + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "onFlush"); + try { + doOnFlush(); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + } + + private void doOnFlush() { mDataSource.trace(ctx -> { final ProtoOutputStream os = ctx.newTracePacket(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index a8b39c41e6bb..891eea072c0d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -888,7 +888,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private DesktopModeWindowDecoration getFocusedDecor() { final int size = mWindowDecorByTaskId.size(); DesktopModeWindowDecoration focusedDecor = null; - for (int i = 0; i < size; i++) { + // TODO(b/323251951): We need to iterate this in reverse to avoid potentially getting + // a decor for a closed task. This is a short term fix while the core issue is addressed, + // which involves refactoring the window decor lifecycle to be visibility based. + for (int i = size - 1; i >= 0; i--) { final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i); if (decor != null && decor.isFocused()) { focusedDecor = decor; 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 75965d6d68d9..1668e3712462 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 @@ -143,7 +143,7 @@ class BubbleViewInfoTest : ShellTestCase() { bubbleController, mainExecutor ) - bubbleBarLayerView = BubbleBarLayerView(context, bubbleController) + bubbleBarLayerView = BubbleBarLayerView(context, bubbleController, bubbleData) } @Test diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index d55d28d469d0..b5f7caaf1b5b 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -31,6 +31,8 @@ #include <vk/GrVkExtensions.h> #include <vk/GrVkTypes.h> +#include <sstream> + #include "Properties.h" #include "RenderThread.h" #include "pipeline/skia/ShaderCache.h" @@ -40,7 +42,8 @@ namespace android { namespace uirenderer { namespace renderthread { -static std::array<std::string_view, 20> sEnableExtensions{ +// Not all of these are strictly required, but are all enabled if present. +static std::array<std::string_view, 21> sEnableExtensions{ VK_KHR_BIND_MEMORY_2_EXTENSION_NAME, VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME, VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME, @@ -61,6 +64,7 @@ static std::array<std::string_view, 20> sEnableExtensions{ VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME, VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME, + VK_EXT_DEVICE_FAULT_EXTENSION_NAME, }; static bool shouldEnableExtension(const std::string_view& extension) { @@ -303,6 +307,15 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe *tailPNext = ycbcrFeature; tailPNext = &ycbcrFeature->pNext; + if (grExtensions.hasExtension(VK_EXT_DEVICE_FAULT_EXTENSION_NAME, 1)) { + VkPhysicalDeviceFaultFeaturesEXT* deviceFaultFeatures = + new VkPhysicalDeviceFaultFeaturesEXT; + deviceFaultFeatures->sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT; + deviceFaultFeatures->pNext = nullptr; + *tailPNext = deviceFaultFeatures; + tailPNext = &deviceFaultFeatures->pNext; + } + // query to get the physical device features mGetPhysicalDeviceFeatures2(mPhysicalDevice, &features); // this looks like it would slow things down, @@ -405,6 +418,79 @@ void VulkanManager::initialize() { }); } +namespace { +void onVkDeviceFault(const std::string& contextLabel, const std::string& description, + const std::vector<VkDeviceFaultAddressInfoEXT>& addressInfos, + const std::vector<VkDeviceFaultVendorInfoEXT>& vendorInfos, + const std::vector<std::byte>& vendorBinaryData) { + // The final crash string should contain as much differentiating info as possible, up to 1024 + // bytes. As this final message is constructed, the same information is also dumped to the logs + // but in a more verbose format. Building the crash string is unsightly, so the clearer logging + // statement is always placed first to give context. + ALOGE("VK_ERROR_DEVICE_LOST (%s context): %s", contextLabel.c_str(), description.c_str()); + std::stringstream crashMsg; + crashMsg << "VK_ERROR_DEVICE_LOST (" << contextLabel; + + if (!addressInfos.empty()) { + ALOGE("%zu VkDeviceFaultAddressInfoEXT:", addressInfos.size()); + crashMsg << ", " << addressInfos.size() << " address info ("; + for (VkDeviceFaultAddressInfoEXT addressInfo : addressInfos) { + ALOGE(" addressType: %d", (int)addressInfo.addressType); + ALOGE(" reportedAddress: %" PRIu64, addressInfo.reportedAddress); + ALOGE(" addressPrecision: %" PRIu64, addressInfo.addressPrecision); + crashMsg << addressInfo.addressType << ":" + << addressInfo.reportedAddress << ":" + << addressInfo.addressPrecision << ", "; + } + crashMsg.seekp(-2, crashMsg.cur); // Move back to overwrite trailing ", " + crashMsg << ")"; + } + + if (!vendorInfos.empty()) { + ALOGE("%zu VkDeviceFaultVendorInfoEXT:", vendorInfos.size()); + crashMsg << ", " << vendorInfos.size() << " vendor info ("; + for (VkDeviceFaultVendorInfoEXT vendorInfo : vendorInfos) { + ALOGE(" description: %s", vendorInfo.description); + ALOGE(" vendorFaultCode: %" PRIu64, vendorInfo.vendorFaultCode); + ALOGE(" vendorFaultData: %" PRIu64, vendorInfo.vendorFaultData); + // Omit descriptions for individual vendor info structs in the crash string, as the + // fault code and fault data fields should be enough for clustering, and the verbosity + // isn't worth it. Additionally, vendors may just set the general description field of + // the overall fault to the description of the first element in this list, and that + // overall description will be placed at the end of the crash string. + crashMsg << vendorInfo.vendorFaultCode << ":" + << vendorInfo.vendorFaultData << ", "; + } + crashMsg.seekp(-2, crashMsg.cur); // Move back to overwrite trailing ", " + crashMsg << ")"; + } + + if (!vendorBinaryData.empty()) { + // TODO: b/322830575 - Log in base64, or dump directly to a file that gets put in bugreports + ALOGE("%zu bytes of vendor-specific binary data (please notify Android's Core Graphics" + " Stack team if you observe this message).", + vendorBinaryData.size()); + crashMsg << ", " << vendorBinaryData.size() << " bytes binary"; + } + + crashMsg << "): " << description; + LOG_ALWAYS_FATAL("%s", crashMsg.str().c_str()); +} + +void deviceLostProcRenderThread(void* callbackContext, const std::string& description, + const std::vector<VkDeviceFaultAddressInfoEXT>& addressInfos, + const std::vector<VkDeviceFaultVendorInfoEXT>& vendorInfos, + const std::vector<std::byte>& vendorBinaryData) { + onVkDeviceFault("RenderThread", description, addressInfos, vendorInfos, vendorBinaryData); +} +void deviceLostProcUploadThread(void* callbackContext, const std::string& description, + const std::vector<VkDeviceFaultAddressInfoEXT>& addressInfos, + const std::vector<VkDeviceFaultVendorInfoEXT>& vendorInfos, + const std::vector<std::byte>& vendorBinaryData) { + onVkDeviceFault("UploadThread", description, addressInfos, vendorInfos, vendorBinaryData); +} +} // anonymous namespace + static void onGrContextReleased(void* context) { VulkanManager* manager = (VulkanManager*)context; manager->decStrong((void*)onGrContextReleased); @@ -430,6 +516,10 @@ sk_sp<GrDirectContext> VulkanManager::createContext(GrContextOptions& options, backendContext.fVkExtensions = &mExtensions; backendContext.fDeviceFeatures2 = &mPhysicalDeviceFeatures2; backendContext.fGetProc = std::move(getProc); + backendContext.fDeviceLostContext = nullptr; + backendContext.fDeviceLostProc = (contextType == ContextType::kRenderThread) + ? deviceLostProcRenderThread + : deviceLostProcUploadThread; LOG_ALWAYS_FATAL_IF(options.fContextDeleteProc != nullptr, "Conflicting fContextDeleteProcs!"); this->incStrong((void*)onGrContextReleased); diff --git a/location/java/android/location/altitude/AltitudeConverter.java b/location/java/android/location/altitude/AltitudeConverter.java index 461dafb91916..ec1edb8943af 100644 --- a/location/java/android/location/altitude/AltitudeConverter.java +++ b/location/java/android/location/altitude/AltitudeConverter.java @@ -219,6 +219,27 @@ public final class AltitudeConverter { * called on the main thread as data will not be loaded from raw assets. Returns true if a Mean * Sea Level altitude is added to the {@code location}; otherwise, returns false and leaves the * {@code location} unchanged. + * + * <p>Prior calls to {@link #addMslAltitudeToLocation(Context, Location)} off the main thread + * are necessary to load data from raw assets. Example code on the main thread is as follows: + * + * <pre>{@code + * if (!mAltitudeConverter.addMslAltitudeToLocation(location)) { + * // Queue up only one call off the main thread. + * if (mIsAltitudeConverterIdle) { + * mIsAltitudeConverterIdle = false; + * executeOffMainThread(() -> { + * try { + * // Load raw assets for next call attempt on main thread. + * mAltitudeConverter.addMslAltitudeToLocation(mContext, location); + * } catch (IOException e) { + * Log.e(TAG, "Not loading raw assets: " + e); + * } + * mIsAltitudeConverterIdle = true; + * }); + * } + * } + * }</pre> */ @FlaggedApi(Flags.FLAG_GEOID_HEIGHTS_VIA_ALTITUDE_HAL) public boolean addMslAltitudeToLocation(@NonNull Location location) { diff --git a/media/java/android/media/BluetoothProfileConnectionInfo.java b/media/java/android/media/BluetoothProfileConnectionInfo.java index e4dc1521ae70..0613fc655521 100644 --- a/media/java/android/media/BluetoothProfileConnectionInfo.java +++ b/media/java/android/media/BluetoothProfileConnectionInfo.java @@ -15,6 +15,9 @@ */ package android.media; +import static android.media.audio.Flags.FLAG_SCO_MANAGED_BY_AUDIO; + +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.SystemApi; import android.bluetooth.BluetoothProfile; @@ -174,4 +177,13 @@ public final class BluetoothProfileConnectionInfo implements Parcelable { public boolean isLeOutput() { return mIsLeOutput; } + + /** + * Factory method for <code>BluetoothProfileConnectionInfo</code> for an HFP device. + */ + @FlaggedApi(FLAG_SCO_MANAGED_BY_AUDIO) + public static @NonNull BluetoothProfileConnectionInfo createHfpInfo() { + return new BluetoothProfileConnectionInfo(BluetoothProfile.HEADSET, false, + -1, false); + } } diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig index 018eaf6dc32f..f1107059111c 100644 --- a/media/java/android/media/tv/flags/media_tv.aconfig +++ b/media/java/android/media/tv/flags/media_tv.aconfig @@ -12,4 +12,11 @@ flag { namespace: "media_tv" description: "Enable the TV client-side AD framework." bug: "303506816" +} + +flag { + name: "tiaf_v_apis" + namespace: "media_tv" + description: "TIAF V3.0 APIs for Android V" + bug: "303323657" }
\ No newline at end of file diff --git a/nfc/api/current.txt b/nfc/api/current.txt index 1046d8e9aebb..9742d46b8ae9 100644 --- a/nfc/api/current.txt +++ b/nfc/api/current.txt @@ -64,10 +64,8 @@ package android.nfc { } public final class NfcAdapter { - method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean allowTransaction(); method public void disableForegroundDispatch(android.app.Activity); method public void disableReaderMode(android.app.Activity); - method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean disallowTransaction(); method public void enableForegroundDispatch(android.app.Activity, android.app.PendingIntent, android.content.IntentFilter[], String[][]); method public void enableReaderMode(android.app.Activity, android.nfc.NfcAdapter.ReaderCallback, int, android.os.Bundle); method public static android.nfc.NfcAdapter getDefaultAdapter(android.content.Context); @@ -83,6 +81,7 @@ package android.nfc { method @FlaggedApi("android.nfc.enable_nfc_charging") public boolean isWlcEnabled(); method @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public void resetDiscoveryTechnology(@NonNull android.app.Activity); method @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public void setDiscoveryTechnology(@NonNull android.app.Activity, int, int); + method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean setTransactionAllowed(boolean); field public static final String ACTION_ADAPTER_STATE_CHANGED = "android.nfc.action.ADAPTER_STATE_CHANGED"; field public static final String ACTION_NDEF_DISCOVERED = "android.nfc.action.NDEF_DISCOVERED"; field @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public static final String ACTION_PREFERRED_PAYMENT_CHANGED = "android.nfc.action.PREFERRED_PAYMENT_CHANGED"; diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java index 4d56c11d1c39..55506a1a7579 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -1204,37 +1204,21 @@ public final class NfcAdapter { } } - /** - * Disables observe mode to allow the transaction to proceed. See - * {@link #isObserveModeSupported()} for a description of observe mode and - * use {@link #disallowTransaction()} to enable observe mode and block - * transactions again. - * - * @return boolean indicating success or failure. - */ - @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) - public boolean allowTransaction() { - try { - return sService.setObserveMode(false); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - return false; - } - } - /** - * Signals that the transaction has completed and observe mode may be - * reenabled. See {@link #isObserveModeSupported()} for a description of - * observe mode and use {@link #allowTransaction()} to disable observe - * mode and allow transactions to proceed. - * - * @return boolean indicating success or failure. - */ + * Controls whether the NFC adapter will allow transactions to proceed or be in observe mode + * and simply observe and notify the APDU service of polling loop frames. See + * {@link #isObserveModeSupported()} for a description of observe mode. + * + * @param allowed true disables observe mode to allow the transaction to proceed while false + * enables observe mode and does not allow transactions to proceed. + * + * @return boolean indicating success or failure. + */ @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) - public boolean disallowTransaction() { + public boolean setTransactionAllowed(boolean allowed) { try { - return sService.setObserveMode(true); + return sService.setObserveMode(!allowed); } catch (RemoteException e) { attemptDeadServiceRecovery(e); return false; diff --git a/packages/CredentialManager/res/drawable/more_horiz_24px.xml b/packages/CredentialManager/res/drawable/more_horiz_24px.xml index 7b235f84f0fa..0100718ffd08 100644 --- a/packages/CredentialManager/res/drawable/more_horiz_24px.xml +++ b/packages/CredentialManager/res/drawable/more_horiz_24px.xml @@ -3,6 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" + android:contentDescription="@string/more_options_content_description" android:tint="?attr/colorControlNormal"> <path android:fillColor="@android:color/white" diff --git a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml index b97e9927e0ed..fdda9ea06ab9 100644 --- a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml +++ b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml @@ -28,7 +28,7 @@ android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:contentDescription="@string/provider_icon_content_description" + android:contentDescription="@string/more_options_content_description" android:background="@null"/> <TextView android:id="@android:id/text1" diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml index 261154fe9792..c7c2fda6a489 100644 --- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml +++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml @@ -25,7 +25,6 @@ android:id="@android:id/icon1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:contentDescription="@string/dropdown_presentation_more_sign_in_options_text" android:layout_centerVertical="true" android:layout_alignParentStart="true" android:background="@null"/> diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index f98164b8788c..82b47a964204 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -172,5 +172,5 @@ <!-- Strings for dropdown presentation. --> <!-- Text shown in the dropdown presentation to select more sign in options. [CHAR LIMIT=120] --> <string name="dropdown_presentation_more_sign_in_options_text">Sign-in options</string> - <string name="provider_icon_content_description">Credential provider icon</string> + <string name="more_options_content_description">More</string> </resources>
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt index 68f1c861d51b..02afc547e3f8 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt @@ -74,6 +74,8 @@ class RemoteViewsFactory { setMaxHeightMethodName, context.resources.getDimensionPixelSize( com.android.credentialmanager.R.dimen.autofill_icon_size)); + remoteViews.setContentDescription(android.R.id.icon1, credentialEntryInfo + .providerDisplayName); val drawableId = com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_one remoteViews.setInt( diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml index f4641b9930cb..f425f52804c5 100644 --- a/packages/PackageInstaller/res/values/strings.xml +++ b/packages/PackageInstaller/res/values/strings.xml @@ -272,7 +272,7 @@ <!-- The title of a dialog which asks the user to restore (i.e. re-install, re-download) an app after parts of the app have been previously moved into the cloud for temporary storage. "installername" is the app that will facilitate the download of the app. [CHAR LIMIT=50] --> - <string name="unarchive_application_title">Restore <xliff:g id="appname" example="Bird Game">%1$s</xliff:g> from <xliff:g id="installername" example="App Store">%1$s</xliff:g>?</string> + <string name="unarchive_application_title">Restore <xliff:g id="appname" example="Bird Game">%1$s</xliff:g> from <xliff:g id="installername" example="App Store">%2$s</xliff:g>?</string> <!-- After the user confirms the dialog, a download will start. [CHAR LIMIT=none] --> <string name="unarchive_body_text">This app will begin to download in the background</string> <!-- The action to restore (i.e. re-install, re-download) an app after parts of the app have been previously moved diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java index 9af799c37e8f..b20117d78230 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java @@ -25,10 +25,12 @@ import android.app.DialogFragment; import android.app.Fragment; import android.app.FragmentTransaction; import android.content.IntentSender; +import android.content.pm.InstallSourceInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Process; +import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; @@ -97,14 +99,30 @@ public class UnarchiveActivity extends Activity { String appTitle = pm.getApplicationInfo(mPackageName, PackageManager.ApplicationInfoFlags.of( MATCH_ARCHIVED_PACKAGES)).loadLabel(pm).toString(); - // TODO(ag/25387215) Get the real installer title here after fixing getInstallSource for - // archived apps. - showDialogFragment(appTitle, "installerTitle"); + String installerTitle = getResponsibleInstallerTitle(pm, + pm.getInstallSourceInfo(mPackageName)); + showDialogFragment(appTitle, installerTitle); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Invalid packageName: " + e.getMessage()); } } + private String getResponsibleInstallerTitle(PackageManager pm, + InstallSourceInfo installSource) + throws PackageManager.NameNotFoundException { + String packageName = TextUtils.isEmpty(installSource.getUpdateOwnerPackageName()) + ? installSource.getInstallingPackageName() + : installSource.getUpdateOwnerPackageName(); + if (packageName == null) { + // Should be unreachable. + Log.e(TAG, "Installer not found."); + setResult(Activity.RESULT_FIRST_USER); + finish(); + return ""; + } + return pm.getApplicationInfo(packageName, /* flags= */ 0).loadLabel(pm).toString(); + } + @NonNull private String[] getRequestedPermissions(String callingPackage) { String[] requestedPermissions = null; diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index a4b3af990c17..2e64212d298a 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1340,11 +1340,11 @@ <string name="notice_header" translatable="false"></string> <!-- Name of the phone device. [CHAR LIMIT=30] --> - <string name="media_transfer_this_device_name" product="default">This phone</string> + <string name="media_transfer_this_device_name">This phone</string> <!-- Name of the tablet device. [CHAR LIMIT=30] --> - <string name="media_transfer_this_device_name" product="tablet">This tablet</string> + <string name="media_transfer_this_device_name_tablet">This tablet</string> <!-- Name of the default media output of the TV. [CHAR LIMIT=30] --> - <string name="media_transfer_this_device_name" product="tv">@string/tv_media_transfer_default</string> + <string name="media_transfer_this_device_name_tv">@string/tv_media_transfer_default</string> <!-- Name of the dock device. [CHAR LIMIT=30] --> <string name="media_transfer_dock_speaker_device_name">Dock speaker</string> <!-- Default name of the external device. [CHAR LIMIT=30] --> diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java index 15f33d2cff42..ba9180db0887 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java @@ -37,6 +37,7 @@ import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiPortInfo; import android.media.MediaRoute2Info; import android.media.RouteListingPreference; +import android.os.SystemProperties; import android.util.Log; import androidx.annotation.NonNull; @@ -45,6 +46,7 @@ import androidx.annotation.VisibleForTesting; import com.android.settingslib.R; import com.android.settingslib.media.flags.Flags; +import java.util.Arrays; import java.util.List; /** @@ -63,6 +65,17 @@ public class PhoneMediaDevice extends MediaDevice { private final DeviceIconUtil mDeviceIconUtil; + /** Returns this device name for media transfer. */ + public static @NonNull String getMediaTransferThisDeviceName(@NonNull Context context) { + if (isTv(context)) { + return context.getString(R.string.media_transfer_this_device_name_tv); + } else if (isTablet()) { + return context.getString(R.string.media_transfer_this_device_name_tablet); + } else { + return context.getString(R.string.media_transfer_this_device_name); + } + } + /** Returns the device name for the given {@code routeInfo}. */ public static String getSystemRouteNameFromType( @NonNull Context context, @NonNull MediaRoute2Info routeInfo) { @@ -80,7 +93,7 @@ public class PhoneMediaDevice extends MediaDevice { name = context.getString(R.string.media_transfer_dock_speaker_device_name); break; case TYPE_BUILTIN_SPEAKER: - name = context.getString(R.string.media_transfer_this_device_name); + name = getMediaTransferThisDeviceName(context); break; case TYPE_HDMI: name = context.getString(isTv ? R.string.tv_media_transfer_default : @@ -135,6 +148,11 @@ public class PhoneMediaDevice extends MediaDevice { && Flags.enableTvMediaOutputDialog(); } + static boolean isTablet() { + return Arrays.asList(SystemProperties.get("ro.build.characteristics").split(",")) + .contains("tablet"); + } + // MediaRoute2Info.getType was made public on API 34, but exists since API 30. @SuppressWarnings("NewApi") @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt index 3355fb395ca0..6761aa7a1006 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt @@ -16,29 +16,71 @@ package com.android.settingslib.volume.data.repository +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.media.AudioDeviceInfo import android.media.AudioManager +import android.media.AudioManager.OnCommunicationDeviceChangedListener +import androidx.concurrent.futures.DirectExecutor import com.android.internal.util.ConcurrentUtils +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 kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext -/** Provides audio managing functionality and data. */ +/** Provides audio streams state and managing functionality. */ interface AudioRepository { /** Current [AudioManager.getMode]. */ val mode: StateFlow<Int> + + /** + * Ringtone mode. + * + * @see AudioManager.getRingerModeInternal + */ + val ringerMode: StateFlow<RingerMode> + + /** + * Communication device. Emits null when there is no communication device available. + * + * @see AudioDeviceInfo.getType + */ + val communicationDevice: StateFlow<AudioDeviceInfo?> + + /** State of the [AudioStream]. */ + suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> + + /** Current state of the [AudioStream]. */ + suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel + + suspend fun setVolume(audioStream: AudioStream, volume: Int) + + suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean) } class AudioRepositoryImpl( + private val context: Context, private val audioManager: AudioManager, - backgroundCoroutineContext: CoroutineContext, - coroutineScope: CoroutineScope, + private val backgroundCoroutineContext: CoroutineContext, + private val coroutineScope: CoroutineScope, ) : AudioRepository { override val mode: StateFlow<Int> = @@ -50,4 +92,117 @@ class AudioRepositoryImpl( } .flowOn(backgroundCoroutineContext) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), audioManager.mode) + + private val audioManagerIntents: SharedFlow<String> = + callbackFlow { + val receiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent) { + intent.action?.let { action -> launch { send(action) } } + } + } + context.registerReceiver( + receiver, + IntentFilter().apply { + for (action in allActions) { + addAction(action) + } + } + ) + + awaitClose { context.unregisterReceiver(receiver) } + } + .shareIn(coroutineScope, SharingStarted.WhileSubscribed()) + + override val ringerMode: StateFlow<RingerMode> = + audioManagerIntents + .filter { ringerActions.contains(it) } + .map { RingerMode(audioManager.ringerModeInternal) } + .flowOn(backgroundCoroutineContext) + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(), + RingerMode(audioManager.ringerModeInternal), + ) + + override val communicationDevice: StateFlow<AudioDeviceInfo?> + get() = + callbackFlow { + val listener = OnCommunicationDeviceChangedListener { trySend(Unit) } + audioManager.addOnCommunicationDeviceChangedListener( + DirectExecutor.INSTANCE, + listener + ) + + awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) } + } + .filterNotNull() + .map { audioManager.communicationDevice } + .flowOn(backgroundCoroutineContext) + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(), + audioManager.communicationDevice, + ) + + override suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> { + return audioManagerIntents + .filter { modelActions.contains(it) } + .map { getCurrentAudioStream(audioStream) } + .flowOn(backgroundCoroutineContext) + } + + override suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel { + return withContext(backgroundCoroutineContext) { + AudioStreamModel( + audioStream = audioStream, + minVolume = getMinVolume(audioStream), + maxVolume = audioManager.getStreamMaxVolume(audioStream.value), + volume = audioManager.getStreamVolume(audioStream.value), + isAffectedByRingerMode = + audioManager.isStreamAffectedByRingerMode(audioStream.value), + isMuted = audioManager.isStreamMute(audioStream.value) + ) + } + } + + override suspend fun setVolume(audioStream: AudioStream, volume: Int) = + withContext(backgroundCoroutineContext) { + audioManager.setStreamVolume(audioStream.value, volume, 0) + } + + override suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean) = + withContext(backgroundCoroutineContext) { + if (isMuted) { + audioManager.adjustStreamVolume(audioStream.value, 0, AudioManager.ADJUST_MUTE) + } else { + audioManager.adjustStreamVolume(audioStream.value, 0, AudioManager.ADJUST_UNMUTE) + } + } + + private fun getMinVolume(stream: AudioStream): Int = + try { + audioManager.getStreamMinVolume(stream.value) + } catch (e: IllegalArgumentException) { + // Fallback to STREAM_VOICE_CALL because + // CallVolumePreferenceController.java default + // return STREAM_VOICE_CALL in getAudioStream + audioManager.getStreamMinVolume(AudioManager.STREAM_VOICE_CALL) + } + + private companion object { + val modelActions = + setOf( + AudioManager.STREAM_MUTE_CHANGED_ACTION, + AudioManager.MASTER_MUTE_CHANGED_ACTION, + AudioManager.VOLUME_CHANGED_ACTION, + AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION, + AudioManager.STREAM_DEVICES_CHANGED_ACTION, + ) + val ringerActions = + setOf( + AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION, + ) + val allActions = ringerActions + modelActions + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSystemRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSystemRepository.kt new file mode 100644 index 000000000000..2b12936e8118 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSystemRepository.kt @@ -0,0 +1,32 @@ +/* + * 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.settingslib.volume.data.repository + +import android.content.Context +import android.media.AudioSystem + +/** Provides the current state of the audio system. */ +interface AudioSystemRepository { + + val isSingleVolume: Boolean +} + +class AudioSystemRepositoryImpl(private val context: Context) : AudioSystemRepository { + + override val isSingleVolume: Boolean + get() = AudioSystem.isSingleVolume(context) +} diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt new file mode 100644 index 000000000000..58f3c2d61f3b --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt @@ -0,0 +1,45 @@ +/* + * 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.settingslib.volume.shared.model + +import android.media.AudioManager + +/** Type-safe wrapper for [AudioManager] audio stream. */ +@JvmInline +value class AudioStream(val value: Int) { + init { + require(value in supportedStreamTypes) { "Unsupported stream=$value" } + } + + private companion object { + val supportedStreamTypes = + setOf( + AudioManager.STREAM_VOICE_CALL, + AudioManager.STREAM_SYSTEM, + AudioManager.STREAM_RING, + AudioManager.STREAM_MUSIC, + AudioManager.STREAM_ALARM, + AudioManager.STREAM_NOTIFICATION, + AudioManager.STREAM_BLUETOOTH_SCO, + AudioManager.STREAM_SYSTEM_ENFORCED, + AudioManager.STREAM_DTMF, + AudioManager.STREAM_TTS, + AudioManager.STREAM_ACCESSIBILITY, + AudioManager.STREAM_ASSISTANT, + ) + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStreamModel.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStreamModel.kt new file mode 100644 index 000000000000..c1be1ee020f2 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStreamModel.kt @@ -0,0 +1,27 @@ +/* + * 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.settingslib.volume.shared.model + +/** Current state of the audio stream. */ +data class AudioStreamModel( + val audioStream: AudioStream, + val volume: Int, + val minVolume: Int, + val maxVolume: Int, + val isAffectedByRingerMode: Boolean, + val isMuted: Boolean, +) diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/RingerMode.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/RingerMode.kt new file mode 100644 index 000000000000..9f03927264c1 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/RingerMode.kt @@ -0,0 +1,38 @@ +/* + * 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.settingslib.volume.shared.model + +import android.media.AudioManager + +/** Type-safe wrapper for [AudioManager] ringer mode. */ +@JvmInline +value class RingerMode(val value: Int) { + + init { + require(value in supportedRingerModes) { "Unsupported stream=$value" } + } + + private companion object { + val supportedRingerModes = + setOf( + AudioManager.RINGER_MODE_SILENT, + AudioManager.RINGER_MODE_VIBRATE, + AudioManager.RINGER_MODE_NORMAL, + AudioManager.RINGER_MODE_MAX, + ) + } +} diff --git a/packages/SettingsLib/tests/integ/Android.bp b/packages/SettingsLib/tests/integ/Android.bp index ce3a7baf6be6..f303ab5fc54b 100644 --- a/packages/SettingsLib/tests/integ/Android.bp +++ b/packages/SettingsLib/tests/integ/Android.bp @@ -40,6 +40,8 @@ android_test { "android.test.runner", "telephony-common", "android.test.base", + "android.test.mock", + "truth", ], platform_apis: true, @@ -49,16 +51,23 @@ android_test { "androidx.test.core", "androidx.test.rules", "androidx.test.espresso.core", + "androidx.test.ext.junit", "flag-junit", - "mockito-target-minus-junit4", + "kotlinx_coroutines_test", + "mockito-target-extended-minus-junit4", "platform-test-annotations", "truth", "SettingsLibDeviceStateRotationLock", "SettingsLibSettingsSpinner", "SettingsLibUsageProgressBarPreference", "settingslib_media_flags_lib", - "kotlinx_coroutines_test", ], + jni_libs: [ + "libdexmakerjvmtiagent", + "libmultiplejvmtiagentsinterferenceagent", + "libstaticjvmtiagent", + ], dxflags: ["--multi-dex"], + manifest: "AndroidManifest.xml", } diff --git a/packages/SettingsLib/tests/integ/AndroidManifest.xml b/packages/SettingsLib/tests/integ/AndroidManifest.xml index 32048ca6344c..9fb1c1f65961 100644 --- a/packages/SettingsLib/tests/integ/AndroidManifest.xml +++ b/packages/SettingsLib/tests/integ/AndroidManifest.xml @@ -25,7 +25,7 @@ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> - <application> + <application android:debuggable="true" android:testOnly="true"> <uses-library android:name="android.test.runner" /> <activity android:name=".drawer.SettingsDrawerActivityTest$TestActivity"/> diff --git a/packages/SettingsLib/tests/integ/AndroidTest.xml b/packages/SettingsLib/tests/integ/AndroidTest.xml index d0aee8822a72..9de601907a62 100644 --- a/packages/SettingsLib/tests/integ/AndroidTest.xml +++ b/packages/SettingsLib/tests/integ/AndroidTest.xml @@ -16,6 +16,7 @@ <configuration description="Runs Tests for SettingsLib."> <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> <option name="test-file-name" value="SettingsLibTests.apk" /> + <option name="install-arg" value="-t" /> </target_preparer> <option name="test-suite-tag" value="apct" /> diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt new file mode 100644 index 000000000000..7b70c64f349b --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt @@ -0,0 +1,281 @@ +/* + * 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.settingslib.volume.data.repository + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.media.AudioDeviceInfo +import android.media.AudioManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +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.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@Suppress("UnspecifiedRegisterReceiverFlag") +@RunWith(AndroidJUnit4::class) +class AudioRepositoryTest { + + @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver> + @Captor + private lateinit var modeListenerCaptor: ArgumentCaptor<AudioManager.OnModeChangedListener> + @Captor + private lateinit var communicationDeviceListenerCaptor: + ArgumentCaptor<AudioManager.OnCommunicationDeviceChangedListener> + + @Mock private lateinit var context: Context + @Mock private lateinit var audioManager: AudioManager + @Mock private lateinit var communicationDevice: AudioDeviceInfo + + private val volumeByStream: MutableMap<Int, Int> = mutableMapOf() + private val isAffectedByRingerModeByStream: MutableMap<Int, Boolean> = mutableMapOf() + private val isMuteByStream: MutableMap<Int, Boolean> = mutableMapOf() + private val testScope = TestScope() + + private lateinit var underTest: AudioRepository + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + `when`(audioManager.mode).thenReturn(AudioManager.MODE_RINGTONE) + `when`(audioManager.communicationDevice).thenReturn(communicationDevice) + `when`(audioManager.getStreamMinVolume(anyInt())).thenReturn(MIN_VOLUME) + `when`(audioManager.getStreamMaxVolume(anyInt())).thenReturn(MAX_VOLUME) + `when`(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL) + `when`(audioManager.setStreamVolume(anyInt(), anyInt(), anyInt())).then { + volumeByStream[it.arguments[0] as Int] = it.arguments[1] as Int + triggerIntent(AudioManager.ACTION_VOLUME_CHANGED) + } + `when`(audioManager.adjustStreamVolume(anyInt(), anyInt(), anyInt())).then { + isMuteByStream[it.arguments[0] as Int] = it.arguments[2] == AudioManager.ADJUST_MUTE + triggerIntent(AudioManager.STREAM_MUTE_CHANGED_ACTION) + } + `when`(audioManager.getStreamVolume(anyInt())).thenAnswer { + volumeByStream.getOrDefault(it.arguments[0] as Int, 0) + } + `when`(audioManager.isStreamAffectedByRingerMode(anyInt())).thenAnswer { + isAffectedByRingerModeByStream.getOrDefault(it.arguments[0] as Int, false) + } + `when`(audioManager.isStreamMute(anyInt())).thenAnswer { + isMuteByStream.getOrDefault(it.arguments[0] as Int, false) + } + + underTest = + AudioRepositoryImpl( + context, + audioManager, + testScope.testScheduler, + testScope.backgroundScope, + ) + } + + @Test + fun audioModeChanges_repositoryEmits() { + testScope.runTest { + val modes = mutableListOf<Int>() + underTest.mode.onEach { modes.add(it) }.launchIn(backgroundScope) + runCurrent() + + triggerModeChange(AudioManager.MODE_IN_CALL) + runCurrent() + + assertThat(modes).containsExactly(AudioManager.MODE_RINGTONE, AudioManager.MODE_IN_CALL) + } + } + + @Test + fun ringerModeChanges_repositoryEmits() { + testScope.runTest { + val modes = mutableListOf<RingerMode>() + underTest.ringerMode.onEach { modes.add(it) }.launchIn(backgroundScope) + runCurrent() + + `when`(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_SILENT) + triggerIntent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION) + runCurrent() + + assertThat(modes) + .containsExactly( + RingerMode(AudioManager.RINGER_MODE_NORMAL), + RingerMode(AudioManager.RINGER_MODE_SILENT), + ) + } + } + + @Test + fun communicationDeviceChanges_repositoryEmits() { + testScope.runTest { + var device: AudioDeviceInfo? = null + underTest.communicationDevice.onEach { device = it }.launchIn(backgroundScope) + runCurrent() + + triggerConnectedDeviceChange(communicationDevice) + runCurrent() + + assertThat(device).isSameInstanceAs(communicationDevice) + } + } + + @Test + fun adjustingVolume_changesTheStream() { + testScope.runTest { + val audioStream = AudioStream(AudioManager.STREAM_SYSTEM) + var streamModel: AudioStreamModel? = null + underTest + .getAudioStream(audioStream) + .onEach { streamModel = it } + .launchIn(backgroundScope) + runCurrent() + + underTest.setVolume(audioStream, 50) + runCurrent() + + assertThat(streamModel) + .isEqualTo( + AudioStreamModel( + audioStream = audioStream, + volume = 50, + minVolume = MIN_VOLUME, + maxVolume = MAX_VOLUME, + isAffectedByRingerMode = false, + isMuted = false, + ) + ) + } + } + + @Test + fun adjustingVolume_currentModeIsUpToDate() { + testScope.runTest { + val audioStream = AudioStream(AudioManager.STREAM_SYSTEM) + var streamModel: AudioStreamModel? = null + underTest + .getAudioStream(audioStream) + .onEach { streamModel = it } + .launchIn(backgroundScope) + runCurrent() + + underTest.setVolume(audioStream, 50) + runCurrent() + + assertThat(underTest.getCurrentAudioStream(audioStream)).isEqualTo(streamModel) + } + } + + @Test + fun muteStream_mutesTheStream() { + testScope.runTest { + val audioStream = AudioStream(AudioManager.STREAM_SYSTEM) + var streamModel: AudioStreamModel? = null + underTest + .getAudioStream(audioStream) + .onEach { streamModel = it } + .launchIn(backgroundScope) + runCurrent() + + underTest.setMuted(audioStream, true) + runCurrent() + + assertThat(streamModel) + .isEqualTo( + AudioStreamModel( + audioStream = audioStream, + volume = 0, + minVolume = MIN_VOLUME, + maxVolume = MAX_VOLUME, + isAffectedByRingerMode = false, + isMuted = true, + ) + ) + } + } + + @Test + fun unmuteStream_unmutesTheStream() { + testScope.runTest { + val audioStream = AudioStream(AudioManager.STREAM_SYSTEM) + isMuteByStream[audioStream.value] = true + var streamModel: AudioStreamModel? = null + underTest + .getAudioStream(audioStream) + .onEach { streamModel = it } + .launchIn(backgroundScope) + runCurrent() + + underTest.setMuted(audioStream, false) + runCurrent() + + assertThat(streamModel) + .isEqualTo( + AudioStreamModel( + audioStream = audioStream, + volume = 0, + minVolume = MIN_VOLUME, + maxVolume = MAX_VOLUME, + isAffectedByRingerMode = false, + isMuted = false, + ) + ) + } + } + + private fun triggerConnectedDeviceChange(communicationDevice: AudioDeviceInfo?) { + verify(audioManager) + .addOnCommunicationDeviceChangedListener( + any(), + communicationDeviceListenerCaptor.capture(), + ) + communicationDeviceListenerCaptor.value.onCommunicationDeviceChanged(communicationDevice) + } + + private fun triggerModeChange(mode: Int) { + verify(audioManager).addOnModeChangedListener(any(), modeListenerCaptor.capture()) + modeListenerCaptor.value.onModeChanged(mode) + } + + private fun triggerIntent(action: String) { + verify(context).registerReceiver(receiverCaptor.capture(), any()) + receiverCaptor.value.onReceive(context, Intent(action)) + } + + private companion object { + const val MIN_VOLUME = 0 + const val MAX_VOLUME = 100 + } +} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt index 686362fadf02..dddf8e82d5f7 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt @@ -16,9 +16,15 @@ package com.android.settingslib.volume.data.repository +import android.media.AudioDeviceInfo +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.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update class FakeAudioRepository : AudioRepository { @@ -26,7 +32,59 @@ class FakeAudioRepository : AudioRepository { override val mode: StateFlow<Int> get() = mutableMode.asStateFlow() + private val mutableRingerMode = MutableStateFlow(RingerMode(0)) + override val ringerMode: StateFlow<RingerMode> + get() = mutableRingerMode.asStateFlow() + + private val mutableCommunicationDevice = MutableStateFlow<AudioDeviceInfo?>(null) + override val communicationDevice: StateFlow<AudioDeviceInfo?> + get() = mutableCommunicationDevice.asStateFlow() + + private val models: MutableMap<AudioStream, MutableStateFlow<AudioStreamModel>> = mutableMapOf() + + private fun getAudioStreamModelState( + audioStream: AudioStream + ): MutableStateFlow<AudioStreamModel> = + models.getOrPut(audioStream) { + MutableStateFlow( + AudioStreamModel( + audioStream = audioStream, + volume = 0, + minVolume = 0, + maxVolume = 0, + isAffectedByRingerMode = false, + isMuted = false, + ) + ) + } + + override suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> = + getAudioStreamModelState(audioStream).asStateFlow() + + override suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel = + getAudioStreamModelState(audioStream).value + + override suspend fun setVolume(audioStream: AudioStream, volume: Int) { + getAudioStreamModelState(audioStream).update { it.copy(volume = volume) } + } + + override suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean) { + getAudioStreamModelState(audioStream).update { it.copy(isMuted = isMuted) } + } + fun setMode(newMode: Int) { mutableMode.value = newMode } + + fun setRingerMode(newRingerMode: RingerMode) { + mutableRingerMode.value = newRingerMode + } + + fun setCommunicationDevice(device: AudioDeviceInfo?) { + mutableCommunicationDevice.value = device + } + + fun setAudioStreamModel(model: AudioStreamModel) { + getAudioStreamModelState(model.audioStream).update { model } + } } diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt index 3bc1edc9c944..4dbf865475a4 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt @@ -17,8 +17,9 @@ package com.android.settingslib.volume.domain.interactor import android.media.AudioManager +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import androidx.test.runner.AndroidJUnit4 +import com.android.settingslib.BaseTest import com.android.settingslib.volume.data.repository.FakeAudioRepository import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -33,7 +34,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) @SmallTest -class AudioModeInteractorTest { +class AudioModeInteractorTest : BaseTest() { private val testScope = TestScope() private val fakeAudioRepository = FakeAudioRepository() diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java index ceba9be70487..e2d58d660fd5 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java @@ -24,6 +24,7 @@ import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; import static com.android.settingslib.media.PhoneMediaDevice.PHONE_ID; import static com.android.settingslib.media.PhoneMediaDevice.USB_HEADSET_ID; import static com.android.settingslib.media.PhoneMediaDevice.WIRED_HEADSET_ID; +import static com.android.settingslib.media.PhoneMediaDevice.getMediaTransferThisDeviceName; import static com.google.common.truth.Truth.assertThat; @@ -114,7 +115,7 @@ public class PhoneMediaDeviceTest { when(mInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER); assertThat(mPhoneMediaDevice.getName()) - .isEqualTo(mContext.getString(R.string.media_transfer_this_device_name)); + .isEqualTo(getMediaTransferThisDeviceName(mContext)); } @EnableFlags(Flags.FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER) diff --git a/packages/SettingsLib/tests/unit/Android.bp b/packages/SettingsLib/tests/unit/Android.bp index 6d6e2ff8e59b..e2eda4f2691f 100644 --- a/packages/SettingsLib/tests/unit/Android.bp +++ b/packages/SettingsLib/tests/unit/Android.bp @@ -32,5 +32,7 @@ android_test { "androidx.test.ext.junit", "androidx.test.runner", "truth", + "kotlinx_coroutines_test", + "mockito-target-minus-junit4", ], } diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 95e0e1b0fa09..e99fcc92dea7 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -908,6 +908,12 @@ <!-- Permissions required for CTS test - CtsPermissionUiTestCases --> <uses-permission android:name="android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES" /> + <!-- Permissions required for CTS test - CtsContactKeysProviderPrivilegedApp --> + <uses-permission android:name="android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS"/> + + <!-- Permission required for Cts test ScreenRecordingCallbackTests --> + <uses-permission android:name="android.permission.DETECT_SCREEN_RECORDING" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 3db99f284e4c..ba3026ec18f8 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -67,6 +67,16 @@ flag { } flag { + name: "nssl_falsing_fix" + namespace: "systemui" + description: "Minor touch changes to prevent falsing errors in NSSL" + bug: "316551193" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "refactor_get_current_user" namespace: "systemui" description: "KeyguardUpdateMonitor.getCurrentUser() was providing outdated results." @@ -94,7 +104,7 @@ flag { " standard background color is desired. This was the behavior before we discovered" " a resources threading issue, which we worked around by tinting the notification" " backgrounds and footer buttons." - bug: "294347738" + bug: "294830092" } flag { @@ -323,13 +333,6 @@ flag { } flag { - name: "screenshare_notification_hiding" - namespace: "systemui" - description: "Enable hiding of notifications during screenshare" - bug: "312784809" -} - -flag { name: "run_fingerprint_detect_on_dismissible_keyguard" namespace: "systemui" description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible." @@ -365,9 +368,9 @@ flag { } flag { - name: "enable_keyguard_compose" + name: "compose_lockscreen" namespace: "systemui" - description: "Enables the compose version of keyguard." + description: "Enables the compose version of lockscreen that runs standalone, outside of Flexiglass." bug: "301968149" } @@ -384,3 +387,11 @@ flag { description: "Enables on-screen contextual tip about how to take screenshot." bug: "322891421" } + +flag { + name: "shaderlib_loading_effect_refactor" + namespace: "systemui" + description: "Extend shader library to provide the common loading effects." + bug: "282007590" +} + diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 576596fa67fe..4e72dfef82e1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -160,14 +160,23 @@ fun CommunalHub( gridCoordinates ) { detectLongPressGesture { offset -> - isButtonToEditWidgetsShowing = true - // Deduct both grid offset relative to its container and content offset. val adjustedOffset = gridCoordinates?.let { offset - it.positionInWindow() - contentOffset } val index = adjustedOffset?.let { firstIndexAtOffset(gridState, it) } + // Display the button only when the gesture initiates from widgets, + // the CTA tile, or an empty area on the screen. UMO/smartspace have + // their own long-press handlers. To prevent user confusion, we should + // not display this button. + if ( + index == null || + communalContent[index].isWidget() || + communalContent[index] is CommunalContentModel.CtaTileInViewMode + ) { + isButtonToEditWidgetsShowing = true + } val key = index?.let { keyAtIndexIfEditable(communalContent, index) } viewModel.setSelectedKey(key) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt index 68e57b5d51b8..071433eba98d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt @@ -53,7 +53,7 @@ fun StickyKeysIndicator(stickyKeys: Map<ModifierKey, Locked>, modifier: Modifier stickyKeys.forEach { (key, isLocked) -> key(key) { Text( - text = key.text, + text = key.displayedText, fontWeight = if (isLocked.locked) FontWeight.Bold else FontWeight.Normal ) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 5258078e0e9a..d904c8b770bf 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -504,6 +504,7 @@ internal fun SceneTransitionLayoutForTesting( layoutImpl.density = density layoutImpl.swipeSourceDetector = swipeSourceDetector + layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold } layoutImpl.Content(modifier) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt index e8cc0eca0d8b..2dc94a405ae3 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt @@ -38,6 +38,8 @@ import com.android.compose.test.MonotonicClockTestScope import com.android.compose.test.runMonotonicClockTest import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.junit.Test import org.junit.runner.RunWith @@ -47,7 +49,7 @@ private val LAYOUT_SIZE = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) @RunWith(AndroidJUnit4::class) class SceneGestureHandlerTest { private class TestGestureScope( - val coroutineScope: MonotonicClockTestScope, + private val testScope: MonotonicClockTestScope, ) { private val layoutState = MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions) @@ -92,17 +94,16 @@ class SceneGestureHandlerTest { swipeSourceDetector = DefaultEdgeDetector, transitionInterceptionThreshold = transitionInterceptionThreshold, builder = scenesBuilder, - coroutineScope = coroutineScope, + coroutineScope = testScope, ) .apply { setScenesTargetSizeForTest(LAYOUT_SIZE) } val sceneGestureHandler = layoutImpl.gestureHandler(Orientation.Vertical) val horizontalSceneGestureHandler = layoutImpl.gestureHandler(Orientation.Horizontal) - val draggable = sceneGestureHandler.draggable fun nestedScrollConnection(nestedScrollBehavior: NestedScrollBehavior) = SceneNestedScrollHandler( - layoutImpl, + layoutImpl = layoutImpl, orientation = sceneGestureHandler.orientation, topOrLeftBehavior = nestedScrollBehavior, bottomOrRightBehavior = nestedScrollBehavior, @@ -117,8 +118,12 @@ class SceneGestureHandlerTest { fun up(fractionOfScreen: Float) = if (fractionOfScreen < 0f) error("use down()") else -down(fractionOfScreen) - // Offset y: 10% of the screen - val offsetY10 = Offset(x = 0f, y = down(0.1f)) + fun downOffset(fractionOfScreen: Float) = + if (fractionOfScreen < 0f) { + error("upOffset() is required, not implemented yet") + } else { + Offset(x = 0f, y = down(fractionOfScreen)) + } val transitionState: TransitionState get() = layoutState.transitionState @@ -127,15 +132,15 @@ class SceneGestureHandlerTest { get() = (transitionState as Transition).progress fun advanceUntilIdle() { - coroutineScope.testScheduler.advanceUntilIdle() + testScope.testScheduler.advanceUntilIdle() } fun runCurrent() { - coroutineScope.testScheduler.runCurrent() + testScope.testScheduler.runCurrent() } fun assertIdle(currentScene: SceneKey) { - assertWithMessage("transitionState must be Idle").that(transitionState is Idle).isTrue() + assertThat(transitionState).isInstanceOf(Idle::class.java) assertWithMessage("currentScene does not match") .that(transitionState.currentScene) .isEqualTo(currentScene) @@ -148,11 +153,8 @@ class SceneGestureHandlerTest { progress: Float? = null, isUserInputOngoing: Boolean? = null ) { - val transition = transitionState - assertWithMessage("transitionState must be Transition") - .that(transition is Transition) - .isTrue() - transition as Transition + assertThat(transitionState).isInstanceOf(Transition::class.java) + val transition = transitionState as Transition if (currentScene != null) assertWithMessage("currentScene does not match") @@ -180,47 +182,115 @@ class SceneGestureHandlerTest { .that(transition.isUserInputOngoing) .isEqualTo(isUserInputOngoing) } + + fun onDragStarted( + startedPosition: Offset = Offset.Zero, + overSlop: Float, + pointersDown: Int = 1 + ) { + // overSlop should be 0f only if the drag gesture starts with startDragImmediately + if (overSlop == 0f) error("Consider using onDragStartedImmediately()") + onDragStarted(sceneGestureHandler.draggable, startedPosition, overSlop, pointersDown) + } + + fun onDragStartedImmediately(startedPosition: Offset = Offset.Zero, pointersDown: Int = 1) { + onDragStarted( + sceneGestureHandler.draggable, + startedPosition, + overSlop = 0f, + pointersDown + ) + } + + fun onDragStarted( + draggableHandler: DraggableHandler, + startedPosition: Offset = Offset.Zero, + overSlop: Float = 0f, + pointersDown: Int = 1 + ) { + draggableHandler.onDragStarted( + startedPosition = startedPosition, + overSlop = overSlop, + pointersDown = pointersDown, + ) + + // MultiPointerDraggable will always call onDelta with the initial overSlop right after + onDelta(pixels = overSlop) + } + + fun onDelta(pixels: Float) { + sceneGestureHandler.draggable.onDelta(pixels = pixels) + } + + fun onDragStopped(velocity: Float) { + sceneGestureHandler.draggable.onDragStopped(velocity = velocity) + runCurrent() + } + + fun NestedScrollConnection.scroll( + available: Offset, + consumedByScroll: Offset = Offset.Zero, + ) { + val consumedByPreScroll = + onPreScroll( + available = available, + source = NestedScrollSource.Drag, + ) + val consumed = consumedByPreScroll + consumedByScroll + + onPostScroll( + consumed = consumed, + available = available - consumed, + source = NestedScrollSource.Drag + ) + } + + fun NestedScrollConnection.preFling( + available: Velocity, + coroutineScope: CoroutineScope = testScope, + ) { + // onPreFling is a suspend function that returns the consumed velocity once it finishes + // consuming it. In the current scenario, it returns after completing the animation. + // To return immediately, we can initiate a job that allows us to check the status + // before the animation starts. + coroutineScope.launch { onPreFling(available = available) } + runCurrent() + } } private fun runGestureTest(block: suspend TestGestureScope.() -> Unit) { - runMonotonicClockTest { TestGestureScope(coroutineScope = this).block() } - } + runMonotonicClockTest { + val testGestureScope = TestGestureScope(testScope = this) - private fun DraggableHandler.onDragStarted( - overSlop: Float = 0f, - startedPosition: Offset = Offset.Zero, - ) { - onDragStarted(startedPosition, overSlop) - // MultiPointerDraggable will always call onDelta with the initial overSlop right after - onDelta(overSlop) + // run the test + testGestureScope.block() + } } @Test fun testPreconditions() = runGestureTest { assertIdle(currentScene = SceneA) } @Test fun onDragStarted_shouldStartATransition() = runGestureTest { - draggable.onDragStarted(overSlop = down(0.1f)) + onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) } @Test fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest { - draggable.onDragStarted(overSlop = down(0.1f)) + onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) assertThat(progress).isEqualTo(0.1f) - draggable.onDelta(pixels = down(0.1f)) + onDelta(pixels = down(fractionOfScreen = 0.1f)) assertThat(progress).isEqualTo(0.2f) } @Test fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest { - draggable.onDragStarted(overSlop = down(0.1f)) + onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - draggable.onDragStopped( - velocity = velocityThreshold - 0.01f, - ) + onDragStopped(velocity = velocityThreshold - 0.01f) assertTransition(currentScene = SceneA) // wait for the stop animation @@ -230,11 +300,10 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest { - draggable.onDragStarted(overSlop = down(0.1f)) + onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - draggable.onDragStopped(velocity = velocityThreshold) - + onDragStopped(velocity = velocityThreshold) assertTransition(currentScene = SceneC) // wait for the stop animation @@ -244,10 +313,10 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterStarted_returnToIdle() = runGestureTest { - draggable.onDragStarted(overSlop = down(0.1f)) + onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - draggable.onDragStopped(velocity = 0f) + onDragStopped(velocity = 0f) advanceUntilIdle() assertIdle(currentScene = SceneA) } @@ -255,7 +324,7 @@ class SceneGestureHandlerTest { @Test fun onDragReversedDirection_changeToScene() = runGestureTest { // Drag A -> B with progress 0.6 - draggable.onDragStarted(overSlop = -60f) + onDragStarted(overSlop = -60f) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -264,7 +333,7 @@ class SceneGestureHandlerTest { ) // Reverse direction such that A -> C now with 0.4 - draggable.onDelta(100f) + onDelta(pixels = 100f) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -273,7 +342,7 @@ class SceneGestureHandlerTest { ) // After the drag stopped scene C should be committed - draggable.onDragStopped(velocity = velocityThreshold) + onDragStopped(velocity = velocityThreshold) assertTransition(currentScene = SceneC, fromScene = SceneA, toScene = SceneC) // wait for the stop animation @@ -283,9 +352,12 @@ class SceneGestureHandlerTest { @Test fun onDragStartedWithoutActionsInBothDirections_stayIdle() = runGestureTest { - horizontalSceneGestureHandler.draggable.onDragStarted(up(0.3f)) + val horizontalDraggableHandler = horizontalSceneGestureHandler.draggable + + onDragStarted(horizontalDraggableHandler, overSlop = up(fractionOfScreen = 0.3f)) assertIdle(currentScene = SceneA) - horizontalSceneGestureHandler.draggable.onDragStarted(down(0.3f)) + + onDragStarted(horizontalDraggableHandler, overSlop = down(fractionOfScreen = 0.3f)) assertIdle(currentScene = SceneA) } @@ -294,7 +366,7 @@ class SceneGestureHandlerTest { navigateToSceneC() // We are on SceneC which has no action in Down direction - draggable.onDragStarted(10f) + onDragStarted(overSlop = 10f) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -303,7 +375,7 @@ class SceneGestureHandlerTest { ) // Reverse drag direction, it will consume the previous drag - draggable.onDelta(-10f) + onDelta(pixels = -10f) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -312,7 +384,7 @@ class SceneGestureHandlerTest { ) // Continue reverse drag direction, it should record progress to Scene B - draggable.onDelta(-10f) + onDelta(pixels = -10f) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -326,7 +398,10 @@ class SceneGestureHandlerTest { navigateToSceneC() // Start dragging from the bottom - draggable.onDragStarted(up(0.1f), Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE)) + onDragStarted( + startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE), + overSlop = up(fractionOfScreen = 0.1f) + ) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -337,14 +412,14 @@ class SceneGestureHandlerTest { @Test fun onDragToExactlyZero_toSceneIsSet() = runGestureTest { - draggable.onDragStarted(down(0.3f)) + onDragStarted(overSlop = down(fractionOfScreen = 0.3f)) assertTransition( currentScene = SceneA, fromScene = SceneA, toScene = SceneC, progress = 0.3f ) - draggable.onDelta(up(0.3f)) + onDelta(pixels = up(fractionOfScreen = 0.3f)) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -355,8 +430,8 @@ class SceneGestureHandlerTest { private fun TestGestureScope.navigateToSceneC() { assertIdle(currentScene = SceneA) - draggable.onDragStarted(down(1f)) - draggable.onDragStopped(0f) + onDragStarted(overSlop = down(fractionOfScreen = 1f)) + onDragStopped(velocity = 0f) advanceUntilIdle() assertIdle(currentScene = SceneC) } @@ -364,7 +439,7 @@ class SceneGestureHandlerTest { @Test fun onAccelaratedScroll_scrollToThirdScene() = runGestureTest { // Drag A -> B with progress 0.2 - draggable.onDragStarted(overSlop = up(0.2f)) + onDragStarted(overSlop = up(fractionOfScreen = 0.2f)) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -373,13 +448,13 @@ class SceneGestureHandlerTest { ) // Start animation A -> B with progress 0.2 -> 1.0 - draggable.onDragStopped(velocity = -velocityThreshold) + onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB) // While at A -> B do a 100% screen drag (progress 1.2). This should go past B and change // the transition to B -> C with progress 0.2 - draggable.onDragStarted() - draggable.onDelta(up(1f)) + onDragStartedImmediately() + onDelta(pixels = up(fractionOfScreen = 1f)) assertTransition( currentScene = SceneB, fromScene = SceneB, @@ -388,7 +463,7 @@ class SceneGestureHandlerTest { ) // After the drag stopped scene C should be committed - draggable.onDragStopped(velocity = -velocityThreshold) + onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneC, fromScene = SceneB, toScene = SceneC) // wait for the stop animation @@ -398,9 +473,9 @@ class SceneGestureHandlerTest { @Test fun onAccelaratedScrollBothTargetsBecomeNull_settlesToIdle() = runGestureTest { - draggable.onDragStarted(overSlop = up(0.2f)) - draggable.onDelta(up(0.2f)) - draggable.onDragStopped(velocity = -velocityThreshold) + onDragStarted(overSlop = up(fractionOfScreen = 0.2f)) + onDelta(pixels = up(fractionOfScreen = 0.2f)) + onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB) mutableUserActionsA.remove(Swipe.Up) @@ -409,83 +484,79 @@ class SceneGestureHandlerTest { mutableUserActionsB.remove(Swipe.Down) // start accelaratedScroll and scroll over to B -> null - draggable.onDragStarted() - draggable.onDelta(up(0.5f)) - draggable.onDelta(up(0.5f)) + onDragStartedImmediately() + onDelta(pixels = up(fractionOfScreen = 0.5f)) + onDelta(pixels = up(fractionOfScreen = 0.5f)) // here onDragStopped is already triggered, but subsequent onDelta/onDragStopped calls may // still be called. Make sure that they don't crash or change the scene - draggable.onDelta(up(0.5f)) - draggable.onDragStopped(0f) + onDelta(pixels = up(fractionOfScreen = 0.5f)) + onDragStopped(velocity = 0f) advanceUntilIdle() assertIdle(SceneB) // These events can still come in after the animation has settled - draggable.onDelta(up(0.5f)) - draggable.onDragStopped(0f) + onDelta(pixels = up(fractionOfScreen = 0.5f)) + onDragStopped(velocity = 0f) assertIdle(SceneB) } @Test fun onDragTargetsChanged_targetStaysTheSame() = runGestureTest { - draggable.onDragStarted(up(0.1f)) + onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f) mutableUserActionsA[Swipe.Up] = SceneC - draggable.onDelta(up(0.1f)) + onDelta(pixels = up(fractionOfScreen = 0.1f)) // target stays B even though UserActions changed assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.2f) - draggable.onDragStopped(down(0.1f)) + onDragStopped(velocity = down(fractionOfScreen = 0.1f)) advanceUntilIdle() // now target changed to C for new drag - draggable.onDragStarted(up(0.1f)) + onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneC, progress = 0.1f) } @Test fun onDragTargetsChanged_targetsChangeWhenStartingNewDrag() = runGestureTest { - draggable.onDragStarted(up(0.1f)) + onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f) mutableUserActionsA[Swipe.Up] = SceneC - draggable.onDelta(up(0.1f)) - draggable.onDragStopped(down(0.1f)) + onDelta(pixels = up(fractionOfScreen = 0.1f)) + onDragStopped(velocity = down(fractionOfScreen = 0.1f)) // now target changed to C for new drag that started before previous drag settled to Idle - draggable.onDragStarted(overSlop = 0f) - draggable.onDelta(up(0.1f)) + onDragStartedImmediately() + onDelta(pixels = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneC, progress = 0.3f) } @Test fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest { - draggable.onDragStarted(overSlop = down(0.1f)) + onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - draggable.onDragStopped( - velocity = velocityThreshold, - ) - - // The stop animation is not started yet - assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isFalse() - - runCurrent() + onDragStopped(velocity = velocityThreshold) - assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isTrue() - assertThat(sceneGestureHandler.isDrivingTransition).isTrue() assertTransition(currentScene = SceneC) + assertThat(sceneGestureHandler.isDrivingTransition).isTrue() + assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isTrue() // Start a new gesture while the offset is animating - draggable.onDragStarted() + onDragStartedImmediately() assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isFalse() } @Test fun onInitialPreScroll_EdgeWithOverscroll_doNotChangeState() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview) - nestedScroll.onPreScroll(available = offsetY10, source = NestedScrollSource.Drag) + nestedScroll.onPreScroll( + available = downOffset(fractionOfScreen = 0.1f), + source = NestedScrollSource.Drag + ) assertIdle(currentScene = SceneA) } @@ -509,40 +580,29 @@ class SceneGestureHandlerTest { val consumed = nestedScroll.onPostScroll( consumed = Offset.Zero, - available = offsetY10, + available = downOffset(fractionOfScreen = 0.1f), source = NestedScrollSource.Drag ) assertTransition(currentScene = SceneA) assertThat(progress).isEqualTo(0.1f) - assertThat(consumed).isEqualTo(offsetY10) - } - - private fun NestedScrollConnection.scroll( - available: Offset, - consumedByScroll: Offset = Offset.Zero, - ) { - val consumedByPreScroll = - onPreScroll(available = available, source = NestedScrollSource.Drag) - val consumed = consumedByPreScroll + consumedByScroll - onPostScroll( - consumed = consumed, - available = available - consumed, - source = NestedScrollSource.Drag - ) + assertThat(consumed).isEqualTo(downOffset(fractionOfScreen = 0.1f)) } @Test fun afterSceneTransitionIsStarted_interceptPreScrollEvents() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview) - nestedScroll.scroll(available = offsetY10) + nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) assertThat(progress).isEqualTo(0.1f) // start intercept preScroll val consumed = - nestedScroll.onPreScroll(available = offsetY10, source = NestedScrollSource.Drag) + nestedScroll.onPreScroll( + available = downOffset(fractionOfScreen = 0.1f), + source = NestedScrollSource.Drag + ) assertThat(progress).isEqualTo(0.2f) // do nothing on postScroll @@ -553,12 +613,12 @@ class SceneGestureHandlerTest { ) assertThat(progress).isEqualTo(0.2f) - nestedScroll.scroll(available = offsetY10) + nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f)) assertThat(progress).isEqualTo(0.3f) assertTransition(currentScene = SceneA) } - private suspend fun TestGestureScope.preScrollAfterSceneTransition( + private fun TestGestureScope.preScrollAfterSceneTransition( firstScroll: Float, secondScroll: Float ) { @@ -567,10 +627,13 @@ class SceneGestureHandlerTest { nestedScroll.scroll(available = Offset(0f, firstScroll)) // stop scene transition (start the "stop animation") - nestedScroll.onPreFling(available = Velocity.Zero) + nestedScroll.preFling(available = Velocity.Zero) // a pre scroll event, that could be intercepted by SceneGestureHandler - nestedScroll.onPreScroll(Offset(0f, secondScroll), NestedScrollSource.Drag) + nestedScroll.onPreScroll( + available = Offset(0f, secondScroll), + source = NestedScrollSource.Drag + ) } @Test @@ -610,16 +673,17 @@ class SceneGestureHandlerTest { preScrollAfterSceneTransition(firstScroll = firstScroll, secondScroll = secondScroll) + advanceUntilIdle() assertIdle(SceneB) } @Test fun onPreFling_velocityLowerThanThreshold_remainSameScene() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview) - nestedScroll.scroll(available = offsetY10) + nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - nestedScroll.onPreFling(available = Velocity.Zero) + nestedScroll.preFling(available = Velocity.Zero) assertTransition(currentScene = SceneA) // wait for the stop animation @@ -627,15 +691,15 @@ class SceneGestureHandlerTest { assertIdle(currentScene = SceneA) } - private suspend fun TestGestureScope.flingAfterScroll( + private fun TestGestureScope.flingAfterScroll( use: NestedScrollBehavior, idleAfterScroll: Boolean, ) { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = use) - nestedScroll.scroll(available = offsetY10) + nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f)) if (idleAfterScroll) assertIdle(SceneA) else assertTransition(SceneA) - nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold)) + nestedScroll.preFling(available = Velocity(0f, velocityThreshold)) } @Test @@ -679,19 +743,22 @@ class SceneGestureHandlerTest { } /** we started the scroll in the scene, then fling with the velocityThreshold */ - private suspend fun TestGestureScope.flingAfterScrollStartedInScene( + private fun TestGestureScope.flingAfterScrollStartedInScene( use: NestedScrollBehavior, idleAfterScroll: Boolean, ) { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = use) // scroll consumed in child - nestedScroll.scroll(available = offsetY10, consumedByScroll = offsetY10) + nestedScroll.scroll( + available = downOffset(fractionOfScreen = 0.1f), + consumedByScroll = downOffset(fractionOfScreen = 0.1f) + ) // scroll offsetY10 is all available for parents - nestedScroll.scroll(available = offsetY10) + nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f)) if (idleAfterScroll) assertIdle(SceneA) else assertTransition(SceneA) - nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold)) + nestedScroll.preFling(available = Velocity(0f, velocityThreshold)) } @Test @@ -732,20 +799,20 @@ class SceneGestureHandlerTest { @Test fun beforeDraggableStart_drag_shouldBeIgnored() = runGestureTest { - draggable.onDelta(down(0.1f)) + onDelta(pixels = down(fractionOfScreen = 0.1f)) assertIdle(currentScene = SceneA) } @Test fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest { - draggable.onDragStopped(velocityThreshold) + onDragStopped(velocity = velocityThreshold) assertIdle(currentScene = SceneA) } @Test fun beforeNestedScrollStart_stop_shouldBeIgnored() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview) - nestedScroll.onPreFling(Velocity(0f, velocityThreshold)) + nestedScroll.preFling(available = Velocity(0f, velocityThreshold)) assertIdle(currentScene = SceneA) } @@ -753,8 +820,10 @@ class SceneGestureHandlerTest { fun startNestedScrollWhileDragging() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways) + val offsetY10 = downOffset(fractionOfScreen = 0.1f) + // Start a drag and then stop it, given that - draggable.onDragStarted(overSlop = up(0.1f)) + onDragStarted(overSlop = up(0.1f)) assertTransition(currentScene = SceneA) assertThat(progress).isEqualTo(0.1f) @@ -764,7 +833,7 @@ class SceneGestureHandlerTest { assertThat(progress).isEqualTo(0.2f) // this should be ignored, we are scrolling now! - draggable.onDragStopped(-velocityThreshold) + onDragStopped(-velocityThreshold) assertTransition(currentScene = SceneA) nestedScroll.scroll(available = -offsetY10) @@ -773,7 +842,7 @@ class SceneGestureHandlerTest { nestedScroll.scroll(available = -offsetY10) assertThat(progress).isEqualTo(0.4f) - nestedScroll.onPreFling(available = Velocity(0f, -velocityThreshold)) + nestedScroll.preFling(available = Velocity(0f, -velocityThreshold)) assertTransition(currentScene = SceneB) // wait for the stop animation @@ -788,7 +857,7 @@ class SceneGestureHandlerTest { // Swipe up from the middle to transition to scene B. val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f) - draggable.onDragStarted(startedPosition = middle, overSlop = up(0.1f)) + onDragStarted(startedPosition = middle, overSlop = up(0.1f)) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -802,7 +871,7 @@ class SceneGestureHandlerTest { // should intercept it. Because it is intercepted, the overSlop passed to onDragStarted() // should be 0f. assertThat(sceneGestureHandler.shouldImmediatelyIntercept(middle)).isTrue() - draggable.onDragStarted(startedPosition = middle, overSlop = 0f) + onDragStartedImmediately(startedPosition = middle) // We should have intercepted the transition, so the transition should be the same object. assertTransition(currentScene = SceneC, fromScene = SceneC, toScene = SceneB) @@ -813,7 +882,7 @@ class SceneGestureHandlerTest { // instead animate from C to A. val bottom = Offset(SCREEN_SIZE / 2, SCREEN_SIZE) assertThat(sceneGestureHandler.shouldImmediatelyIntercept(bottom)).isFalse() - draggable.onDragStarted(startedPosition = bottom, overSlop = up(0.1f)) + onDragStarted(startedPosition = bottom, overSlop = up(0.1f)) assertTransition( currentScene = SceneC, diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt index fbcd5b27836e..0245cf2a26b8 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt @@ -3,7 +3,9 @@ package com.android.compose.test import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.TestMonotonicFrameClock import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext @@ -12,16 +14,38 @@ import kotlinx.coroutines.withContext * function. * * The [TestCoroutineScheduler] is passed to provide the functionality to wait for idle. + * + * Note: Please refer to the documentation for [runTest], as this feature utilizes it. This will + * provide a comprehensive understanding of all its behaviors. */ -@OptIn(ExperimentalTestApi::class) +@OptIn(ExperimentalTestApi::class, ExperimentalCoroutinesApi::class) fun runMonotonicClockTest(block: suspend MonotonicClockTestScope.() -> Unit) = runTest { - // We need a CoroutineScope (like a TestScope) to create a TestMonotonicFrameClock. - withContext(TestMonotonicFrameClock(this)) { - MonotonicClockTestScope(coroutineScope = this, testScheduler = testScheduler).block() + val testScope: TestScope = this + + withContext(TestMonotonicFrameClock(coroutineScope = testScope)) { + val testScopeWithMonotonicFrameClock: CoroutineScope = this + + val scope = + MonotonicClockTestScope( + testScope = testScopeWithMonotonicFrameClock, + testScheduler = testScope.testScheduler, + backgroundScope = backgroundScope, + ) + + // Run the test + scope.block() } } +/** + * A coroutine scope that for launching test coroutines for Compose. + * + * @param testScheduler The delay-skipping scheduler used by the test dispatchers running the code + * in this scope (see [TestScope.testScheduler]). + * @param backgroundScope A scope for background work (see [TestScope.backgroundScope]). + */ class MonotonicClockTestScope( - coroutineScope: CoroutineScope, - val testScheduler: TestCoroutineScheduler -) : CoroutineScope by coroutineScope + testScope: CoroutineScope, + val testScheduler: TestCoroutineScheduler, + val backgroundScope: CoroutineScope, +) : CoroutineScope by testScope diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt index 4b21105a20a0..e39d7edddcfd 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt @@ -20,8 +20,10 @@ import android.provider.Settings import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @@ -32,6 +34,12 @@ class NotificationSettingsRepository( private val backgroundDispatcher: CoroutineDispatcher, private val secureSettingsRepository: SecureSettingsRepository, ) { + val isNotificationHistoryEnabled: Flow<Boolean> = + secureSettingsRepository + .intSetting(name = Settings.Secure.NOTIFICATION_HISTORY_ENABLED) + .map { it == 1 } + .distinctUntilChanged() + /** The current state of the notification setting. */ val isShowNotificationsOnLockScreenEnabled: StateFlow<Boolean> = secureSettingsRepository diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt index 9ec6ec8d3811..04e8090e3ae2 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt @@ -23,6 +23,8 @@ import kotlinx.coroutines.flow.StateFlow class NotificationSettingsInteractor( private val repository: NotificationSettingsRepository, ) { + val isNotificationHistoryEnabled = repository.isNotificationHistoryEnabled + /** Should notifications be visible on the lockscreen? */ val isShowNotificationsOnLockScreenEnabled: StateFlow<Boolean> = repository.isShowNotificationsOnLockScreenEnabled diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt index b6605ed9d007..fa47a02d78c9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt @@ -59,15 +59,21 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { } @Test + fun isEnabled_settingNotInitialized_returnsFalseByDefault() = + scope.runTest { + val actualValue by collectLastValue(underTest.isEnabled(testUser1)) + + runCurrent() + + Truth.assertThat(actualValue).isFalse() + } + + @Test fun isEnabled_initiallyGetsSettingsValue() = scope.runTest { val actualValue by collectLastValue(underTest.isEnabled(testUser1)) - settings.putIntForUser( - SETTING_NAME, - ENABLED, - testUser1.identifier - ) + settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier) runCurrent() Truth.assertThat(actualValue).isTrue() @@ -78,25 +84,13 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { scope.runTest { val flowValues: List<Boolean> by collectValues(underTest.isEnabled(testUser1)) - settings.putIntForUser( - SETTING_NAME, - DISABLED, - testUser1.identifier - ) + settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier) runCurrent() - settings.putIntForUser( - SETTING_NAME, - ENABLED, - testUser1.identifier - ) + settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier) runCurrent() - settings.putIntForUser( - SETTING_NAME, - DISABLED, - testUser1.identifier - ) + settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier) runCurrent() Truth.assertThat(flowValues.size).isEqualTo(3) @@ -109,26 +103,14 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { val lastValueUser1 by collectLastValue(underTest.isEnabled(testUser1)) val lastValueUser2 by collectLastValue(underTest.isEnabled(testUser2)) - settings.putIntForUser( - SETTING_NAME, - DISABLED, - testUser1.identifier - ) - settings.putIntForUser( - SETTING_NAME, - DISABLED, - testUser2.identifier - ) + settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier) + settings.putIntForUser(SETTING_NAME, DISABLED, testUser2.identifier) runCurrent() Truth.assertThat(lastValueUser1).isFalse() Truth.assertThat(lastValueUser2).isFalse() - settings.putIntForUser( - SETTING_NAME, - ENABLED, - testUser1.identifier - ) + settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier) runCurrent() Truth.assertThat(lastValueUser1).isTrue() @@ -142,11 +124,7 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { runCurrent() Truth.assertThat(success).isTrue() - val actualValue = - settings.getIntForUser( - SETTING_NAME, - testUser1.identifier - ) + val actualValue = settings.getIntForUser(SETTING_NAME, testUser1.identifier) Truth.assertThat(actualValue).isEqualTo(ENABLED) } @@ -157,11 +135,7 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { runCurrent() Truth.assertThat(success).isTrue() - val actualValue = - settings.getIntForUser( - SETTING_NAME, - testUser1.identifier - ) + val actualValue = settings.getIntForUser(SETTING_NAME, testUser1.identifier) Truth.assertThat(actualValue).isEqualTo(DISABLED) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt index 30eb782e5938..3d8159e70061 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt @@ -24,6 +24,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -59,6 +60,16 @@ class ColorInversionRepositoryImplTest : SysuiTestCase() { } @Test + fun isEnabled_settingNotInitialized_returnsFalseByDefault() = + scope.runTest { + val actualValue by collectLastValue(underTest.isEnabled(testUser1)) + + runCurrent() + + Truth.assertThat(actualValue).isFalse() + } + + @Test fun isEnabled_initiallyGetsSettingsValue() = scope.runTest { val actualValue by collectLastValue(underTest.isEnabled(testUser1)) @@ -72,8 +83,7 @@ class ColorInversionRepositoryImplTest : SysuiTestCase() { @Test fun isEnabled_settingUpdated_valueUpdated() = scope.runTest { - val flowValues: List<Boolean> by - collectValues(underTest.isEnabled(testUser1)) + val flowValues: List<Boolean> by collectValues(underTest.isEnabled(testUser1)) settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier) runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt index 76b0d4aaa8ca..45f98be2ca12 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt @@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.util.mockito.KotlinArgumentCaptor @@ -44,78 +45,79 @@ import org.mockito.MockitoAnnotations class CommunalMediaRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var mediaDataManager: MediaDataManager @Mock private lateinit var mediaData: MediaData + @Mock private lateinit var tableLogBuffer: TableLogBuffer + + private lateinit var underTest: CommunalMediaRepositoryImpl private val mediaDataListenerCaptor: KotlinArgumentCaptor<MediaDataManager.Listener> by lazy { KotlinArgumentCaptor(MediaDataManager.Listener::class.java) } - private lateinit var mediaRepository: CommunalMediaRepository - private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) @Before fun setUp() { MockitoAnnotations.initMocks(this) + + underTest = + CommunalMediaRepositoryImpl( + mediaDataManager, + tableLogBuffer, + ) } @Test fun hasAnyMediaOrRecommendation_defaultsToFalse() = testScope.runTest { - mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager) - - val mediaModel = collectLastValue(mediaRepository.mediaModel) + val mediaModel = collectLastValue(underTest.mediaModel) runCurrent() - assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse() + assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isFalse() } @Test fun mediaModel_updatesWhenMediaDataLoaded() = testScope.runTest { - mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager) - // Listener is added verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture()) // Initial value is false. - val mediaModel = collectLastValue(mediaRepository.mediaModel) + val mediaModel = collectLastValue(underTest.mediaModel) runCurrent() - assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse() + assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isFalse() // Change to media available and notify the listener. - whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(true) + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true) whenever(mediaData.createdTimestampMillis).thenReturn(1234L) mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData) runCurrent() // Media active now returns true. - assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isTrue() + assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isTrue() assertThat(mediaModel()?.createdTimestampMillis).isEqualTo(1234L) } @Test fun mediaModel_updatesWhenMediaDataRemoved() = testScope.runTest { - mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager) - // Listener is added verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture()) // Change to media available and notify the listener. - whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(true) + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true) mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData) runCurrent() // Media active now returns true. - val mediaModel = collectLastValue(mediaRepository.mediaModel) - assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isTrue() + val mediaModel = collectLastValue(underTest.mediaModel) + assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isTrue() // Change to media unavailable and notify the listener. - whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(false) + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false) mediaDataListenerCaptor.value.onMediaDataRemoved("key") runCurrent() // Media active now returns false. - assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse() + assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isFalse() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt index d15e15e179fb..6bff0dc7bd9e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt @@ -25,6 +25,8 @@ import com.android.systemui.communal.data.repository.CommunalPrefsRepositoryImpl import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.settings.UserFileManager import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository @@ -36,11 +38,15 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) @android.platform.test.annotations.EnabledOnRavenwood class CommunalPrefsRepositoryImplTest : SysuiTestCase() { + @Mock private lateinit var tableLogBuffer: TableLogBuffer + private lateinit var underTest: CommunalPrefsRepositoryImpl private val kosmos = testKosmos() @@ -51,6 +57,8 @@ class CommunalPrefsRepositoryImplTest : SysuiTestCase() { @Before fun setUp() { + MockitoAnnotations.initMocks(this) + userRepository = kosmos.fakeUserRepository userRepository.setUserInfos(USER_INFOS) @@ -67,6 +75,8 @@ class CommunalPrefsRepositoryImplTest : SysuiTestCase() { kosmos.testDispatcher, userRepository, userFileManager, + logcatLogBuffer("CommunalPrefsRepositoryImplTest"), + tableLogBuffer, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt index 0c66bbb63439..2911a50c2737 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.settings.FakeSettings @@ -34,12 +35,15 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) @android.platform.test.annotations.EnabledOnRavenwood class CommunalTutorialRepositoryImplTest : SysuiTestCase() { + @Mock private lateinit var tableLogBuffer: TableLogBuffer + private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -64,6 +68,7 @@ class CommunalTutorialRepositoryImplTest : SysuiTestCase() { userRepository, secureSettings, logcatLogBuffer("CommunalTutorialRepositoryImplTest"), + tableLogBuffer, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt index c979ca63950a..475179dfdb72 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt @@ -198,13 +198,22 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { } @Test - fun deleteWidget_removeWidgetId_andDeleteFromDb() = + fun deleteWidgetFromDb() = testScope.runTest { val id = 1 - underTest.deleteWidget(id) + underTest.deleteWidgetFromDb(id) runCurrent() verify(communalWidgetDao).deleteWidgetById(id) + } + + @Test + fun deleteWidgetFromHost() = + testScope.runTest { + val id = 1 + underTest.deleteWidgetFromHost(id) + runCurrent() + verify(appWidgetHost).deleteAppWidgetId(id) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index ee01bf9c26e4..c5485c5c5c33 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -642,6 +642,53 @@ class CommunalInteractorTest : SysuiTestCase() { } @Test + fun isCommunalVisible() = + testScope.runTest { + val transitionState = + MutableStateFlow<ObservableCommunalTransitionState>( + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Blank) + ) + communalRepository.setTransitionState(transitionState) + + // isCommunalVisible is false when not on communal. + val isCommunalVisible by collectLastValue(underTest.isCommunalVisible) + assertThat(isCommunalVisible).isEqualTo(false) + + // Start transition to communal. + transitionState.value = + ObservableCommunalTransitionState.Transition( + fromScene = CommunalSceneKey.Blank, + toScene = CommunalSceneKey.Communal, + progress = flowOf(0f), + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + + // isCommunalVisible is true once transition starts. + assertThat(isCommunalVisible).isEqualTo(true) + + // Finish transition to communal + transitionState.value = + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal) + + // isCommunalVisible is true since we're on communal. + assertThat(isCommunalVisible).isEqualTo(true) + + // Start transition away from communal. + transitionState.value = + ObservableCommunalTransitionState.Transition( + fromScene = CommunalSceneKey.Communal, + toScene = CommunalSceneKey.Blank, + progress = flowOf(1.0f), + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + + // isCommunalVisible is still true as the false as soon as transition away runs. + assertThat(isCommunalVisible).isEqualTo(true) + } + + @Test fun testShowWidgetEditorStartsActivity() = testScope.runTest { underTest.showWidgetEditor() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 273d1cd55626..cf727cf5e180 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository @@ -82,6 +83,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { kosmos.communalInteractor, mediaHost, uiEventLogger, + logcatLogBuffer("CommunalEditModeViewModelTest"), ) } @@ -142,6 +144,49 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { } @Test + fun deleteWidget() = + testScope.runTest { + tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + + // Widgets available. + val widgets = + listOf( + CommunalWidgetContentModel( + appWidgetId = 0, + priority = 30, + providerInfo = mock(), + ), + CommunalWidgetContentModel( + appWidgetId = 1, + priority = 20, + providerInfo = mock(), + ), + ) + widgetRepository.setCommunalWidgets(widgets) + + val communalContent by collectLastValue(underTest.communalContent) + + // Widgets and CTA tile are shown. + assertThat(communalContent?.size).isEqualTo(3) + assertThat(communalContent?.get(0)) + .isInstanceOf(CommunalContentModel.Widget::class.java) + assertThat(communalContent?.get(1)) + .isInstanceOf(CommunalContentModel.Widget::class.java) + assertThat(communalContent?.get(2)) + .isInstanceOf(CommunalContentModel.CtaTileInEditMode::class.java) + + underTest.onDeleteWidget(widgets.get(0).appWidgetId) + + // Only one widget and CTA tile remain. + assertThat(communalContent?.size).isEqualTo(2) + val item = communalContent?.get(0) + val appWidgetId = if (item is CommunalContentModel.Widget) item.appWidgetId else null + assertThat(appWidgetId).isEqualTo(widgets.get(1).appWidgetId) + assertThat(communalContent?.get(1)) + .isInstanceOf(CommunalContentModel.CtaTileInEditMode::class.java) + } + + @Test fun reorderWidget_uiEventLogging_start() { underTest.onReorderWidgetStart() verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 0723e8306f71..73d309118548 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -40,6 +40,7 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository @@ -99,6 +100,7 @@ class CommunalViewModelTest : SysuiTestCase() { kosmos.communalInteractor, kosmos.communalTutorialInteractor, mediaHost, + logcatLogBuffer("CommunalViewModelTest"), ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt index efccf7a81ccd..e4ce6cb992c3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt @@ -17,7 +17,7 @@ package com.android.systemui.dreams.homecontrols import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController -import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.authorizedPanelsRepository import com.android.systemui.controls.panels.selectedComponentRepository import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor import com.android.systemui.kosmos.Kosmos @@ -38,4 +38,3 @@ val Kosmos.homeControlsComponentInteractor by val Kosmos.controlsComponent by Kosmos.Fixture<ControlsComponent> { mock() } val Kosmos.controlsListingController by Kosmos.Fixture<ControlsListingController> { mock() } -val Kosmos.authorizedPanelsRepository by Kosmos.Fixture<AuthorizedPanelsRepository> { mock() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt index ce74a905ef66..6ad32cc39f23 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt @@ -27,13 +27,14 @@ import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.panels.AuthorizedPanelsRepository -import com.android.systemui.controls.panels.FakeSelectedComponentRepository import com.android.systemui.controls.panels.SelectedComponentRepository +import com.android.systemui.controls.panels.authorizedPanelsRepository import com.android.systemui.controls.panels.selectedComponentRepository import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope +import com.android.systemui.settings.fakeUserTracker import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository @@ -41,6 +42,9 @@ import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -48,6 +52,7 @@ import org.junit.runner.RunWith import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class HomeControlsComponentInteractorTest : SysuiTestCase() { @@ -59,20 +64,20 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository private lateinit var underTest: HomeControlsComponentInteractor private lateinit var userRepository: FakeUserRepository - private lateinit var selectedComponentRepository: FakeSelectedComponentRepository + private lateinit var selectedComponentRepository: SelectedComponentRepository @Before fun setUp() { MockitoAnnotations.initMocks(this) - userRepository = kosmos.fakeUserRepository - userRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER)) controlsComponent = kosmos.controlsComponent authorizedPanelsRepository = kosmos.authorizedPanelsRepository controlsListingController = kosmos.controlsListingController selectedComponentRepository = kosmos.selectedComponentRepository - selectedComponentRepository.setCurrentUserHandle(PRIMARY_USER.userHandle) + userRepository = kosmos.fakeUserRepository + userRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER)) + whenever(controlsComponent.getControlsListingController()) .thenReturn(Optional.of(controlsListingController)) @@ -90,14 +95,13 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { fun testPanelComponentReturnsComponentNameForSelectedItemByUser() = with(kosmos) { testScope.runTest { - whenever(authorizedPanelsRepository.getAuthorizedPanels()) - .thenReturn(setOf(TEST_PACKAGE_PANEL)) - userRepository.setSelectedUserInfo(PRIMARY_USER) + setActiveUser(PRIMARY_USER) + authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE)) selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) val actualValue by collectLastValue(underTest.panelComponent) assertThat(actualValue).isNull() runServicesUpdate() - assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL) + assertThat(actualValue).isEqualTo(TEST_COMPONENT) } } @@ -105,16 +109,15 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { fun testPanelComponentReturnsComponentNameAsInitialValueWithoutServiceUpdate() = with(kosmos) { testScope.runTest { - whenever(authorizedPanelsRepository.getAuthorizedPanels()) - .thenReturn(setOf(TEST_PACKAGE_PANEL)) - userRepository.setSelectedUserInfo(PRIMARY_USER) + setActiveUser(PRIMARY_USER) + authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE)) selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) whenever(controlsListingController.getCurrentServices()) .thenReturn( - listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)) + listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = true)) ) val actualValue by collectLastValue(underTest.panelComponent) - assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL) + assertThat(actualValue).isEqualTo(TEST_COMPONENT) } } @@ -122,9 +125,8 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { fun testPanelComponentReturnsNullForHomeControlsThatDoesNotSupportPanel() = with(kosmos) { testScope.runTest { - whenever(authorizedPanelsRepository.getAuthorizedPanels()) - .thenReturn(setOf(TEST_PACKAGE_PANEL)) - userRepository.setSelectedUserInfo(PRIMARY_USER) + setActiveUser(PRIMARY_USER) + authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE)) selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL) val actualValue by collectLastValue(underTest.panelComponent) assertThat(actualValue).isNull() @@ -137,8 +139,8 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { fun testPanelComponentReturnsNullWhenPanelIsUnauthorized() = with(kosmos) { testScope.runTest { - whenever(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf()) - userRepository.setSelectedUserInfo(PRIMARY_USER) + setActiveUser(PRIMARY_USER) + authorizedPanelsRepository.removeAuthorizedPanels(setOf(TEST_PACKAGE)) selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) val actualValue by collectLastValue(underTest.panelComponent) assertThat(actualValue).isNull() @@ -151,17 +153,24 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { fun testPanelComponentReturnsComponentNameForDifferentUsers() = with(kosmos) { testScope.runTest { - whenever(authorizedPanelsRepository.getAuthorizedPanels()) - .thenReturn(setOf(TEST_PACKAGE_PANEL)) - userRepository.setSelectedUserInfo(ANOTHER_USER) + val actualValue by collectLastValue(underTest.panelComponent) + + // Secondary user has non-panel selected. + setActiveUser(ANOTHER_USER) selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL) - selectedComponentRepository.setCurrentUserHandle(ANOTHER_USER.userHandle) + + // Primary user has panel selected. + setActiveUser(PRIMARY_USER) + authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE)) selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) - val actualValue by collectLastValue(underTest.panelComponent) - assertThat(actualValue).isNull() runServicesUpdate() - assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL) + assertThat(actualValue).isEqualTo(TEST_COMPONENT) + + // Back to secondary user, should be null. + setActiveUser(ANOTHER_USER) + runServicesUpdate() + assertThat(actualValue).isNull() } } @@ -169,8 +178,7 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { fun testPanelComponentReturnsNullWhenControlsComponentReturnsNullForListingController() = with(kosmos) { testScope.runTest { - whenever(authorizedPanelsRepository.getAuthorizedPanels()) - .thenReturn(setOf(TEST_PACKAGE_PANEL)) + authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE)) whenever(controlsComponent.getControlsListingController()) .thenReturn(Optional.empty()) userRepository.setSelectedUserInfo(PRIMARY_USER) @@ -182,11 +190,17 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { private fun runServicesUpdate(hasPanelBoolean: Boolean = true) { val listings = - listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = hasPanelBoolean)) + listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = hasPanelBoolean)) val callback = withArgCaptor { verify(controlsListingController).addCallback(capture()) } callback.onServicesUpdated(listings) } + private suspend fun TestScope.setActiveUser(user: UserInfo) { + userRepository.setSelectedUserInfo(user) + kosmos.fakeUserTracker.set(listOf(user), 0) + runCurrent() + } + private fun ControlsServiceInfo( componentName: ComponentName, label: CharSequence, @@ -237,19 +251,9 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { ) private const val TEST_PACKAGE = "pkg" private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service") - private const val TEST_PACKAGE_PANEL = "pkg.panel" - private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service") private val TEST_SELECTED_COMPONENT_PANEL = - SelectedComponentRepository.SelectedComponent( - TEST_PACKAGE_PANEL, - TEST_COMPONENT_PANEL, - true - ) + SelectedComponentRepository.SelectedComponent(TEST_PACKAGE, TEST_COMPONENT, true) private val TEST_SELECTED_COMPONENT_NON_PANEL = - SelectedComponentRepository.SelectedComponent( - TEST_PACKAGE_PANEL, - TEST_COMPONENT_PANEL, - false - ) + SelectedComponentRepository.SelectedComponent(TEST_PACKAGE, TEST_COMPONENT, false) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt index 6610e7007eb2..87b1bbb9ff70 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.panels.AuthorizedPanelsRepository import com.android.systemui.controls.panels.SelectedComponentRepository +import com.android.systemui.controls.panels.authorizedPanelsRepository import com.android.systemui.controls.panels.selectedComponentRepository import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor import com.android.systemui.kosmos.applicationCoroutineScope @@ -84,8 +85,7 @@ class HomeControlsDreamStartableTest : SysuiTestCase() { userRepository.setUserInfos(listOf(PRIMARY_USER)) - whenever(authorizedPanelsRepository.getAuthorizedPanels()) - .thenReturn(setOf(TEST_PACKAGE_PANEL)) + authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE_PANEL)) whenever(controlsComponent.getControlsListingController()) .thenReturn(Optional.of(controlsListingController)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java index 74c197075461..2a9bc4a1d80e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java @@ -28,7 +28,6 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.shared.system.InputChannelCompat; -import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.CentralSurfaces; import org.junit.Before; @@ -47,8 +46,6 @@ public class CommunalTouchHandlerTest extends SysuiTestCase { @Mock CentralSurfaces mCentralSurfaces; @Mock - NotificationShadeWindowController mNotificationShadeWindowController; - @Mock DreamTouchHandler.TouchSession mTouchSession; CommunalTouchHandler mTouchHandler; @@ -59,17 +56,10 @@ public class CommunalTouchHandlerTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mTouchHandler = new CommunalTouchHandler( Optional.of(mCentralSurfaces), - mNotificationShadeWindowController, INITIATION_WIDTH); } @Test - public void testSessionStartForcesShadeOpen() { - mTouchHandler.onSessionStart(mTouchSession); - verify(mNotificationShadeWindowController).setForcePluginOpen(true, mTouchHandler); - } - - @Test public void testEventPropagation() { final MotionEvent motionEvent = Mockito.mock(MotionEvent.class); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt index 2fe4ef78bfdc..f400cb18295c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt @@ -25,9 +25,11 @@ import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository +import com.android.systemui.biometrics.domain.interactor.biometricStatusInteractor import com.android.systemui.biometrics.domain.interactor.displayStateInteractor import com.android.systemui.biometrics.domain.interactor.sideFpsSensorInteractor import com.android.systemui.biometrics.fakeFingerprintInteractiveToAuthProvider +import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.coroutines.collectLastValue @@ -146,6 +148,7 @@ class SideFpsProgressBarViewModelTest : SysuiTestCase() { kosmos.fakeKeyguardRepository.setIsDozing(false) kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( AcquiredFingerprintAuthenticationStatus( + AuthenticationReason.DeviceEntryAuthentication, BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START ) ) @@ -165,6 +168,7 @@ class SideFpsProgressBarViewModelTest : SysuiTestCase() { kosmos.fakeKeyguardRepository.setIsDozing(true) kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( AcquiredFingerprintAuthenticationStatus( + AuthenticationReason.DeviceEntryAuthentication, BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START ) ) @@ -177,6 +181,7 @@ class SideFpsProgressBarViewModelTest : SysuiTestCase() { private fun createViewModel() = SideFpsProgressBarViewModel( kosmos.applicationContext, + kosmos.biometricStatusInteractor, kosmos.deviceEntryFingerprintAuthInteractor, kosmos.sideFpsSensorInteractor, kosmos.dozeServiceHost, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt new file mode 100644 index 000000000000..b7b3fdbed955 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt @@ -0,0 +1,79 @@ +/* + * 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.tiles.impl.fontscaling.domain + +import android.graphics.drawable.TestStubDrawable +import android.widget.Switch +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject +import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel +import com.android.systemui.qs.tiles.impl.fontscaling.qsFontScalingTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.res.R +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class FontScalingTileMapperTest : SysuiTestCase() { + private val kosmos = Kosmos() + private val fontScalingTileConfig = kosmos.qsFontScalingTileConfig + + private val mapper by lazy { + FontScalingTileMapper( + context.orCreateTestableResources + .apply { addOverride(R.drawable.ic_qs_font_scaling, TestStubDrawable()) } + .resources, + context.theme + ) + } + + @Test + fun activeStateMatchesEnabledModel() { + val inputModel = FontScalingTileModel + + val outputState = mapper.map(fontScalingTileConfig, inputModel) + + val expectedState = createFontScalingTileState() + QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) + } + + private fun createFontScalingTileState(): QSTileState = + QSTileState( + { + Icon.Loaded( + context.getDrawable( + R.drawable.ic_qs_font_scaling, + )!!, + null + ) + }, + context.getString(R.string.quick_settings_font_scaling_label), + QSTileState.ActivationState.ACTIVE, + null, + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK), + context.getString(R.string.quick_settings_font_scaling_label), + null, + QSTileState.SideViewIcon.Chevron, + QSTileState.EnabledState.ENABLED, + Switch::class.qualifiedName + ) +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractorTest.kt new file mode 100644 index 000000000000..39bc8a6fa6a2 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractorTest.kt @@ -0,0 +1,67 @@ +/* + * 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.tiles.impl.fontscaling.domain.interactor + +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger +import com.google.common.truth.Truth +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class FontScalingTileDataInteractorTest : SysuiTestCase() { + private val underTest: FontScalingTileDataInteractor = FontScalingTileDataInteractor() + private val testUser = UserHandle.of(1) + + @Test + fun collectsExactlyOneValue() = runTest { + val flowValues by + collectValues(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))) + + runCurrent() + + Truth.assertThat(flowValues.size).isEqualTo(1) + } + + @Test + fun lastValueIsNotEmpty() = runTest { + val flowValue by + collectLastValue(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))) + + runCurrent() + + Truth.assertThat(flowValue).isNotNull() + } + + @Test + fun isAvailable() = runTest { + val availability by collectLastValue(underTest.availability(testUser)) + + Truth.assertThat(availability).isTrue() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt new file mode 100644 index 000000000000..2384915c8703 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt @@ -0,0 +1,142 @@ +/* + * 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.tiles.impl.fontscaling.domain.interactor + +import android.provider.Settings +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.accessibility.fontscaling.FontScalingDialogDelegate +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler +import com.android.systemui.qs.tiles.base.actions.intentInputs +import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx +import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx.click +import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel +import com.android.systemui.statusbar.phone.FakeKeyguardStateController +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.nullable +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +class FontScalingUserActionInteractorTest : SysuiTestCase() { + private val kosmos = Kosmos() + private val qsTileIntentUserActionHandler = FakeQSTileIntentUserInputHandler() + private val keyguardStateController = FakeKeyguardStateController() + + private lateinit var underTest: FontScalingTileUserActionInteractor + + @Mock private lateinit var fontScalingDialogDelegate: FontScalingDialogDelegate + @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator + @Mock private lateinit var dialog: SystemUIDialog + @Mock private lateinit var activityStarter: ActivityStarter + + @Captor private lateinit var argumentCaptor: ArgumentCaptor<Runnable> + + @Before + fun setup() { + activityStarter = mock<ActivityStarter>() + dialogLaunchAnimator = mock<DialogLaunchAnimator>() + dialog = mock<SystemUIDialog>() + fontScalingDialogDelegate = + mock<FontScalingDialogDelegate> { whenever(createDialog()).thenReturn(dialog) } + argumentCaptor = ArgumentCaptor.forClass(Runnable::class.java) + + underTest = + FontScalingTileUserActionInteractor( + kosmos.testScope.coroutineContext, + qsTileIntentUserActionHandler, + { fontScalingDialogDelegate }, + keyguardStateController, + dialogLaunchAnimator, + activityStarter + ) + } + + @Test + fun clickTile_screenUnlocked_showDialogAnimationFromView() = + kosmos.testScope.runTest { + keyguardStateController.isShowing = false + val testView = View(context) + + underTest.handleInput(click(FontScalingTileModel, view = testView)) + + verify(activityStarter) + .executeRunnableDismissingKeyguard( + argumentCaptor.capture(), + eq(null), + eq(true), + eq(true), + eq(false) + ) + argumentCaptor.value.run() + verify(dialogLaunchAnimator).showFromView(any(), eq(testView), nullable(), anyBoolean()) + } + + @Test + fun clickTile_onLockScreen_neverShowDialogAnimationFromView_butShowsDialog() = + kosmos.testScope.runTest { + keyguardStateController.isShowing = true + val testView = View(context) + + underTest.handleInput(click(FontScalingTileModel, view = testView)) + + verify(activityStarter) + .executeRunnableDismissingKeyguard( + argumentCaptor.capture(), + eq(null), + eq(true), + eq(true), + eq(false) + ) + argumentCaptor.value.run() + verify(dialogLaunchAnimator, never()) + .showFromView(any(), eq(testView), nullable(), anyBoolean()) + verify(dialog).show() + } + + @Test + fun handleLongClick() = + kosmos.testScope.runTest { + underTest.handleInput(QSTileInputTestKtx.longClick(FontScalingTileModel)) + + Truth.assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1) + val intentInput = qsTileIntentUserActionHandler.intentInputs.last() + val actualIntentAction = intentInput.intent.action + val expectedIntentAction = Settings.ACTION_TEXT_READING_SETTINGS + Truth.assertThat(actualIntentAction).isEqualTo(expectedIntentAction) + } +} 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 504ded30a06c..189ba7b1965a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -76,6 +76,8 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobi import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository +import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor import com.android.systemui.telephony.data.repository.fakeTelephonyRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any @@ -262,6 +264,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { simBouncerInteractor = dagger.Lazy { kosmos.simBouncerInteractor }, authenticationInteractor = dagger.Lazy { kosmos.authenticationInteractor }, windowController = mock(), + deviceProvisioningInteractor = kosmos.deviceProvisioningInteractor, ) startable.start() @@ -518,6 +521,21 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { assertCurrentScene(SceneKey.Lockscreen) } + @Test + fun deviceProvisioningAndFactoryResetProtection() = + testScope.runTest { + val isVisible by collectLastValue(sceneContainerViewModel.isVisible) + kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(false) + kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(true) + assertThat(isVisible).isFalse() + + kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(false) + assertThat(isVisible).isFalse() + + kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(true) + assertThat(isVisible).isTrue() + } + /** * Asserts that the current scene in the view-model matches what's expected. * diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 16cb6231a872..12dbf11255b6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -49,6 +49,8 @@ import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository +import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat @@ -112,6 +114,7 @@ class SceneContainerStartableTest : SysuiTestCase() { simBouncerInteractor = dagger.Lazy { kosmos.simBouncerInteractor }, authenticationInteractor = dagger.Lazy { authenticationInteractor }, windowController = windowController, + deviceProvisioningInteractor = kosmos.deviceProvisioningInteractor, ) } @@ -162,6 +165,30 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + fun hydrateVisibility_basedOnDeviceProvisioningAndFactoryResetProtection() = + testScope.runTest { + val isVisible by collectLastValue(sceneInteractor.isVisible) + prepareState( + isDeviceUnlocked = true, + initialSceneKey = SceneKey.Lockscreen, + isDeviceProvisioned = false, + isFrpActive = true, + ) + + underTest.start() + assertThat(isVisible).isFalse() + + kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(false) + assertThat(isVisible).isFalse() + + kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(true) + assertThat(isVisible).isTrue() + + kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(true) + assertThat(isVisible).isFalse() + } + + @Test fun startsInLockscreenScene() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) @@ -743,6 +770,8 @@ class SceneContainerStartableTest : SysuiTestCase() { authenticationMethod: AuthenticationMethodModel? = null, isLockscreenEnabled: Boolean = true, startsAwake: Boolean = true, + isDeviceProvisioned: Boolean = true, + isFrpActive: Boolean = false, ): MutableStateFlow<ObservableTransitionState> { if (authenticationMethod?.isSecure == true) { assert(isLockscreenEnabled) { @@ -779,6 +808,10 @@ class SceneContainerStartableTest : SysuiTestCase() { } else { powerInteractor.setAsleepForTest() } + + kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(isDeviceProvisioned) + kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(isFrpActive) + runCurrent() return transitionStateFlow diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java index c669c6f6fb1c..1f6ba29c5fc1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java @@ -33,6 +33,10 @@ public class FakeKeyguardStateController implements KeyguardStateController { mOccluded = occluded; } + public void setShowing(boolean isShowing) { + mShowing = isShowing; + } + @Override public boolean isShowing() { return mShowing; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt index c7e5b645db5f..c23a05128df5 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt @@ -61,7 +61,8 @@ constructor( .observerFlow(userHandle.identifier, SETTING_NAME) .onStart { emit(Unit) } .map { - secureSettings.getIntForUser(SETTING_NAME, userHandle.identifier) == ENABLED + secureSettings.getIntForUser(SETTING_NAME, DISABLED, userHandle.identifier) == + ENABLED } .distinctUntilChanged() .flowOn(bgCoroutineContext) diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt index 419eada91f87..852579794a27 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt @@ -61,7 +61,8 @@ constructor( .observerFlow(userHandle.identifier, SETTING_NAME) .onStart { emit(Unit) } .map { - secureSettings.getIntForUser(SETTING_NAME, userHandle.identifier) == ENABLED + secureSettings.getIntForUser(SETTING_NAME, DISABLED, userHandle.identifier) == + ENABLED } .distinctUntilChanged() .flowOn(bgCoroutineContext) diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt index 135ab35a4681..4047623a4b2f 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt @@ -31,6 +31,10 @@ import com.android.systemui.qs.tiles.impl.colorcorrection.domain.ColorCorrection import com.android.systemui.qs.tiles.impl.colorcorrection.domain.interactor.ColorCorrectionTileDataInteractor import com.android.systemui.qs.tiles.impl.colorcorrection.domain.interactor.ColorCorrectionUserActionInteractor import com.android.systemui.qs.tiles.impl.colorcorrection.domain.model.ColorCorrectionTileModel +import com.android.systemui.qs.tiles.impl.fontscaling.domain.FontScalingTileMapper +import com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor.FontScalingTileDataInteractor +import com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor.FontScalingTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel import com.android.systemui.qs.tiles.impl.inversion.domain.ColorInversionTileMapper import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionTileDataInteractor import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionUserActionInteractor @@ -93,6 +97,7 @@ interface QSAccessibilityModule { companion object { const val COLOR_CORRECTION_TILE_SPEC = "color_correction" const val COLOR_INVERSION_TILE_SPEC = "inversion" + const val FONT_SCALING_TILE_SPEC = "font_scaling" @Provides @IntoMap @@ -155,5 +160,36 @@ interface QSAccessibilityModule { stateInteractor, mapper, ) + + @Provides + @IntoMap + @StringKey(FONT_SCALING_TILE_SPEC) + fun provideFontScalingTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(FONT_SCALING_TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_font_scaling, + labelRes = R.string.quick_settings_font_scaling_label, + ), + instanceId = uiEventLogger.getNewInstanceId(), + ) + + /** Inject FontScaling Tile into tileViewModelMap in QSModule */ + @Provides + @IntoMap + @StringKey(FONT_SCALING_TILE_SPEC) + fun provideFontScalingTileViewModel( + factory: QSTileViewModelFactory.Static<FontScalingTileModel>, + mapper: FontScalingTileMapper, + stateInteractor: FontScalingTileDataInteractor, + userActionInteractor: FontScalingTileUserActionInteractor + ): QSTileViewModel = + factory.create( + TileSpec.create(FONT_SCALING_TILE_SPEC), + userActionInteractor, + stateInteractor, + mapper, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index 417608336974..4ea5f4c7af0f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -198,11 +198,13 @@ class UdfpsControllerOverlay @JvmOverloads constructor( UdfpsTouchOverlayBinder.bind( view = this, viewModel = deviceEntryUdfpsTouchOverlayViewModel.get(), + udfpsOverlayInteractor = udfpsOverlayInteractor, ) else -> UdfpsTouchOverlayBinder.bind( view = this, viewModel = defaultUdfpsTouchOverlayViewModel.get(), + udfpsOverlayInteractor = udfpsOverlayInteractor, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt index d28dbc0ae06f..27bb023e3366 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt @@ -24,17 +24,24 @@ import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_OTHER import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR +import android.hardware.biometrics.BiometricSourceType import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations +import com.android.systemui.biometrics.shared.model.AuthenticationState import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn /** A repository for the state of biometric authentication. */ @@ -44,6 +51,9 @@ interface BiometricStatusRepository { * [NotRunning]. */ val fingerprintAuthenticationReason: Flow<AuthenticationReason> + + /** The current status of an acquired fingerprint. */ + val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> } @SysUISingleton @@ -54,53 +64,53 @@ constructor( private val biometricManager: BiometricManager? ) : BiometricStatusRepository { - override val fingerprintAuthenticationReason: Flow<AuthenticationReason> = + private val authenticationState: Flow<AuthenticationState> = conflatedCallbackFlow { - val updateFingerprintAuthenticateReason = { reason: AuthenticationReason -> - trySendWithFailureLogging( - reason, - TAG, - "Error sending fingerprintAuthenticateReason reason" - ) + val updateAuthenticationState = { state: AuthenticationState -> + trySendWithFailureLogging(state, TAG, "Error sending AuthenticationState state") } val authenticationStateListener = object : AuthenticationStateListener.Stub() { override fun onAuthenticationStarted(requestReason: Int) { - val authenticationReason = - when (requestReason) { - REASON_AUTH_BP -> - AuthenticationReason.BiometricPromptAuthentication - REASON_AUTH_KEYGUARD -> - AuthenticationReason.DeviceEntryAuthentication - REASON_AUTH_OTHER -> AuthenticationReason.OtherAuthentication - REASON_AUTH_SETTINGS -> - AuthenticationReason.SettingsAuthentication( - SettingsOperations.OTHER - ) - REASON_ENROLL_ENROLLING -> - AuthenticationReason.SettingsAuthentication( - SettingsOperations.ENROLL_ENROLLING - ) - REASON_ENROLL_FIND_SENSOR -> - AuthenticationReason.SettingsAuthentication( - SettingsOperations.ENROLL_FIND_SENSOR - ) - else -> AuthenticationReason.Unknown - } - updateFingerprintAuthenticateReason(authenticationReason) + val authenticationReason = requestReason.toAuthenticationReason() + updateAuthenticationState( + AuthenticationState.AuthenticationStarted(authenticationReason) + ) } override fun onAuthenticationStopped() { - updateFingerprintAuthenticateReason(AuthenticationReason.NotRunning) + updateAuthenticationState( + AuthenticationState.AuthenticationStopped( + AuthenticationReason.NotRunning + ) + ) } override fun onAuthenticationSucceeded(requestReason: Int, userId: Int) {} override fun onAuthenticationFailed(requestReason: Int, userId: Int) {} + + override fun onAuthenticationAcquired( + biometricSourceType: BiometricSourceType, + requestReason: Int, + acquiredInfo: Int + ) { + val authReason = requestReason.toAuthenticationReason() + + updateAuthenticationState( + AuthenticationState.AuthenticationAcquired( + biometricSourceType, + authReason, + acquiredInfo + ) + ) + } } - updateFingerprintAuthenticateReason(AuthenticationReason.NotRunning) + updateAuthenticationState( + AuthenticationState.AuthenticationStarted(AuthenticationReason.NotRunning) + ) biometricManager?.registerAuthenticationStateListener(authenticationStateListener) awaitClose { biometricManager?.unregisterAuthenticationStateListener( @@ -110,7 +120,36 @@ constructor( } .shareIn(applicationScope, started = SharingStarted.Eagerly, replay = 1) + override val fingerprintAuthenticationReason: Flow<AuthenticationReason> = + authenticationState.map { it.requestReason } + + override val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> = + authenticationState + .filterIsInstance<AuthenticationState.AuthenticationAcquired>() + .filter { + it.biometricSourceType == BiometricSourceType.FINGERPRINT && + // TODO(b/322555228) This check will be removed after consolidating device + // entry auth messages (currently in DeviceEntryFingerprintAuthRepository) + // with BP auth messages (here) + it.requestReason == AuthenticationReason.BiometricPromptAuthentication + } + .map { AcquiredFingerprintAuthenticationStatus(it.requestReason, it.acquiredInfo) } + companion object { private const val TAG = "BiometricStatusRepositoryImpl" } } + +private fun Int.toAuthenticationReason(): AuthenticationReason = + when (this) { + REASON_AUTH_BP -> AuthenticationReason.BiometricPromptAuthentication + REASON_AUTH_KEYGUARD -> AuthenticationReason.DeviceEntryAuthentication + REASON_AUTH_OTHER -> AuthenticationReason.OtherAuthentication + REASON_AUTH_SETTINGS -> + AuthenticationReason.SettingsAuthentication(SettingsOperations.OTHER) + REASON_ENROLL_ENROLLING -> + AuthenticationReason.SettingsAuthentication(SettingsOperations.ENROLL_ENROLLING) + REASON_ENROLL_FIND_SENSOR -> + AuthenticationReason.SettingsAuthentication(SettingsOperations.ENROLL_FIND_SENSOR) + else -> AuthenticationReason.Unknown + } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt index 55a2d3d7563e..ed1557cccd01 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt @@ -20,6 +20,7 @@ import android.app.ActivityTaskManager import com.android.systemui.biometrics.data.repository.BiometricStatusRepository import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations +import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -31,6 +32,9 @@ interface BiometricStatusInteractor { * filtered for when the overlay should be shown, otherwise [NotRunning]. */ val sfpsAuthenticationReason: Flow<AuthenticationReason> + + /** The current status of an acquired fingerprint. */ + val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> } class BiometricStatusInteractorImpl @@ -50,6 +54,9 @@ constructor( } } + override val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> = + biometricStatusRepository.fingerprintAcquiredStatus + companion object { private const val TAG = "BiometricStatusInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt new file mode 100644 index 000000000000..77cf8406725f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt @@ -0,0 +1,57 @@ +/* + * 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.biometrics.shared.model + +import android.hardware.biometrics.BiometricSourceType + +/** + * Describes the current state of biometric authentication, including whether authentication is + * started, stopped, or acquired and relevant parameters, and the [AuthenticationReason] for + * authentication. + */ +sealed interface AuthenticationState { + val requestReason: AuthenticationReason + + /** + * Authentication started + * + * @param requestReason [AuthenticationReason] for starting authentication + */ + data class AuthenticationStarted(override val requestReason: AuthenticationReason) : + AuthenticationState + + /** + * Authentication stopped + * + * @param requestReason [AuthenticationReason.NotRunning] + */ + data class AuthenticationStopped(override val requestReason: AuthenticationReason) : + AuthenticationState + + /** + * Authentication acquired + * + * @param biometricSourceType indicates [BiometricSourceType] of acquired authentication + * @param requestReason indicates [AuthenticationReason] for requesting auth + * @param acquiredInfo indicates + */ + data class AuthenticationAcquired( + val biometricSourceType: BiometricSourceType, + override val requestReason: AuthenticationReason, + val acquiredInfo: Int + ) : AuthenticationState +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt index 80d37b4741e4..7b4be0220ff2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt @@ -50,10 +50,12 @@ import com.android.systemui.util.kotlin.sample import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch /** Binds the side fingerprint sensor indicator view to [SideFpsOverlayViewModel]. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class SideFpsOverlayViewBinder @Inject diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/UdfpsTouchOverlayBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/UdfpsTouchOverlayBinder.kt index bb6a68b7aa2c..2e29c3b59c4c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/UdfpsTouchOverlayBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/UdfpsTouchOverlayBinder.kt @@ -16,9 +16,11 @@ package com.android.systemui.biometrics.ui.binder +import android.util.Log import androidx.core.view.isInvisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.biometrics.ui.view.UdfpsTouchOverlay import com.android.systemui.biometrics.ui.viewmodel.UdfpsTouchOverlayViewModel import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor @@ -31,19 +33,26 @@ object UdfpsTouchOverlayBinder { /** * Updates visibility for the UdfpsTouchOverlay which controls whether the view will receive - * touches or not. + * touches or not. For some devices, this is instead handled by UdfpsOverlayInteractor, so this + * viewBinder will send the information to the interactor. */ @JvmStatic fun bind( view: UdfpsTouchOverlay, viewModel: UdfpsTouchOverlayViewModel, + udfpsOverlayInteractor: UdfpsOverlayInteractor, ) { if (DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()) return view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { viewModel.shouldHandleTouches.collect { shouldHandleTouches -> + Log.d( + "UdfpsTouchOverlayBinder", + "[$view]: update shouldHandleTouches=$shouldHandleTouches" + ) view.isInvisible = !shouldHandleTouches + udfpsOverlayInteractor.setHandleTouches(shouldHandleTouches) } } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index 0f1340a63032..a39aabfabd73 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.biometrics.ui.viewmodel import android.content.Context +import android.content.pm.PackageManager import android.graphics.Rect import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable @@ -244,7 +245,13 @@ constructor( !customBiometricPrompt() || it == null -> null it.logoRes != -1 -> context.resources.getDrawable(it.logoRes, context.theme) it.logoBitmap != null -> BitmapDrawable(context.resources, it.logoBitmap) - else -> context.packageManager.getApplicationIcon(it.opPackageName) + else -> + try { + context.packageManager.getApplicationIcon(it.opPackageName) + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Cannot find icon for package " + it.opPackageName, e) + null + } } } .distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt index ce726034913f..cfda75c7851a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt @@ -41,12 +41,14 @@ import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlay import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel import com.android.systemui.res.R import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged /** Models UI of the side fingerprint sensor indicator view. */ +@OptIn(ExperimentalCoroutinesApi::class) class SideFpsOverlayViewModel @Inject constructor( @@ -176,8 +178,8 @@ constructor( val lottieCallbacks: Flow<List<LottieCallback>> = combine( biometricStatusInteractor.sfpsAuthenticationReason, - deviceEntrySideFpsOverlayInteractor.showIndicatorForDeviceEntry.distinctUntilChanged(), - sideFpsProgressBarViewModel.isVisible, + deviceEntrySideFpsOverlayInteractor.showIndicatorForDeviceEntry, + sideFpsProgressBarViewModel.isVisible ) { reason: AuthenticationReason, showIndicatorForDeviceEntry: Boolean, progressBarIsVisible -> val callbacks = mutableListOf<LottieCallback>() diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt index 779446d3cd37..3b727d2a7ade 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt @@ -97,7 +97,7 @@ interface CommunalWidgetDao { fun getWidgets(): Flow<Map<CommunalItemRank, CommunalWidgetItem>> @Query("SELECT * FROM communal_widget_table WHERE widget_id = :id") - fun getWidgetByIdNow(id: Int): CommunalWidgetItem + fun getWidgetByIdNow(id: Int): CommunalWidgetItem? @Delete fun deleteWidgets(vararg widgets: CommunalWidgetItem) @@ -120,7 +120,9 @@ interface CommunalWidgetDao { fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) { widgetIdToPriorityMap.forEach { (id, priority) -> val widget = getWidgetByIdNow(id) - updateItemRank(widget.itemId, priority) + if (widget != null) { + updateItemRank(widget.itemId, priority) + } } } @@ -134,9 +136,13 @@ interface CommunalWidgetDao { } @Transaction - fun deleteWidgetById(widgetId: Int) { - val widget = getWidgetByIdNow(widgetId) + fun deleteWidgetById(widgetId: Int): Boolean { + val widget = + getWidgetByIdNow(widgetId) ?: // no entry to delete from db + return false + deleteItemRankById(widget.itemId) deleteWidgets(widget) + return true } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt index cf2e33ce1df5..33edb800756d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt @@ -16,15 +16,34 @@ package com.android.systemui.communal.data.model +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableRowLogger + /** Data model of media on the communal hub. */ data class CommunalMediaModel( - val hasAnyMediaOrRecommendation: Boolean, + val hasActiveMediaOrRecommendation: Boolean, val createdTimestampMillis: Long = 0L, -) { +) : Diffable<CommunalMediaModel> { companion object { val INACTIVE = CommunalMediaModel( - hasAnyMediaOrRecommendation = false, + hasActiveMediaOrRecommendation = false, + ) + } + + override fun logDiffs(prevVal: CommunalMediaModel, row: TableRowLogger) { + if (hasActiveMediaOrRecommendation != prevVal.hasActiveMediaOrRecommendation) { + row.logChange( + columnName = "isMediaActive", + value = hasActiveMediaOrRecommendation, + ) + } + + if (createdTimestampMillis != prevVal.createdTimestampMillis) { + row.logChange( + columnName = "mediaCreationTimestamp", + value = createdTimestampMillis.toString(), ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt index e8a561b37d20..201be51b873c 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt @@ -18,6 +18,9 @@ package com.android.systemui.communal.data.repository import com.android.systemui.communal.data.model.CommunalMediaModel import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.dagger.CommunalTableLog +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.pipeline.MediaDataManager import javax.inject.Inject @@ -34,6 +37,7 @@ class CommunalMediaRepositoryImpl @Inject constructor( private val mediaDataManager: MediaDataManager, + @CommunalTableLog tableLogBuffer: TableLogBuffer, ) : CommunalMediaRepository { private val mediaDataListener = @@ -61,13 +65,18 @@ constructor( private val _mediaModel: MutableStateFlow<CommunalMediaModel> = MutableStateFlow(CommunalMediaModel.INACTIVE) - override val mediaModel: Flow<CommunalMediaModel> = _mediaModel + override val mediaModel: Flow<CommunalMediaModel> = + _mediaModel.logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnPrefix = "", + initialValue = CommunalMediaModel.INACTIVE, + ) private fun updateMediaModel(data: MediaData? = null) { - if (mediaDataManager.hasAnyMediaOrRecommendation()) { + if (mediaDataManager.hasActiveMediaOrRecommendation()) { _mediaModel.value = CommunalMediaModel( - hasAnyMediaOrRecommendation = true, + hasActiveMediaOrRecommendation = true, createdTimestampMillis = data?.createdTimestampMillis ?: 0L, ) } else { diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt index c2ea2e93ce58..0e9b32ffd12a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt @@ -21,9 +21,15 @@ import android.content.SharedPreferences import android.content.pm.UserInfo import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog +import com.android.systemui.log.dagger.CommunalTableLog +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.settings.UserFileManager -import com.android.systemui.settings.UserFileManagerExt.observeSharedPreferences import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.util.kotlin.SharedPreferencesExt.observe import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -59,11 +65,21 @@ constructor( @Background private val bgDispatcher: CoroutineDispatcher, private val userRepository: UserRepository, private val userFileManager: UserFileManager, + @CommunalLog logBuffer: LogBuffer, + @CommunalTableLog tableLogBuffer: TableLogBuffer, ) : CommunalPrefsRepository { + private val logger = Logger(logBuffer, "CommunalPrefsRepositoryImpl") + override val isCtaDismissed: Flow<Boolean> = userRepository.selectedUserInfo .flatMapLatest(::observeCtaDismissState) + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnPrefix = "", + columnName = "isCtaDismissed", + initialValue = false, + ) .stateIn( scope = backgroundScope, started = SharingStarted.WhileSubscribed(), @@ -76,11 +92,13 @@ constructor( .edit() .putBoolean(CTA_DISMISSED_STATE, true) .apply() + + logger.i("Dismissed CTA tile") } private fun observeCtaDismissState(user: UserInfo): Flow<Boolean> = - userFileManager - .observeSharedPreferences(FILE_NAME, Context.MODE_PRIVATE, user.id) + getSharedPrefsForUser(user) + .observe(CTA_DISMISSED_STATE) // Emit at the start of collection to ensure we get an initial value .onStart { emit(Unit) } .map { getCtaDismissedState() } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt index 046aaaa33342..4c06e97c89b0 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt @@ -25,6 +25,9 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog +import com.android.systemui.log.dagger.CommunalTableLog +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow @@ -66,6 +69,7 @@ constructor( private val userRepository: UserRepository, private val secureSettings: SecureSettings, @CommunalLog logBuffer: LogBuffer, + @CommunalTableLog tableLogBuffer: TableLogBuffer, ) : CommunalTutorialRepository { companion object { @@ -94,6 +98,12 @@ constructor( settingsState .map { it.hubModeTutorialState } .filterNotNull() + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnPrefix = "", + columnName = "tutorialSettingState", + initialValue = HUB_MODE_TUTORIAL_NOT_STARTED, + ) .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index f36547b01802..2ac9d053f8d0 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -54,8 +54,11 @@ interface CommunalWidgetRepository { configurator: WidgetConfigurator? = null ) {} - /** Delete a widget by id from app widget service and the database. */ - fun deleteWidget(widgetId: Int) {} + /** Delete a widget by id from the database. */ + fun deleteWidgetFromDb(widgetId: Int) {} + + /** Delete a widget by id from app widget host. */ + fun deleteWidgetFromHost(widgetId: Int) {} /** * Update the order of widgets in the database. @@ -143,9 +146,18 @@ constructor( } } - override fun deleteWidget(widgetId: Int) { + override fun deleteWidgetFromDb(widgetId: Int) { + bgScope.launch { + if (communalWidgetDao.deleteWidgetById(widgetId)) { + logger.i("Deleted widget with id $widgetId from DB .") + } else { + logger.w("Widget with id $widgetId cannot be deleted from DB.") + } + } + } + + override fun deleteWidgetFromHost(widgetId: Int) { bgScope.launch { - communalWidgetDao.deleteWidgetById(widgetId) appWidgetHost.deleteAppWidgetId(widgetId) logger.i("Deleted widget with id $widgetId.") } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 80fee640974d..950ac3c3aae6 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -33,16 +33,25 @@ import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog +import com.android.systemui.log.dagger.CommunalTableLog +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.smartspace.data.repository.SmartspaceRepository import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.BooleanFlowOperators.and import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.BooleanFlowOperators.or import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine @@ -50,6 +59,8 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.shareIn /** Encapsulates business-logic related to communal mode. */ @OptIn(ExperimentalCoroutinesApi::class) @@ -57,6 +68,7 @@ import kotlinx.coroutines.flow.map class CommunalInteractor @Inject constructor( + @Application applicationScope: CoroutineScope, private val communalRepository: CommunalRepository, private val widgetRepository: CommunalWidgetRepository, private val communalPrefsRepository: CommunalPrefsRepository, @@ -65,8 +77,12 @@ constructor( userRepository: UserRepository, keyguardInteractor: KeyguardInteractor, private val appWidgetHost: CommunalAppWidgetHost, - private val editWidgetsActivityStarter: EditWidgetsActivityStarter + private val editWidgetsActivityStarter: EditWidgetsActivityStarter, + @CommunalLog logBuffer: LogBuffer, + @CommunalTableLog tableLogBuffer: TableLogBuffer, ) { + private val logger = Logger(logBuffer, "CommunalInteractor") + private val _editModeOpen = MutableStateFlow(false) /** Whether edit mode is currently open. */ @@ -85,6 +101,22 @@ constructor( or(keyguardInteractor.isKeyguardVisible, keyguardInteractor.isDreaming) ) .distinctUntilChanged() + .onEach { available -> + logger.i({ "Communal is ${if (bool1) "" else "un"}available" }) { + bool1 = available + } + } + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnPrefix = "", + columnName = "isCommunalAvailable", + initialValue = false, + ) + .shareIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) /** * Target scene as requested by the underlying [SceneTransitionLayout] or through @@ -129,11 +161,36 @@ constructor( .distinctUntilChanged() /** - * Flow that emits a boolean if the communal UI is showing, ie. the [desiredScene] is the - * [CommunalSceneKey.Communal]. + * Flow that emits a boolean if the communal UI is the target scene, ie. the [desiredScene] is + * the [CommunalSceneKey.Communal]. + * + * This will be true as soon as the desired scene is set programmatically or at whatever point + * during a fling that SceneTransitionLayout determines that the end state will be the communal + * scene. The value also does not change while flinging away until the target scene is no longer + * communal. + * + * If you need a flow that is only true when communal is fully showing and not in transition, + * use [isIdleOnCommunal]. */ + // TODO(b/323215860): rename to something more appropriate after cleaning up usages val isCommunalShowing: Flow<Boolean> = - communalRepository.desiredScene.map { it == CommunalSceneKey.Communal } + communalRepository.desiredScene + .map { it == CommunalSceneKey.Communal } + .distinctUntilChanged() + .onEach { showing -> + logger.i({ "Communal is ${if (bool1) "showing" else "gone"}" }) { bool1 = showing } + } + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnPrefix = "", + columnName = "isCommunalShowing", + initialValue = false, + ) + .shareIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) /** * Flow that emits a boolean if the communal UI is fully visible and not in transition. @@ -146,6 +203,16 @@ constructor( it is ObservableCommunalTransitionState.Idle && it.scene == CommunalSceneKey.Communal } + /** + * Flow that emits a boolean if any portion of the communal UI is visible at all. + * + * This flow will be true during any transition and when idle on the communal scene. + */ + val isCommunalVisible: Flow<Boolean> = + communalRepository.transitionState.map { + !(it is ObservableCommunalTransitionState.Idle && it.scene == CommunalSceneKey.Blank) + } + /** Callback received whenever the [SceneTransitionLayout] finishes a scene transition. */ fun onSceneChanged(newScene: CommunalSceneKey) { communalRepository.setDesiredScene(newScene) @@ -170,9 +237,15 @@ constructor( configurator: WidgetConfigurator?, ) = widgetRepository.addWidget(componentName, priority, configurator) - /** Delete a widget by id. */ - fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id) + /** + * Delete a widget by id from the database. [CommunalAppWidgetHostStartable] invokes this + * function to manage the deletion from the database for uninstalled or user-deleted widgets, + * following the removal of a widget from the host. + */ + fun deleteWidgetFromDb(id: Int) = widgetRepository.deleteWidgetFromDb(id) + /** Delete a widget by id from AppWidgetHost. Called when user deletes a widget from the hub */ + fun deleteWidgetFromHost(id: Int) = widgetRepository.deleteWidgetFromHost(id) /** * Reorder the widgets. * @@ -245,7 +318,7 @@ constructor( ) // Add UMO - if (media.hasAnyMediaOrRecommendation) { + if (media.hasActiveMediaOrRecommendation) { ongoingContent.add( CommunalContentModel.Umo( createdTimestampMillis = media.createdTimestampMillis, diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt index 309c84e4e955..1404ee20cb72 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt @@ -22,6 +22,9 @@ import com.android.systemui.communal.data.repository.CommunalTutorialRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.log.dagger.CommunalTableLog +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -49,6 +52,7 @@ constructor( keyguardInteractor: KeyguardInteractor, private val communalRepository: CommunalRepository, communalInteractor: CommunalInteractor, + @CommunalTableLog tableLogBuffer: TableLogBuffer, ) { /** An observable for whether the tutorial is available. */ val isTutorialAvailable: StateFlow<Boolean> = @@ -61,6 +65,12 @@ constructor( isKeyguardVisible && tutorialSettingState != Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED } + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnPrefix = "", + columnName = "isTutorialAvailable", + initialValue = false, + ) .stateIn( scope = scope, started = SharingStarted.WhileSubscribed(), diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index ebcfb8bb8209..69d55815fe01 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -21,6 +21,9 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.media.dagger.MediaModule import javax.inject.Inject @@ -29,6 +32,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach /** The view model for communal hub in edit mode. */ @SysUISingleton @@ -38,21 +42,27 @@ constructor( private val communalInteractor: CommunalInteractor, @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, private val uiEventLogger: UiEventLogger, + @CommunalLog logBuffer: LogBuffer, ) : BaseCommunalViewModel(communalInteractor, mediaHost) { + + private val logger = Logger(logBuffer, "CommunalEditModeViewModel") + override val isEditMode = true // Only widgets are editable. The CTA tile comes last in the list and remains visible. override val communalContent: Flow<List<CommunalContentModel>> = - communalInteractor.widgetContent.map { widgets -> - widgets + listOf(CommunalContentModel.CtaTileInEditMode()) - } + communalInteractor.widgetContent + .map { widgets -> widgets + listOf(CommunalContentModel.CtaTileInEditMode()) } + .onEach { models -> + logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } } + } private val _reorderingWidgets = MutableStateFlow(false) override val reorderingWidgets: StateFlow<Boolean> get() = _reorderingWidgets - override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id) + override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidgetFromHost(id) override fun onReorderWidgets(widgetIdToPriorityMap: Map<Int, Int>) = communalInteractor.updateWidgetOrder(widgetIdToPriorityMap) diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index d7a3705248ff..40d2d1656fbc 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -21,6 +21,9 @@ import com.android.systemui.communal.domain.interactor.CommunalTutorialInteracto import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.media.controls.ui.MediaHostState @@ -37,6 +40,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch /** The default view model used for showing the communal hub. */ @@ -48,22 +52,30 @@ constructor( private val communalInteractor: CommunalInteractor, tutorialInteractor: CommunalTutorialInteractor, @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, + @CommunalLog logBuffer: LogBuffer, ) : BaseCommunalViewModel(communalInteractor, mediaHost) { + + private val logger = Logger(logBuffer, "CommunalViewModel") + @OptIn(ExperimentalCoroutinesApi::class) override val communalContent: Flow<List<CommunalContentModel>> = - tutorialInteractor.isTutorialAvailable.flatMapLatest { isTutorialMode -> - if (isTutorialMode) { - return@flatMapLatest flowOf(communalInteractor.tutorialContent) + tutorialInteractor.isTutorialAvailable + .flatMapLatest { isTutorialMode -> + if (isTutorialMode) { + return@flatMapLatest flowOf(communalInteractor.tutorialContent) + } + combine( + communalInteractor.ongoingContent, + communalInteractor.widgetContent, + communalInteractor.ctaTileContent, + ) { ongoing, widgets, ctaTile, + -> + ongoing + widgets + ctaTile + } } - combine( - communalInteractor.ongoingContent, - communalInteractor.widgetContent, - communalInteractor.ctaTileContent, - ) { ongoing, widgets, ctaTile, - -> - ongoing + widgets + ctaTile + .onEach { models -> + logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } } } - } private val _isPopupOnDismissCtaShowing: MutableStateFlow<Boolean> = MutableStateFlow(false) override val isPopupOnDismissCtaShowing: Flow<Boolean> = @@ -75,7 +87,7 @@ constructor( with(mediaHost) { expansion = MediaHostState.EXPANDED expandedMatchesParentHeight = true - showsOnlyActiveMedia = false + showsOnlyActiveMedia = true falsingProtectionNeeded = false init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt index fb9abeb4fbcf..6fd0fbe80fd7 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt @@ -50,7 +50,7 @@ constructor( .launchIn(bgScope) appWidgetHost.appWidgetIdToRemove - .onEach { appWidgetId -> communalInteractor.deleteWidget(appWidgetId) } + .onEach { appWidgetId -> communalInteractor.deleteWidgetFromDb(appWidgetId) } .launchIn(bgScope) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index eb6dc4379db6..92e8153840eb 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -32,6 +32,9 @@ import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.compose.ComposeFacade.setCommunalEditWidgetActivityContent +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog import com.android.systemui.res.R import javax.inject.Inject @@ -42,7 +45,8 @@ constructor( private val communalViewModel: CommunalEditModeViewModel, private var windowManagerService: IWindowManager? = null, private val uiEventLogger: UiEventLogger, - private val widgetConfiguratorFactory: WidgetConfigurationController.Factory + private val widgetConfiguratorFactory: WidgetConfigurationController.Factory, + @CommunalLog logBuffer: LogBuffer, ) : ComponentActivity() { companion object { private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag" @@ -54,6 +58,8 @@ constructor( const val EXTRA_PRESELECTED_KEY = "preselected_key" } + private val logger = Logger(logBuffer, "EditWidgetsActivity") + private val widgetConfigurator by lazy { widgetConfiguratorFactory.create(this) } private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> = @@ -157,11 +163,15 @@ constructor( override fun onStart() { super.onStart() + + logger.i("Starting the communal widget editor activity") uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_SHOWN) } override fun onStop() { super.onStop() + + logger.i("Stopping the communal widget editor activity") uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_GONE) } diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt index ae9c37aa0e7b..b35bec4eead1 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt @@ -17,11 +17,16 @@ package com.android.systemui.controls.panels +import android.os.UserHandle +import kotlinx.coroutines.flow.Flow + /** * Repository for keeping track of which packages the panel has authorized to show control panels * (embedded activity). */ interface AuthorizedPanelsRepository { + /** Exposes the authorized panels as a [Flow] for subscribing to updates */ + fun observeAuthorizedPanels(user: UserHandle): Flow<Set<String>> /** A set of package names that the user has previously authorized to show panels. */ fun getAuthorizedPanels(): Set<String> diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt index 4e935df12ac1..7c2dae34707b 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt @@ -19,11 +19,16 @@ package com.android.systemui.controls.panels import android.content.Context import android.content.SharedPreferences +import android.os.UserHandle import com.android.systemui.res.R import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl +import com.android.systemui.util.kotlin.SharedPreferencesExt.observe import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart class AuthorizedPanelsRepositoryImpl @Inject @@ -33,19 +38,24 @@ constructor( private val userTracker: UserTracker, ) : AuthorizedPanelsRepository { + override fun observeAuthorizedPanels(user: UserHandle): Flow<Set<String>> { + val prefs = instantiateSharedPrefs(user) + return prefs.observe(KEY).onStart { emit(Unit) }.map { getAuthorizedPanelsInternal(prefs) } + } + override fun getAuthorizedPanels(): Set<String> { - return getAuthorizedPanelsInternal(instantiateSharedPrefs()) + return getAuthorizedPanelsInternal(instantiateSharedPrefs(userTracker.userHandle)) } override fun getPreferredPackages(): Set<String> = context.resources.getStringArray(R.array.config_controlsPreferredPackages).toSet() override fun addAuthorizedPanels(packageNames: Set<String>) { - addAuthorizedPanelsInternal(instantiateSharedPrefs(), packageNames) + addAuthorizedPanelsInternal(instantiateSharedPrefs(userTracker.userHandle), packageNames) } override fun removeAuthorizedPanels(packageNames: Set<String>) { - with(instantiateSharedPrefs()) { + with(instantiateSharedPrefs(userTracker.userHandle)) { val currentSet = getAuthorizedPanelsInternal(this) edit().putStringSet(KEY, currentSet - packageNames).apply() } @@ -63,12 +73,12 @@ constructor( sharedPreferences.edit().putStringSet(KEY, currentSet + packageNames).apply() } - private fun instantiateSharedPrefs(): SharedPreferences { + private fun instantiateSharedPrefs(user: UserHandle): SharedPreferences { val sharedPref = userFileManager.getSharedPreferences( DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, Context.MODE_PRIVATE, - userTracker.userId, + user.identifier, ) // We should add default packages when we've never run this diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt index 0baa81a12e4f..9be049400962 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt @@ -20,21 +20,18 @@ import android.content.ComponentName import android.content.Context import android.content.SharedPreferences import android.os.UserHandle -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.flags.FeatureFlags import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl +import com.android.systemui.util.kotlin.SharedPreferencesExt.observe import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) @SysUISingleton @@ -43,9 +40,7 @@ class SelectedComponentRepositoryImpl constructor( private val userFileManager: UserFileManager, private val userTracker: UserTracker, - private val featureFlags: FeatureFlags, - @Background private val bgDispatcher: CoroutineDispatcher, - @Application private val applicationScope: CoroutineScope + @Background private val bgDispatcher: CoroutineDispatcher ) : SelectedComponentRepository { private companion object { @@ -66,22 +61,11 @@ constructor( override fun selectedComponentFlow( userHandle: UserHandle ): Flow<SelectedComponentRepository.SelectedComponent?> { - return conflatedCallbackFlow { - val sharedPreferencesByUserId = getSharedPreferencesForUser(userHandle.identifier) - val listener = - SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - applicationScope.launch(bgDispatcher) { - if (key == PREF_COMPONENT) { - trySend(getSelectedComponent(userHandle)) - } - } - } - sharedPreferencesByUserId.registerOnSharedPreferenceChangeListener(listener) - send(getSelectedComponent(userHandle)) - awaitClose { - sharedPreferencesByUserId.unregisterOnSharedPreferenceChangeListener(listener) - } - } + val prefs = getSharedPreferencesForUser(userHandle.identifier) + return prefs + .observe(PREF_COMPONENT) + .onStart { emit(Unit) } + .map { getSelectedComponent(userHandle) } .flowOn(bgDispatcher) } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index e9d1e94a63b1..dd186d624c84 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -42,7 +42,7 @@ import com.android.systemui.recents.RecentsImplementation; import com.android.systemui.rotationlock.RotationLockModule; import com.android.systemui.scene.SceneContainerFrameworkModule; import com.android.systemui.screenshot.ReferenceScreenshotModule; -import com.android.systemui.settings.dagger.MultiUserUtilsModule; +import com.android.systemui.settings.MultiUserUtilsModule; import com.android.systemui.shade.NotificationShadeWindowControllerImpl; import com.android.systemui.shade.ShadeModule; import com.android.systemui.statusbar.CommandQueue; diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 5ee2045865a6..a3d6ad456e54 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -47,7 +47,7 @@ import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver import com.android.systemui.media.taptotransfer.sender.MediaTttSenderCoordinator import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherCoreStartable -import com.android.systemui.settings.dagger.MultiUserUtilsModule +import com.android.systemui.settings.MultiUserUtilsModule import com.android.systemui.shortcut.ShortcutKeyDispatcher import com.android.systemui.statusbar.ImmersiveModeConfirmation import com.android.systemui.statusbar.gesture.GesturePointerEventListener diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt index 91e0547ff93d..0cab10db756f 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt @@ -47,24 +47,30 @@ class HomeControlsComponentInteractor @Inject constructor( private val selectedComponentRepository: SelectedComponentRepository, - private val controlsComponent: ControlsComponent, - private val authorizedPanelsRepository: AuthorizedPanelsRepository, + controlsComponent: ControlsComponent, + authorizedPanelsRepository: AuthorizedPanelsRepository, userRepository: UserRepository, @Background private val bgScope: CoroutineScope ) { - private val controlsListingController = + private val controlsListingController: ControlsListingController? = controlsComponent.getControlsListingController().getOrNull() /** Gets the current user's selected panel, or null if there isn't one */ - private val selectedItem: Flow<SelectedComponentRepository.SelectedComponent?> = + private val selectedPanel: Flow<SelectedComponentRepository.SelectedComponent?> = userRepository.selectedUserInfo .flatMapLatest { user -> selectedComponentRepository.selectedComponentFlow(user.userHandle) } .map { if (it?.isPanel == true) it else null } - /** Gets all the available panels which are authorized by the user */ - private fun allPanelItem(): Flow<List<PanelComponent>> { + /** Gets the current user's authorized panels */ + private val allAuthorizedPanels: Flow<Set<String>> = + userRepository.selectedUserInfo.flatMapLatest { user -> + authorizedPanelsRepository.observeAuthorizedPanels(user.userHandle) + } + + /** Gets all the available services from [ControlsListingController] */ + private fun allAvailableServices(): Flow<List<ControlsServiceInfo>> { if (controlsListingController == null) { return emptyFlow() } @@ -79,26 +85,38 @@ constructor( awaitClose { controlsListingController.removeCallback(listener) } } .onStart { emit(controlsListingController.getCurrentServices()) } - .map { serviceInfos -> - val authorizedPanels = authorizedPanelsRepository.getAuthorizedPanels() - serviceInfos.mapNotNull { - val panelActivity = it.panelActivity - if (it.componentName.packageName in authorizedPanels && panelActivity != null) { - PanelComponent(it.componentName, panelActivity) - } else { - null - } + } + + /** Gets all panels which are available and authorized by the user */ + private val allAvailableAndAuthorizedPanels: Flow<List<PanelComponent>> = + combine( + allAvailableServices(), + allAuthorizedPanels, + ) { serviceInfos, authorizedPanels -> + serviceInfos.mapNotNull { + val panelActivity = it.panelActivity + if (it.componentName.packageName in authorizedPanels && panelActivity != null) { + PanelComponent(it.componentName, panelActivity) + } else { + null } } - } + } + val panelComponent: StateFlow<ComponentName?> = - combine(allPanelItem(), selectedItem) { items, selected -> + combine( + allAvailableAndAuthorizedPanels, + selectedPanel, + ) { panels, selected -> val item = - items.firstOrNull { it.componentName == selected?.componentName } - ?: items.firstOrNull() + panels.firstOrNull { it.componentName == selected?.componentName } + ?: panels.firstOrNull() item?.panelActivity } .stateIn(bgScope, SharingStarted.WhileSubscribed(), null) - data class PanelComponent(val componentName: ComponentName, val panelActivity: ComponentName) + private data class PanelComponent( + val componentName: ComponentName, + val panelActivity: ComponentName, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java index c9b56a2ebd9a..05279fcdf51c 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java @@ -23,7 +23,6 @@ import android.graphics.Region; import android.view.GestureDetector; import android.view.MotionEvent; -import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.CentralSurfaces; import java.util.Optional; @@ -34,17 +33,14 @@ import javax.inject.Named; /** {@link DreamTouchHandler} responsible for handling touches to open communal hub. **/ public class CommunalTouchHandler implements DreamTouchHandler { private final int mInitiationWidth; - private final NotificationShadeWindowController mNotificationShadeWindowController; private final Optional<CentralSurfaces> mCentralSurfaces; @Inject public CommunalTouchHandler( Optional<CentralSurfaces> centralSurfaces, - NotificationShadeWindowController notificationShadeWindowController, @Named(COMMUNAL_GESTURE_INITIATION_WIDTH) int initiationWidth) { mInitiationWidth = initiationWidth; mCentralSurfaces = centralSurfaces; - mNotificationShadeWindowController = notificationShadeWindowController; } @Override @@ -60,9 +56,8 @@ public class CommunalTouchHandler implements DreamTouchHandler { } private void handleSessionStart(CentralSurfaces surfaces, TouchSession session) { - // Force the notification shade window open (otherwise the hub won't show while swiping). - mNotificationShadeWindowController.setForcePluginOpen(true, this); - + // Notification shade window has its own logic to be visible if the hub is open, no need to + // do anything here other than send touch events over. session.registerInputListener(ev -> { surfaces.handleDreamTouch((MotionEvent) ev); if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) { diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index df0566e246a8..41ce3fd11e8a 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -23,9 +23,12 @@ import com.android.server.notification.Flags.crossAppPoliteNotifications import com.android.server.notification.Flags.politeNotifications import com.android.server.notification.Flags.vibrateWhileUnlocked import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR +import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT import com.android.systemui.Flags.keyguardBottomAreaRefactor +import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW +import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor @@ -55,6 +58,11 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // SceneContainer dependencies SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta } SceneContainerFlag.getMainStaticFlag() dependsOn MIGRATE_KEYGUARD_STATUS_BAR_VIEW + + // ComposeLockscreen dependencies + ComposeLockscreen.token dependsOn KeyguardShadeMigrationNssl.token + ComposeLockscreen.token dependsOn keyguardBottomAreaRefactor + ComposeLockscreen.token dependsOn migrateClocksToBlueprint } private inline val politeNotifications @@ -65,4 +73,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked()) private inline val keyguardBottomAreaRefactor get() = FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()) + private inline val migrateClocksToBlueprint + get() = FlagToken(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, migrateClocksToBlueprint()) } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index c69c9ef93761..6eff79284847 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -585,10 +585,6 @@ object Flags { @JvmField val SHARE_WIFI_QS_BUTTON = releasedFlag("share_wifi_qs_button") - // TODO(b/287205379): Tracking bug - @JvmField - val QS_CONTAINER_GRAPH_OPTIMIZER = releasedFlag( "qs_container_graph_optimizer") - /** Enable showing a dialog when clicking on Quick Settings bluetooth tile. */ @JvmField val BLUETOOTH_QS_TILE_DIALOG = releasedFlag("bluetooth_qs_tile_dialog") diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt index d5f082a2566f..72a81cbac9d5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt @@ -19,10 +19,10 @@ package com.android.systemui.keyboard.stickykeys.shared.model @JvmInline value class Locked(val locked: Boolean) -enum class ModifierKey(val text: String) { +enum class ModifierKey(val displayedText: String) { ALT("ALT LEFT"), ALT_GR("ALT RIGHT"), CTRL("CTRL"), - META("META"), + META("ACTION"), SHIFT("SHIFT"), } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index b5f9c69a71b0..4cabd70cb142 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -17,7 +17,6 @@ package com.android.systemui.keyguard; import static android.content.pm.PackageManager.PERMISSION_GRANTED; -import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; @@ -61,7 +60,6 @@ import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationDefinition; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; -import android.view.WindowManager; import android.view.WindowManagerPolicyConstants; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; @@ -108,20 +106,7 @@ public class KeyguardService extends Service { private final ScreenOnCoordinator mScreenOnCoordinator; private final ShellTransitions mShellTransitions; private final DisplayTracker mDisplayTracker; - private PowerInteractor mPowerInteractor; - - private static int newModeToLegacyMode(int newMode) { - switch (newMode) { - case WindowManager.TRANSIT_OPEN: - case WindowManager.TRANSIT_TO_FRONT: - return MODE_OPENING; - case WindowManager.TRANSIT_CLOSE: - case WindowManager.TRANSIT_TO_BACK: - return MODE_CLOSING; - default: - return 2; // MODE_CHANGING - } - } + private final PowerInteractor mPowerInteractor; private static RemoteAnimationTarget[] wrap(TransitionInfo info, boolean wallpapers, SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap, @@ -253,8 +238,7 @@ public class KeyguardService extends Service { public void mergeAnimation(IBinder candidateTransition, TransitionInfo candidateInfo, SurfaceControl.Transaction candidateT, IBinder currentTransition, - IRemoteTransitionFinishedCallback candidateFinishCallback) - throws RemoteException { + IRemoteTransitionFinishedCallback candidateFinishCallback) { if ((candidateInfo.getFlags() & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0) { keyguardViewMediator.setPendingLock(true); keyguardViewMediator.cancelKeyguardExitAnimation(); @@ -265,13 +249,13 @@ public class KeyguardService extends Service { runner.onAnimationCancelled(); finish(currentTransition); } catch (RemoteException e) { - // nothing, we'll just let it finish on its own I guess. + // Ignore. } } @Override - public void onTransitionConsumed(IBinder transition, boolean aborted) - throws RemoteException { + public void onTransitionConsumed(IBinder transition, boolean aborted) { + // No-op. } private static void initAlphaForAnimationTargets(@NonNull SurfaceControl.Transaction t, @@ -283,7 +267,7 @@ public class KeyguardService extends Service { } private void finish(IBinder transition) throws RemoteException { - IRemoteTransitionFinishedCallback finishCallback = null; + final IRemoteTransitionFinishedCallback finishCallback; SurfaceControl.Transaction finishTransaction = null; synchronized (mLeashMap) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 794befa3725d..f085e88da954 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -45,12 +45,12 @@ import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.annotation.SuppressLint; import android.app.AlarmManager; import android.app.BroadcastOptions; import android.app.IActivityTaskManager; import android.app.PendingIntent; import android.app.StatusBarManager; -import android.app.WallpaperManager; import android.app.WindowConfiguration; import android.app.trust.TrustManager; import android.content.BroadcastReceiver; @@ -174,6 +174,8 @@ import com.android.systemui.util.time.SystemClock; import com.android.systemui.wallpapers.data.repository.WallpaperRepository; import com.android.wm.shell.keyguard.KeyguardTransitions; +import dagger.Lazy; + import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -183,7 +185,6 @@ import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; -import dagger.Lazy; import kotlinx.coroutines.CoroutineDispatcher; /** @@ -326,7 +327,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private AlarmManager mAlarmManager; private AudioManager mAudioManager; private StatusBarManager mStatusBarManager; - private WallpaperManager mWallpaperManager; private final IStatusBarService mStatusBarService; private final IBinder mStatusBarDisableToken = new Binder(); private final UserTracker mUserTracker; @@ -356,13 +356,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private final SecureSettings mSecureSettings; private final SystemSettings mSystemSettings; private final SystemClock mSystemClock; - private SystemPropertiesHelper mSystemPropertiesHelper; + private final SystemPropertiesHelper mSystemPropertiesHelper; /** * Used to keep the device awake while to ensure the keyguard finishes opening before * we sleep. */ - private PowerManager.WakeLock mShowKeyguardWakeLock; + private final PowerManager.WakeLock mShowKeyguardWakeLock; private final Lazy<KeyguardViewController> mKeyguardViewControllerLazy; @@ -405,13 +405,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private boolean mWakeAndUnlocking = false; /** - * Helps remember whether the screen has turned on since the last time - * it turned off due to timeout. see {@link #onScreenTurnedOff(int)} + * Helps remember whether the screen has turned on since the last time it turned off due to + * timeout. See {@link #onScreenTurnedOff} */ private int mDelayedShowingSequence; /** - * Similar to {@link #mDelayedProfileShowingSequence}, but it is for profile case. + * Similar to {@link #mDelayedShowingSequence}, but it is for profile case. */ private int mDelayedProfileShowingSequence; @@ -439,7 +439,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private boolean mGoingToSleep; // last known state of the cellular connection - private String mPhoneState = TelephonyManager.EXTRA_STATE_IDLE; + private final String mPhoneState = TelephonyManager.EXTRA_STATE_IDLE; /** * Whether a hide is pending and we are just waiting for #startKeyguardExitAnimation to be @@ -1088,8 +1088,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException { - if (!handleOnAnimationStart( - transit, apps, wallpapers, nonApps, finishedCallback)) { + if (!handleOnAnimationStart(apps, finishedCallback)) { // Usually we rely on animation completion to synchronize occluded status, // but there was no animation to play, so just update it now. setOccluded(true /* isOccluded */, false /* animate */); @@ -1097,9 +1096,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } } - private boolean handleOnAnimationStart(int transit, RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, - IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException { + private boolean handleOnAnimationStart(RemoteAnimationTarget[] apps, + IRemoteAnimationFinishedCallback finishedCallback) { if (apps == null || apps.length == 0 || apps[0] == null) { Log.d(TAG, "No apps provided to the OccludeByDream runner; " + "skipping occluding animation."); @@ -1107,8 +1105,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } final RemoteAnimationTarget primary = apps[0]; - final boolean isDream = (apps[0].taskInfo != null - && apps[0].taskInfo.topActivityType + final boolean isDream = (primary.taskInfo != null + && primary.taskInfo.topActivityType == WindowConfiguration.ACTIVITY_TYPE_DREAM); if (!isDream) { Log.w(TAG, "The occluding app isn't Dream; " @@ -1322,9 +1320,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } private DeviceConfigProxy mDeviceConfig; - private DozeParameters mDozeParameters; - private SelectedUserInteractor mSelectedUserInteractor; - private KeyguardInteractor mKeyguardInteractor; + private final DozeParameters mDozeParameters; + private final SelectedUserInteractor mSelectedUserInteractor; + private final KeyguardInteractor mKeyguardInteractor; @VisibleForTesting protected FoldGracePeriodProvider mFoldGracePeriodProvider = new FoldGracePeriodProvider(); @@ -1346,14 +1344,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private final Lazy<KeyguardUnlockAnimationController> mKeyguardUnlockAnimationControllerLazy; private final InteractionJankMonitor mInteractionJankMonitor; private boolean mWallpaperSupportsAmbientMode; - private ScreenOnCoordinator mScreenOnCoordinator; private final KeyguardTransitions mKeyguardTransitions; - private Lazy<ActivityLaunchAnimator> mActivityLaunchAnimator; - private Lazy<ScrimController> mScrimControllerLazy; - private IActivityTaskManager mActivityTaskManagerService; + private final Lazy<ActivityLaunchAnimator> mActivityLaunchAnimator; + private final Lazy<ScrimController> mScrimControllerLazy; + private final IActivityTaskManager mActivityTaskManagerService; - private FeatureFlags mFeatureFlags; private final UiEventLogger mUiEventLogger; private final SessionTracker mSessionTracker; private final CoroutineDispatcher mMainDispatcher; @@ -1361,7 +1357,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mDreamingToLockscreenTransitionViewModel; private RemoteAnimationTarget mRemoteAnimationTarget; - private Lazy<WindowManagerLockscreenVisibilityManager> mWmLockscreenVisibilityManager; + private final Lazy<WindowManagerLockscreenVisibilityManager> mWmLockscreenVisibilityManager; /** * Injected constructor. See {@link KeyguardModule}. @@ -1433,7 +1429,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mShadeController = shadeControllerLazy; dumpManager.registerDumpable(this); mDeviceConfig = deviceConfig; - mScreenOnCoordinator = screenOnCoordinator; mKeyguardTransitions = keyguardTransitions; mNotificationShadeWindowControllerLazy = notificationShadeWindowControllerLazy; mShowHomeOverLockscreen = mDeviceConfig.getBoolean( @@ -1445,9 +1440,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mHandler::post, mOnPropertiesChangedListener); mInGestureNavigationMode = - QuickStepContract.isGesturalMode(navigationModeController.addListener(mode -> { - mInGestureNavigationMode = QuickStepContract.isGesturalMode(mode); - })); + QuickStepContract.isGesturalMode(navigationModeController.addListener(mode -> + mInGestureNavigationMode = QuickStepContract.isGesturalMode(mode))); mDozeParameters = dozeParameters; mSelectedUserInteractor = selectedUserInteractor; mKeyguardInteractor = keyguardInteractor; @@ -1474,7 +1468,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mDreamOpenAnimationDuration = (int) DREAMING_ANIMATION_DURATION_MS; - mFeatureFlags = featureFlags; mUiEventLogger = uiEventLogger; mSessionTracker = sessionTracker; @@ -1578,14 +1571,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, this::setWallpaperSupportsAmbientMode); } - // TODO(b/273443374) remove, temporary util to get a feature flag - private WallpaperManager getWallpaperManager() { - if (mWallpaperManager == null) { - mWallpaperManager = mContext.getSystemService(WallpaperManager.class); - } - return mWallpaperManager; - } - @Override public void start() { synchronized (this) { @@ -1611,11 +1596,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, ViewRootImpl viewRootImpl = mKeyguardViewControllerLazy.get().getViewRootImpl(); if (viewRootImpl != null) { - collectFlow(viewRootImpl.getView(), - mDreamingToLockscreenTransitionViewModel.get().getDreamOverlayAlpha(), + DreamingToLockscreenTransitionViewModel viewModel = + mDreamingToLockscreenTransitionViewModel.get(); + collectFlow(viewRootImpl.getView(), viewModel.getDreamOverlayAlpha(), getRemoteSurfaceAlphaApplier(), mMainDispatcher); - collectFlow(viewRootImpl.getView(), - mDreamingToLockscreenTransitionViewModel.get().getTransitionEnded(), + collectFlow(viewRootImpl.getView(), viewModel.getTransitionEnded(), getFinishedCallbackConsumer(), mMainDispatcher); } } @@ -2304,6 +2289,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, showKeyguard(options); } + @SuppressLint("MissingPermission") private void lockProfile(int userId) { mTrustManager.setDeviceLockedForUser(userId, true); } @@ -2497,13 +2483,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, }; /** - * This handler will be associated with the policy thread, which will also - * be the UI thread of the keyguard. Since the apis of the policy, and therefore - * this class, can be called by other threads, any action that directly - * interacts with the keyguard ui should be posted to this handler, rather - * than called directly. + * This handler will be associated with the policy thread, which will also be the UI thread of + * the keyguard. Since the apis of the policy, and therefore this class, can be called by other + * threads, any action that directly interacts with the keyguard ui should be posted to this + * handler, rather than called directly. */ - private Handler mHandler = new Handler(Looper.myLooper(), null, true /*async*/) { + private final Handler mHandler = new Handler(Looper.myLooper(), null, true /*async*/) { @Override public void handleMessage(Message msg) { String message = ""; @@ -2766,7 +2751,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, try { mActivityTaskManagerService.setLockScreenShown(showing, aodShowing); - } catch (RemoteException e) { + } catch (RemoteException ignored) { } }); } @@ -2790,9 +2775,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (!mSystemReady) { if (DEBUG) Log.d(TAG, "ignoring handleShow because system is not ready."); return; - } else { - if (DEBUG) Log.d(TAG, "handleShow"); } + if (DEBUG) Log.d(TAG, "handleShow"); mKeyguardExitAnimationRunner = null; mWakeAndUnlocking = false; @@ -2851,6 +2835,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } private final Runnable mKeyguardGoingAwayRunnable = new Runnable() { + @SuppressLint("MissingPermission") @Override public void run() { Trace.beginSection("KeyguardViewMediator.mKeyGuardGoingAwayRunnable"); @@ -2925,24 +2910,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, return; } - final String reasonDescription; - - switch(reason) { - case WakeAndUnlockUpdateReason.FULFILL: - reasonDescription = "fulfilling existing request"; - break; - case WakeAndUnlockUpdateReason.HIDE: - reasonDescription = "hiding keyguard"; - break; - case WakeAndUnlockUpdateReason.SHOW: - reasonDescription = "showing keyguard"; - break; - case WakeAndUnlockUpdateReason.WAKE_AND_UNLOCK: - reasonDescription = "waking to unlock"; - break; - default: - throw new IllegalStateException("Unexpected value: " + reason); - } + final String reasonDescription = switch (reason) { + case WakeAndUnlockUpdateReason.FULFILL -> "fulfilling existing request"; + case WakeAndUnlockUpdateReason.HIDE -> "hiding keyguard"; + case WakeAndUnlockUpdateReason.SHOW -> "showing keyguard"; + case WakeAndUnlockUpdateReason.WAKE_AND_UNLOCK -> "waking to unlock"; + default -> throw new IllegalStateException("Unexpected value: " + reason); + }; final boolean unsetUnfulfilled = !updatedValue && reason != WakeAndUnlockUpdateReason.FULFILL; @@ -3057,7 +3031,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, IRemoteAnimationFinishedCallback callback = new IRemoteAnimationFinishedCallback() { @Override - public void onAnimationFinished() throws RemoteException { + public void onAnimationFinished() { if (!KeyguardWmStateRefactor.isEnabled()) { try { finishedCallback.onAnimationFinished(); @@ -3542,11 +3516,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, /** * Registers the CentralSurfaces to which the Keyguard View is mounted. * - * @param centralSurfaces - * @param panelView - * @param biometricUnlockController - * @param notificationContainer - * @param bypassController * @return the View Controller for the Keyguard View this class is mediating. */ public KeyguardViewController registerCentralSurfaces(CentralSurfaces centralSurfaces, @@ -3773,9 +3742,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } }); updateInputRestrictedLocked(); - mUiBgExecutor.execute(() -> { - mTrustManager.reportKeyguardShowingChanged(); - }); + mUiBgExecutor.execute(mTrustManager::reportKeyguardShowingChanged); } private void notifyTrustedChangedLocked(boolean trusted) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index e16f8dcbb00e..70da3e7ad1fa 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -90,10 +90,12 @@ import dagger.multibindings.IntoMap; import java.util.concurrent.Executor; import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.ExperimentalCoroutinesApi; /** * Dagger Module providing keyguard. */ +@ExperimentalCoroutinesApi @Module(subcomponents = { KeyguardQsUserSwitchComponent.class, KeyguardStatusBarViewComponent.class, @@ -115,7 +117,7 @@ public interface KeyguardModule { */ @Provides @SysUISingleton - public static KeyguardViewMediator newKeyguardViewMediator( + static KeyguardViewMediator newKeyguardViewMediator( Context context, UiEventLogger uiEventLogger, SessionTracker sessionTracker, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt index 9a13558d3327..b152eea63028 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt @@ -22,6 +22,7 @@ import android.hardware.biometrics.BiometricSourceType import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton @@ -174,6 +175,8 @@ constructor( mainDispatcher ) // keyguardUpdateMonitor requires registration on main thread. + // TODO(b/322555228) Remove after consolidating device entry auth messages with BP auth messages + // in BiometricStatusRepository override val authenticationStatus: Flow<FingerprintAuthenticationStatus> get() = conflatedCallbackFlow { val callback = @@ -236,7 +239,8 @@ constructor( sendUpdateIfFingerprint( biometricSourceType, AcquiredFingerprintAuthenticationStatus( - acquireInfo, + AuthenticationReason.DeviceEntryAuthentication, + acquireInfo ), ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt index b1a2297526ce..e017129bd5c5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt @@ -94,6 +94,7 @@ constructor( context.resources.getBoolean(R.bool.config_show_sidefps_hint_on_bouncer) val sfpsDetectionRunning = keyguardUpdateMonitor.isFingerprintDetectionRunning val isUnlockingWithFpAllowed = keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed + return primaryBouncerInteractor.isBouncerShowing() && sfpsEnabled && sfpsDetectionRunning && diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt new file mode 100644 index 000000000000..7f0b483919b3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt @@ -0,0 +1,54 @@ +/* + * 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.shared + +import com.android.systemui.Flags +import com.android.systemui.compose.ComposeFacade +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the compose lockscreen flag state. */ +@Suppress("NOTHING_TO_INLINE") +object ComposeLockscreen { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_COMPOSE_LOCKSCREEN + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.composeLockscreen() && ComposeFacade.isComposeAvailable() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt index cc385a8eea85..474de77f09ab 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt @@ -20,6 +20,7 @@ import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQ import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START import android.hardware.fingerprint.FingerprintManager import android.os.SystemClock.elapsedRealtime +import com.android.systemui.biometrics.shared.model.AuthenticationReason /** * Fingerprint authentication status provided by @@ -40,8 +41,10 @@ data class HelpFingerprintAuthenticationStatus( ) : FingerprintAuthenticationStatus() /** Fingerprint acquired message. */ -data class AcquiredFingerprintAuthenticationStatus(val acquiredInfo: Int) : - FingerprintAuthenticationStatus() { +data class AcquiredFingerprintAuthenticationStatus( + val authenticationReason: AuthenticationReason, + val acquiredInfo: Int +) : FingerprintAuthenticationStatus() { val fingerprintCaptureStarted: Boolean = acquiredInfo == FINGERPRINT_ACQUIRED_START diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 48092c6374e0..789d30ff1a31 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -191,6 +191,7 @@ object KeyguardRootViewBinder { .collect { y -> childViews[burnInLayerId]?.translationY = y childViews[largeClockId]?.translationY = y + childViews[aodNotificationIconContainerId]?.translationY = y } } @@ -200,6 +201,7 @@ object KeyguardRootViewBinder { .collect { x -> childViews[burnInLayerId]?.translationX = x childViews[largeClockId]?.translationX = x + childViews[aodNotificationIconContainerId]?.translationX = x } } @@ -219,6 +221,10 @@ object KeyguardRootViewBinder { // transition with other parts in burnInLayer childViews[burnInLayerId]?.scaleX = scaleViewModel.scale childViews[burnInLayerId]?.scaleY = scaleViewModel.scale + childViews[aodNotificationIconContainerId]?.scaleX = + scaleViewModel.scale + childViews[aodNotificationIconContainerId]?.scaleY = + scaleViewModel.scale } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt index 3d36eb03a1bc..9a1fcc1a6a51 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt @@ -41,11 +41,13 @@ constructor( return } - val nic = constraintLayout.requireViewById<View>(R.id.aod_notification_icon_container) + // The burn-in layer requires at least 1 view at all times + val emptyView = View(context, null).apply { id = View.generateViewId() } + constraintLayout.addView(emptyView) burnInLayer = AodBurnInLayer(context).apply { id = R.id.burn_in_layer - addView(nic) + addView(emptyView) if (!migrateClocksToBlueprint()) { val statusView = constraintLayout.requireViewById<View>(R.id.keyguard_status_view) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt index 310ec95a22df..ad6a36c71e39 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt @@ -27,6 +27,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.res.R import javax.inject.Inject +import kotlin.math.roundToInt import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -74,7 +75,18 @@ constructor( isTransitionToAod && isUdfps } .distinctUntilChanged() - private val padding: Flow<Int> = udfpsOverlayInteractor.iconPadding + + private val padding: Flow<Int> = + deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { udfpsSupported -> + if (udfpsSupported) { + udfpsOverlayInteractor.iconPadding + } else { + configurationInteractor.scaleForResolution.map { scale -> + (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale) + .roundToInt() + } + } + } val viewModel: Flow<ForegroundIconViewModel> = combine( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt index ca9c8571f6b9..67c42f0fe343 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt @@ -22,8 +22,10 @@ import android.graphics.Point import androidx.annotation.VisibleForTesting import androidx.core.animation.addListener import com.android.systemui.Flags +import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor +import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.isDefaultOrientation import com.android.systemui.dagger.SysUISingleton @@ -34,6 +36,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.res.R @@ -49,10 +52,12 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.launch @@ -62,7 +67,8 @@ class SideFpsProgressBarViewModel @Inject constructor( private val context: Context, - private val fpAuthRepository: DeviceEntryFingerprintAuthInteractor, + private val biometricStatusInteractor: BiometricStatusInteractor, + private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, private val sfpsSensorInteractor: SideFpsSensorInteractor, // todo (b/317432075) Injecting DozeServiceHost directly instead of using it through // DozeInteractor as DozeServiceHost already depends on DozeInteractor. @@ -86,6 +92,23 @@ constructor( private val additionalSensorLengthPadding = context.resources.getDimension(R.dimen.sfps_progress_bar_length_extra_padding).toInt() + // Merged [FingerprintAuthenticationStatus] from BiometricPrompt acquired messages and + // device entry authentication messages + private val mergedFingerprintAuthenticationStatus = + merge( + biometricStatusInteractor.fingerprintAcquiredStatus, + deviceEntryFingerprintAuthInteractor.authenticationStatus + ) + .filter { + if (it is AcquiredFingerprintAuthenticationStatus) { + it.authenticationReason == AuthenticationReason.DeviceEntryAuthentication || + it.authenticationReason == + AuthenticationReason.BiometricPromptAuthentication + } else { + true + } + } + val isVisible: Flow<Boolean> = _visible.asStateFlow() val progress: Flow<Float> = _progress.asStateFlow() @@ -147,7 +170,14 @@ constructor( viewLeftTop } - val isFingerprintAuthRunning: Flow<Boolean> = fpAuthRepository.isRunning + val isFingerprintAuthRunning: Flow<Boolean> = + combine( + deviceEntryFingerprintAuthInteractor.isRunning, + biometricStatusInteractor.sfpsAuthenticationReason + ) { deviceEntryAuthIsRunning, sfpsAuthReason -> + deviceEntryAuthIsRunning || + sfpsAuthReason == AuthenticationReason.BiometricPromptAuthentication + } val rotation: Flow<Float> = combine(displayStateInteractor.currentRotation, sfpsSensorInteractor.sensorLocation, ::Pair) @@ -185,7 +215,8 @@ constructor( sfpsSensorInteractor.authenticationDuration .flatMapLatest { authDuration -> _animator?.cancel() - fpAuthRepository.authenticationStatus.map { authStatus -> + mergedFingerprintAuthenticationStatus.map { + authStatus: FingerprintAuthenticationStatus -> when (authStatus) { is AcquiredFingerprintAuthenticationStatus -> { if (authStatus.fingerprintCaptureStarted) { diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/CommunalTableLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/CommunalTableLog.kt new file mode 100644 index 000000000000..fe7dc4bdf59f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/CommunalTableLog.kt @@ -0,0 +1,25 @@ +/* + * 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.log.dagger + +import javax.inject.Qualifier + +/** A [TableLogBuffer] for communal-related logging. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class CommunalTableLog diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 3e0094081638..ac579d6d2491 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -579,6 +579,16 @@ public class LogModule { return factory.create("CommunalLog", 250); } + /** + * Provides a {@link TableLogBuffer} for communal-related logs. + */ + @Provides + @SysUISingleton + @CommunalTableLog + public static TableLogBuffer provideCommunalTableLogBuffer(TableLogBufferFactory factory) { + return factory.create("CommunalTableLog", 250); + } + /** Provides a {@link LogBuffer} for display metrics related logs. */ @Provides @SysUISingleton @@ -618,4 +628,13 @@ public class LogModule { public static LogBuffer providePackageChangeRepoLogBuffer(LogBufferFactory factory) { return factory.create("PackageChangeRepo", 50); } + + /** Provides a {@link LogBuffer} for NavBarButtonClicks. */ + @Provides + @SysUISingleton + @NavBarButtonClickLog + public static LogBuffer provideNavBarButtonClickLogBuffer(LogBufferFactory factory) { + return factory.create("NavBarButtonClick", 50); + } + } diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NavBarButtonClickLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NavBarButtonClickLog.java new file mode 100644 index 000000000000..939dab2ecefa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NavBarButtonClickLog.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.systemui.log.dagger; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.android.systemui.log.LogBuffer; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +/** A {@link LogBuffer} for {@link com.android.systemui.navigationbar.NavigationBar}. */ +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface NavBarButtonClickLog { +} diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarButtonClickLogger.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarButtonClickLogger.kt new file mode 100644 index 000000000000..408acf3ce1a1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarButtonClickLogger.kt @@ -0,0 +1,48 @@ +/* + * 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.navigationbar + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.dagger.NavBarButtonClickLog +import javax.inject.Inject + +class NavBarButtonClickLogger +@Inject +constructor(@NavBarButtonClickLog private val buffer: LogBuffer) { + fun logHomeButtonClick() { + buffer.log(TAG, LogLevel.DEBUG, {}, { "Home Button Triggered" }) + } + + fun logBackButtonClick() { + buffer.log(TAG, LogLevel.DEBUG, {}, { "Back Button Triggered" }) + } + + fun logRecentsButtonClick() { + buffer.log(TAG, LogLevel.DEBUG, {}, { "Recents Button Triggered" }) + } + + fun logImeSwitcherClick() { + buffer.log(TAG, LogLevel.DEBUG, {}, { "Ime Switcher Triggered" }) + } + + fun logAccessibilityButtonClick() { + buffer.log(TAG, LogLevel.DEBUG, {}, { "Accessibility Button Triggered" }) + } +} + +private const val TAG = "NavBarButtonClick" diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 068e5fd61ea4..95b75ac7a875 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -84,11 +84,7 @@ import android.view.InsetsFrameProvider; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; -import android.view.SurfaceControl; -import android.view.SurfaceControl.Transaction; import android.view.View; -import android.view.ViewRootImpl; -import android.view.ViewRootImpl.SurfaceChangedCallback; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.InternalInsetsInfo; import android.view.ViewTreeObserver.OnComputeInternalInsetsListener; @@ -285,6 +281,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private boolean mImeVisible; private final Rect mSamplingBounds = new Rect(); private final Binder mInsetsSourceOwner = new Binder(); + private final NavBarButtonClickLogger mNavBarButtonClickLogger; /** * When quickswitching between apps of different orientations, we draw a secondary home handle @@ -559,7 +556,8 @@ public class NavigationBar extends ViewController<NavigationBarView> implements UserContextProvider userContextProvider, WakefulnessLifecycle wakefulnessLifecycle, TaskStackChangeListeners taskStackChangeListeners, - DisplayTracker displayTracker) { + DisplayTracker displayTracker, + NavBarButtonClickLogger navBarButtonClickLogger) { super(navigationBarView); mFrame = navigationBarFrame; mContext = context; @@ -601,6 +599,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mTaskStackChangeListeners = taskStackChangeListeners; mDisplayTracker = displayTracker; mEdgeBackGestureHandler = navBarHelper.getEdgeBackGestureHandler(); + mNavBarButtonClickLogger = navBarButtonClickLogger; mNavColorSampleMargin = getResources() .getDimensionPixelSize(R.dimen.navigation_handle_sample_horizontal_margin); @@ -1276,6 +1275,10 @@ public class NavigationBar extends ViewController<NavigationBarView> implements ButtonDispatcher homeButton = mView.getHomeButton(); homeButton.setOnTouchListener(this::onHomeTouch); + homeButton.setNavBarButtonClickLogger(mNavBarButtonClickLogger); + + ButtonDispatcher backButton = mView.getBackButton(); + backButton.setNavBarButtonClickLogger(mNavBarButtonClickLogger); reconfigureHomeLongClick(); @@ -1388,6 +1391,8 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } private void onRecentsClick(View v) { + mNavBarButtonClickLogger.logRecentsButtonClick(); + if (LatencyTracker.isEnabled(mContext)) { LatencyTracker.getInstance(mContext).onActionStart( LatencyTracker.ACTION_TOGGLE_RECENTS); @@ -1397,6 +1402,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } private void onImeSwitcherClick(View v) { + mNavBarButtonClickLogger.logImeSwitcherClick(); mInputMethodManager.showInputMethodPickerFromSystem( true /* showAuxiliarySubtypes */, mDisplayId); mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP); @@ -1486,6 +1492,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } private void onAccessibilityClick(View v) { + mNavBarButtonClickLogger.logAccessibilityButtonClick(); final Display display = v.getDisplay(); mAccessibilityManager.notifyAccessibilityButtonClicked( display != null ? display.getDisplayId() : mDisplayTracker.getDefaultDisplayId()); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ButtonDispatcher.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ButtonDispatcher.java index 5739abcae7eb..fc37b9f0979d 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ButtonDispatcher.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ButtonDispatcher.java @@ -23,6 +23,9 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.view.View; import android.view.View.AccessibilityDelegate; +import android.view.ViewGroup; + +import com.android.systemui.navigationbar.NavBarButtonClickLogger; import java.util.ArrayList; @@ -52,6 +55,7 @@ public class ButtonDispatcher { private boolean mVertical; private ValueAnimator mFadeAnimator; private AccessibilityDelegate mAccessibilityDelegate; + private NavBarButtonClickLogger mNavBarButtonClickLogger; private final ValueAnimator.AnimatorUpdateListener mAlphaListener = animation -> setAlpha( @@ -341,4 +345,36 @@ public class ButtonDispatcher { */ public void onDestroy() { } + + /** + * Sets the NavBarButtonClickLogger for all the KeyButtonViews respectively. + */ + public void setNavBarButtonClickLogger(NavBarButtonClickLogger navBarButtonClickLogger) { + if (navBarButtonClickLogger != null) { + mNavBarButtonClickLogger = navBarButtonClickLogger; + final int size = mViews.size(); + for (int i = 0; i < size; i++) { + final View v = mViews.get(i); + setNavBarButtonClickLoggerForViewChildren(v); + } + } + } + + /** + * Recursively explores view hierarchy until the children of provided view are of type + * KeyButtonView, so the NavBarButtonClickLogger can be set on them. + */ + private void setNavBarButtonClickLoggerForViewChildren(View v) { + if (v instanceof KeyButtonView) { + ((KeyButtonView) v).setNavBarButtonClickLogger(mNavBarButtonClickLogger); + return; + } + + if (v instanceof ViewGroup viewGroup) { + final int childrenCount = viewGroup.getChildCount(); + for (int i = 0; i < childrenCount; i++) { + setNavBarButtonClickLoggerForViewChildren(viewGroup.getChildAt(i)); + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java index df6843d31ab1..dbe87eafef7b 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java @@ -59,6 +59,7 @@ import com.android.internal.logging.UiEventLoggerImpl; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.Dependency; import com.android.systemui.assist.AssistManager; +import com.android.systemui.navigationbar.NavBarButtonClickLogger; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.res.R; import com.android.systemui.shared.navigationbar.KeyButtonRipple; @@ -86,6 +87,7 @@ public class KeyButtonView extends ImageView implements ButtonInterface { private final Paint mOvalBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); private float mDarkIntensity; private boolean mHasOvalBg = false; + private NavBarButtonClickLogger mNavBarButtonClickLogger; @VisibleForTesting public enum NavBarButtonEvent implements UiEventLogger.UiEventEnum { @@ -197,6 +199,10 @@ public class KeyButtonView extends ImageView implements ButtonInterface { mOnClickListener = onClickListener; } + public void setNavBarButtonClickLogger(NavBarButtonClickLogger navBarButtonClickLogger) { + mNavBarButtonClickLogger = navBarButtonClickLogger; + } + public void loadAsync(Icon icon) { new AsyncTask<Icon, Void, Drawable>() { @Override @@ -389,11 +395,19 @@ public class KeyButtonView extends ImageView implements ButtonInterface { uiEvent = longPressSet ? NavBarButtonEvent.NAVBAR_BACK_BUTTON_LONGPRESS : NavBarButtonEvent.NAVBAR_BACK_BUTTON_TAP; + + if (mNavBarButtonClickLogger != null) { + mNavBarButtonClickLogger.logBackButtonClick(); + } break; case KeyEvent.KEYCODE_HOME: uiEvent = longPressSet ? NavBarButtonEvent.NAVBAR_HOME_BUTTON_LONGPRESS : NavBarButtonEvent.NAVBAR_HOME_BUTTON_TAP; + + if (mNavBarButtonClickLogger != null) { + mNavBarButtonClickLogger.logHomeButtonClick(); + } break; case KeyEvent.KEYCODE_APP_SWITCH: uiEvent = longPressSet diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt new file mode 100644 index 000000000000..26069c774364 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt @@ -0,0 +1,54 @@ +/* + * 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.tiles.impl.fontscaling.domain + +import android.content.res.Resources +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper +import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.res.R +import javax.inject.Inject + +/** Maps [FontScalingTileModel] to [QSTileState]. */ +class FontScalingTileMapper +@Inject +constructor( + @Main private val resources: Resources, + private val theme: Resources.Theme, +) : QSTileDataToStateMapper<FontScalingTileModel> { + + override fun map(config: QSTileConfig, data: FontScalingTileModel): QSTileState = + QSTileState.build(resources, theme, config.uiConfig) { + val icon = + Icon.Loaded( + resources.getDrawable( + R.drawable.ic_qs_font_scaling, + theme, + ), + contentDescription = null + ) + this.icon = { icon } + contentDescription = label + activationState = QSTileState.ActivationState.ACTIVE + sideViewIcon = QSTileState.SideViewIcon.Chevron + supportedActions = + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractor.kt new file mode 100644 index 000000000000..745e6a301689 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractor.kt @@ -0,0 +1,36 @@ +/* + * 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.tiles.impl.fontscaling.domain.interactor + +import android.os.UserHandle +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger +import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor +import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +/** Provides [FontScalingTileModel]. */ +class FontScalingTileDataInteractor @Inject constructor() : + QSTileDataInteractor<FontScalingTileModel> { + override fun tileData( + user: UserHandle, + triggers: Flow<DataUpdateTrigger> + ): Flow<FontScalingTileModel> = flowOf(FontScalingTileModel) + + override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt new file mode 100644 index 000000000000..b6f4afb84259 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt @@ -0,0 +1,96 @@ +/* + * 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.tiles.impl.fontscaling.domain.interactor + +import android.content.Intent +import android.provider.Settings +import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.accessibility.fontscaling.FontScalingDialogDelegate +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler +import com.android.systemui.qs.tiles.base.interactor.QSTileInput +import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.policy.KeyguardStateController +import javax.inject.Inject +import javax.inject.Provider +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.withContext + +/** Handles font scaling tile clicks. */ +class FontScalingTileUserActionInteractor +@Inject +constructor( + @Main private val coroutineContext: CoroutineContext, + private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, + private val fontScalingDialogDelegateProvider: Provider<FontScalingDialogDelegate>, + private val keyguardStateController: KeyguardStateController, + private val dialogLaunchAnimator: DialogLaunchAnimator, + private val activityStarter: ActivityStarter, +) : QSTileUserActionInteractor<FontScalingTileModel> { + + override suspend fun handleInput(input: QSTileInput<FontScalingTileModel>): Unit = + with(input) { + when (action) { + is QSTileUserAction.Click -> { + // We animate from the touched view only if we are not on the keyguard + val animateFromView: Boolean = + action.view != null && !keyguardStateController.isShowing + val runnable = Runnable { + val dialog: SystemUIDialog = + fontScalingDialogDelegateProvider.get().createDialog() + if (animateFromView) { + dialogLaunchAnimator.showFromView( + dialog, + action.view!!, + DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG + ) + ) + } else { + dialog.show() + } + } + + withContext(coroutineContext) { + activityStarter.executeRunnableDismissingKeyguard( + runnable, + /* cancelAction= */ null, + /* dismissShade= */ true, + /* afterKeyguardGone= */ true, + /* deferred= */ false + ) + } + } + is QSTileUserAction.LongClick -> { + qsTileIntentUserActionHandler.handle( + action.view, + Intent(Settings.ACTION_TEXT_READING_SETTINGS) + ) + } + } + } + companion object { + private const val INTERACTION_JANK_TAG = "font_scaling" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/model/FontScalingTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/model/FontScalingTileModel.kt new file mode 100644 index 000000000000..76042dffa47e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/model/FontScalingTileModel.kt @@ -0,0 +1,20 @@ +/* + * 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.tiles.impl.fontscaling.domain.model + +/** FontScaling tile model. No data needed as the tile just opens a dialog. */ +data object FontScalingTileModel diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 246ccb1180b0..56c0ca910bd7 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -42,6 +42,7 @@ import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled +import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor import com.android.systemui.util.asIndenting import com.android.systemui.util.printSection import com.android.systemui.util.println @@ -51,10 +52,12 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch @@ -81,6 +84,7 @@ constructor( private val simBouncerInteractor: Lazy<SimBouncerInteractor>, private val authenticationInteractor: Lazy<AuthenticationInteractor>, private val windowController: NotificationShadeWindowController, + private val deviceProvisioningInteractor: DeviceProvisioningInteractor, ) : CoreStartable { override fun start() { @@ -112,26 +116,39 @@ constructor( private fun hydrateVisibility() { applicationScope.launch { // TODO(b/296114544): Combine with some global hun state to make it visible! - sceneInteractor.transitionState - .mapNotNull { state -> - when (state) { - is ObservableTransitionState.Idle -> { - if (state.scene != SceneKey.Gone) { - true to "scene is not Gone" - } else { - false to "scene is Gone" - } - } - is ObservableTransitionState.Transition -> { - if (state.fromScene == SceneKey.Gone) { - true to "scene transitioning away from Gone" - } else { - null + combine( + deviceProvisioningInteractor.isDeviceProvisioned, + deviceProvisioningInteractor.isFactoryResetProtectionActive, + ) { isDeviceProvisioned, isFrpActive -> + isDeviceProvisioned && !isFrpActive + } + .distinctUntilChanged() + .flatMapLatest { isAllowedToBeVisible -> + if (isAllowedToBeVisible) { + sceneInteractor.transitionState + .mapNotNull { state -> + when (state) { + is ObservableTransitionState.Idle -> { + if (state.scene != SceneKey.Gone) { + true to "scene is not Gone" + } else { + false to "scene is Gone" + } + } + is ObservableTransitionState.Transition -> { + if (state.fromScene == SceneKey.Gone) { + true to "scene transitioning away from Gone" + } else { + null + } + } + } } - } + .distinctUntilChanged() + } else { + flowOf(false to "Device not provisioned or Factory Reset Protection active") } } - .distinctUntilChanged() .collect { (isVisible, loggingReason) -> sceneInteractor.setVisible(isVisible, loggingReason) } diff --git a/packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java b/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java index a0dd924b3959..fd807dbec581 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java +++ b/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.settings.dagger; +package com.android.systemui.settings; import android.app.ActivityManager; import android.app.IActivityManager; @@ -29,14 +29,6 @@ import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlagsClassic; -import com.android.systemui.settings.DisplayTracker; -import com.android.systemui.settings.DisplayTrackerImpl; -import com.android.systemui.settings.UserContentResolverProvider; -import com.android.systemui.settings.UserContextProvider; -import com.android.systemui.settings.UserFileManager; -import com.android.systemui.settings.UserFileManagerImpl; -import com.android.systemui.settings.UserTracker; -import com.android.systemui.settings.UserTrackerImpl; import dagger.Binds; import dagger.Module; diff --git a/packages/SystemUI/src/com/android/systemui/settings/SecureSettingsRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/settings/SecureSettingsRepositoryModule.kt new file mode 100644 index 000000000000..76d1d3dd145e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/SecureSettingsRepositoryModule.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.settings + +import android.content.ContentResolver +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository +import com.android.systemui.shared.settings.data.repository.SecureSettingsRepositoryImpl +import dagger.Module +import dagger.Provides +import kotlinx.coroutines.CoroutineDispatcher + +@Module +object SecureSettingsRepositoryModule { + @JvmStatic + @Provides + @SysUISingleton + fun provideSecureSettingsRepository( + contentResolver: ContentResolver, + @Background backgroundDispatcher: CoroutineDispatcher, + ): SecureSettingsRepository = + SecureSettingsRepositoryImpl(contentResolver, backgroundDispatcher) +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 00534743588c..f7fed537a167 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -183,7 +183,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mBackgroundExecutor = backgroundExecutor; mColorExtractor = colorExtractor; mScreenOffAnimationController = screenOffAnimationController; - dumpManager.registerDumpable(this); + // prefix with {slow} to make sure this dumps at the END of the critical section. + dumpManager.registerCriticalDumpable("{slow}NotificationShadeWindowControllerImpl", this); mAuthController = authController; mUserInteractor = userInteractor; mSceneContainerFlags = sceneContainerFlags; @@ -331,8 +332,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW ); collectFlow( mWindowRootView, - mCommunalInteractor.get().isCommunalShowing(), - this::onCommunalShowingChanged + mCommunalInteractor.get().isCommunalVisible(), + this::onCommunalVisibleChanged ); } @@ -475,6 +476,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } visible = true; mLogger.d("Visibility forced to be true"); + } else if (state.communalVisible) { + visible = true; + mLogger.d("Visibility forced to be true by communal"); } if (mWindowRootView != null) { if (visible) { @@ -510,15 +514,15 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } private void applyUserActivityTimeout(NotificationShadeWindowState state) { - final Boolean communalShowing = state.isCommunalShowingAndNotOccluded(); + final Boolean communalVisible = state.isCommunalVisibleAndNotOccluded(); final Boolean keyguardShowing = state.isKeyguardShowingAndNotOccluded(); long timeout = -1; - if ((communalShowing || keyguardShowing) + if ((communalVisible || keyguardShowing) && state.statusBarState == StatusBarState.KEYGUARD && !state.qsExpanded) { if (state.bouncerShowing) { timeout = KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS; - } else if (communalShowing) { + } else if (communalVisible) { timeout = CommunalInteractor.AWAKE_INTERVAL_MS; } else if (keyguardShowing) { timeout = mLockScreenDisplayTimeout; @@ -624,7 +628,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW state.dozing, state.scrimsVisibility, state.backgroundBlurRadius, - state.communalShowing + state.communalVisible ); } @@ -749,8 +753,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } @VisibleForTesting - void onCommunalShowingChanged(Boolean showing) { - mCurrentState.communalShowing = showing; + void onCommunalVisibleChanged(Boolean visible) { + mCurrentState.communalVisible = visible; apply(mCurrentState); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt index f9c9d83e03aa..e0a98b3b7f9e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt @@ -58,15 +58,15 @@ class NotificationShadeWindowState( @JvmField var dreaming: Boolean = false, @JvmField var scrimsVisibility: Int = 0, @JvmField var backgroundBlurRadius: Int = 0, - @JvmField var communalShowing: Boolean = false, + @JvmField var communalVisible: Boolean = false, ) { fun isKeyguardShowingAndNotOccluded(): Boolean { return keyguardShowing && !keyguardOccluded } - fun isCommunalShowingAndNotOccluded(): Boolean { - return communalShowing && !keyguardOccluded + fun isCommunalVisibleAndNotOccluded(): Boolean { + return communalVisible && !keyguardOccluded } /** List of [String] to be used as a [Row] with [DumpsysTableLogger]. */ @@ -99,7 +99,7 @@ class NotificationShadeWindowState( dozing.toString(), scrimsVisibility.toString(), backgroundBlurRadius.toString(), - communalShowing.toString(), + communalVisible.toString(), ) } @@ -140,7 +140,7 @@ class NotificationShadeWindowState( dozing: Boolean, scrimsVisibility: Int, backgroundBlurRadius: Int, - communalShowing: Boolean, + communalVisible: Boolean, ) { buffer.advance().apply { this.keyguardShowing = keyguardShowing @@ -172,7 +172,7 @@ class NotificationShadeWindowState( this.dozing = dozing this.scrimsVisibility = scrimsVisibility this.backgroundBlurRadius = backgroundBlurRadius - this.communalShowing = communalShowing + this.communalVisible = communalVisible } } @@ -218,7 +218,7 @@ class NotificationShadeWindowState( "dozing", "scrimsVisibility", "backgroundBlurRadius", - "communalShowing" + "communalVisible" ) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index 84cad1d16d73..c0afa32571e7 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -30,8 +30,6 @@ import androidx.lifecycle.lifecycleScope import com.android.systemui.Flags.centralizedStatusBarDimensRefactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.fragments.FragmentService import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.lifecycle.repeatWhenAttached @@ -59,18 +57,17 @@ internal const val INSET_DEBOUNCE_MILLIS = 500L @SysUISingleton class NotificationsQSContainerController @Inject constructor( - view: NotificationsQuickSettingsContainer, - private val navigationModeController: NavigationModeController, - private val overviewProxyService: OverviewProxyService, - private val shadeHeaderController: ShadeHeaderController, - private val shadeInteractor: ShadeInteractor, - private val fragmentService: FragmentService, - @Main private val delayableExecutor: DelayableExecutor, - private val featureFlags: FeatureFlags, - private val - notificationStackScrollLayoutController: NotificationStackScrollLayoutController, - private val splitShadeStateController: SplitShadeStateController, - private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>, + view: NotificationsQuickSettingsContainer, + private val navigationModeController: NavigationModeController, + private val overviewProxyService: OverviewProxyService, + private val shadeHeaderController: ShadeHeaderController, + private val shadeInteractor: ShadeInteractor, + private val fragmentService: FragmentService, + @Main private val delayableExecutor: DelayableExecutor, + private val + notificationStackScrollLayoutController: NotificationStackScrollLayoutController, + private val splitShadeStateController: SplitShadeStateController, + private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>, ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController { private var splitShadeEnabled = false @@ -133,9 +130,6 @@ class NotificationsQSContainerController @Inject constructor( isGestureNavigation = QuickStepContract.isGesturalMode(currentMode) mView.setStackScroller(notificationStackScrollLayoutController.getView()) - if (featureFlags.isEnabled(Flags.QS_CONTAINER_GRAPH_OPTIMIZER)){ - mView.enableGraphOptimization() - } } public override fun onViewAttached() { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java index de3d16a57a1f..25e558ee42dd 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java @@ -32,10 +32,10 @@ import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; -import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl; -import com.android.systemui.res.R; import com.android.systemui.fragments.FragmentHostManager.FragmentListener; +import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl; import com.android.systemui.plugins.qs.QS; +import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.AboveShelfObserver; import java.util.ArrayList; @@ -73,6 +73,7 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout public NotificationsQuickSettingsContainer(Context context, AttributeSet attrs) { super(context, attrs); + setOptimizationLevel(getOptimizationLevel() | OPTIMIZATION_GRAPH); } @Override @@ -180,10 +181,6 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout super.dispatchDraw(canvas); } - void enableGraphOptimization() { - setOptimizationLevel(getOptimizationLevel() | OPTIMIZATION_GRAPH); - } - @Override public boolean dispatchTouchEvent(MotionEvent ev) { return TouchLogger.logDispatchTouch("NotificationsQuickSettingsContainer", ev, diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index 10b9db0a349b..4e8b4039cc79 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -20,6 +20,7 @@ import android.view.MotionEvent import com.android.systemui.assist.AssistManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.log.LogBuffer import com.android.systemui.log.dagger.ShadeTouchLog @@ -34,11 +35,13 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Implementation of ShadeController backed by scenes instead of NPVC. @@ -50,6 +53,7 @@ import kotlinx.coroutines.launch class ShadeControllerSceneImpl @Inject constructor( + @Main private val mainDispatcher: CoroutineDispatcher, @Background private val scope: CoroutineScope, private val shadeInteractor: ShadeInteractor, private val sceneInteractor: SceneInteractor, @@ -193,7 +197,11 @@ constructor( } override fun setVisibilityListener(listener: ShadeVisibilityListener) { - scope.launch { sceneInteractor.isVisible.collect { listener.expandedVisibleChanged(it) } } + scope.launch { + sceneInteractor.isVisible.collect { isVisible -> + withContext(mainDispatcher) { listener.expandedVisibleChanged(isVisible) } + } + } } @ExperimentalCoroutinesApi diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt index a71cf950cbe1..e619806abc34 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt @@ -25,9 +25,10 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository import com.android.systemui.statusbar.phone.DozeParameters -import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository +import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor import com.android.systemui.user.domain.interactor.UserSwitcherInteractor +import com.android.systemui.util.kotlin.combine import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -44,7 +45,7 @@ class ShadeInteractorImpl @Inject constructor( @Application val scope: CoroutineScope, - deviceProvisioningRepository: DeviceProvisioningRepository, + deviceProvisioningInteractor: DeviceProvisioningInteractor, disableFlagsRepository: DisableFlagsRepository, dozeParams: DozeParameters, keyguardRepository: KeyguardRepository, @@ -56,7 +57,7 @@ constructor( ) : ShadeInteractor, BaseShadeInteractor by baseShadeInteractor { override val isShadeEnabled: StateFlow<Boolean> = combine( - deviceProvisioningRepository.isFactoryResetProtectionActive, + deviceProvisioningInteractor.isFactoryResetProtectionActive, disableFlagsRepository.disableFlags, ) { isFrpActive, isDisabledByFlags -> isDisabledByFlags.isShadeEnabled() && !isFrpActive @@ -83,7 +84,7 @@ constructor( powerInteractor.isAsleep, keyguardTransitionInteractor.isInTransitionToStateWhere { it == KeyguardState.AOD }, keyguardRepository.dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING }, - deviceProvisioningRepository.isFactoryResetProtectionActive, + deviceProvisioningInteractor.isFactoryResetProtectionActive, ) { isAsleep, goingToSleep, isPulsing, isFrpActive -> when { // Touches are disabled when Factory Reset Protection is active @@ -103,7 +104,7 @@ constructor( isShadeEnabled, keyguardRepository.isDozing, userSetupRepository.isUserSetUp, - deviceProvisioningRepository.isDeviceProvisioned, + deviceProvisioningInteractor.isDeviceProvisioned, ) { disableFlags, isShadeEnabled, isDozing, isUserSetup, isDeviceProvisioned -> isDeviceProvisioned && // Disallow QS during setup if it's a simple user switcher. (The user intends to diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index 7f8be1cc7e55..ef5026538216 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -14,6 +14,7 @@ import androidx.annotation.FloatRange import androidx.annotation.VisibleForTesting import com.android.systemui.Dumpable import com.android.systemui.ExpandHelper +import com.android.systemui.Flags.nsslFalsingFix import com.android.systemui.Gefingerpoken import com.android.systemui.biometrics.UdfpsKeyguardViewControllerLegacy import com.android.systemui.classifier.Classifier @@ -889,7 +890,7 @@ class DragDownHelper( isDraggingDown = false isTrackpadReverseScroll = false shadeRepository.setLegacyLockscreenShadeTracking(false) - if (KeyguardShadeMigrationNssl.isEnabled) { + if (nsslFalsingFix() || KeyguardShadeMigrationNssl.isEnabled) { return true } } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java index 32cd56ca223f..b64e0b7d3187 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java @@ -56,7 +56,6 @@ import com.android.systemui.statusbar.phone.ManagedProfileControllerImpl; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl; import com.android.systemui.statusbar.phone.StatusBarIconList; -import com.android.systemui.statusbar.phone.StatusBarNotificationPresenterModule; import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -73,7 +72,7 @@ import dagger.multibindings.IntoMap; * their own version of CentralSurfaces can include just dependencies, without injecting * CentralSurfaces itself. */ -@Module(includes = {StatusBarNotificationPresenterModule.class}) +@Module public interface CentralSurfacesDependenciesModule { /** */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java index 99d4b2e525d1..27536bce97f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java @@ -18,12 +18,14 @@ package com.android.systemui.statusbar.dagger; import com.android.systemui.statusbar.notification.dagger.NotificationsModule; import com.android.systemui.statusbar.notification.row.NotificationRowModule; +import com.android.systemui.statusbar.phone.StatusBarNotificationPresenterModule; import com.android.systemui.statusbar.phone.dagger.StatusBarPhoneModule; import dagger.Module; -/** */ -@Module(includes = {StatusBarPhoneModule.class, CentralSurfacesDependenciesModule.class, +/** */ +@Module(includes = {CentralSurfacesDependenciesModule.class, + StatusBarNotificationPresenterModule.class, StatusBarPhoneModule.class, NotificationsModule.class, NotificationRowModule.class}) public interface CentralSurfacesModule { } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt index ae4ba27775b8..29627e14e4a0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt @@ -18,7 +18,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.os.UserHandle import com.android.keyguard.KeyguardUpdateMonitor -import com.android.systemui.Flags.screenshareNotificationHiding +import com.android.server.notification.Flags.screenshareNotificationHiding import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.StatusBarState diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java index cd816aea452b..954e80505cbe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.notification.collection.inflation; -import static com.android.systemui.Flags.screenshareNotificationHiding; +import static com.android.server.notification.Flags.screenshareNotificationHiding; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index 6bba72b2cd49..92b0c048f3fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -51,6 +51,7 @@ import com.android.systemui.statusbar.notification.collection.render.NotifGutsVi import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.data.NotificationDataLayerModule; +import com.android.systemui.statusbar.notification.domain.NotificationDomainLayerModule; import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor; import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModelModule; import com.android.systemui.statusbar.notification.icon.ConversationIconManager; @@ -78,14 +79,14 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarter; import com.android.systemui.statusbar.policy.HeadsUpManager; -import javax.inject.Provider; - import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.ClassKey; import dagger.multibindings.IntoMap; +import javax.inject.Provider; + /** * Dagger Module for classes found within the com.android.systemui.statusbar.notification package. */ @@ -94,6 +95,7 @@ import dagger.multibindings.IntoMap; FooterViewModelModule.class, KeyguardNotificationVisibilityProviderModule.class, NotificationDataLayerModule.class, + NotificationDomainLayerModule.class, NotifPipelineChoreographerModule.class, NotificationSectionHeadersModule.class, ActivatableNotificationViewModelModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt index 2cac0002f013..b187cf15cccd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt @@ -17,4 +17,10 @@ package com.android.systemui.statusbar.notification.data import dagger.Module -@Module(includes = []) interface NotificationDataLayerModule +@Module( + includes = + [ + NotificationSettingsRepositoryModule::class, + ] +) +interface NotificationDataLayerModule diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt new file mode 100644 index 000000000000..a7970c70e4ed --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.data + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.settings.SecureSettingsRepositoryModule +import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository +import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository +import dagger.Module +import dagger.Provides +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope + +@Module(includes = [SecureSettingsRepositoryModule::class]) +object NotificationSettingsRepositoryModule { + @Provides + @SysUISingleton + fun provideNotificationSettingsRepository( + @Background backgroundScope: CoroutineScope, + @Background backgroundDispatcher: CoroutineDispatcher, + secureSettingsRepository: SecureSettingsRepository, + ): NotificationSettingsRepository = + NotificationSettingsRepository( + backgroundScope, + backgroundDispatcher, + secureSettingsRepository + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/NotificationDomainLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/NotificationDomainLayerModule.kt new file mode 100644 index 000000000000..5c49b28f343f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/NotificationDomainLayerModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.domain + +import com.android.systemui.statusbar.notification.domain.interactor.NotificationSettingsInteractorModule +import dagger.Module + +@Module(includes = [NotificationSettingsInteractorModule::class]) +object NotificationDomainLayerModule diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationSettingsInteractorModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationSettingsInteractorModule.kt new file mode 100644 index 000000000000..0a9e12a23b8e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationSettingsInteractorModule.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.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.statusbar.notification.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository +import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor +import dagger.Module +import dagger.Provides + +@Module +object NotificationSettingsInteractorModule { + @Provides + @SysUISingleton + fun provideNotificationSettingsInteractor(repository: NotificationSettingsRepository) = + NotificationSettingsInteractor(repository) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java index 3616fd6d8cd1..16f18a3c3fb6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java @@ -54,7 +54,7 @@ public class FooterView extends StackScrollerDecorView { private static final String TAG = "FooterView"; private FooterViewButton mClearAllButton; - private FooterViewButton mManageButton; + private FooterViewButton mManageOrHistoryButton; private boolean mShowHistory; // String cache, for performance reasons. // Reading them from a Resources object can be quite slow sometimes. @@ -68,6 +68,8 @@ public class FooterView extends StackScrollerDecorView { private @StringRes int mClearAllButtonTextId; private @StringRes int mClearAllButtonDescriptionId; + private @StringRes int mManageOrHistoryButtonTextId; + private @StringRes int mManageOrHistoryButtonDescriptionId; private @StringRes int mMessageStringId; private @DrawableRes int mMessageIconId; @@ -155,6 +157,43 @@ public class FooterView extends StackScrollerDecorView { mClearAllButton.setContentDescription(getContext().getString(mClearAllButtonDescriptionId)); } + /** Set the text label for the "Manage"/"History" button. */ + public void setManageOrHistoryButtonText(@StringRes int textId) { + if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return; + if (mManageOrHistoryButtonTextId == textId) { + return; // nothing changed + } + mManageOrHistoryButtonTextId = textId; + updateManageOrHistoryButtonText(); + } + + private void updateManageOrHistoryButtonText() { + if (mManageOrHistoryButtonTextId == 0) { + return; // not initialized yet + } + mManageOrHistoryButton.setText(getContext().getString(mManageOrHistoryButtonTextId)); + } + + /** Set the accessibility content description for the "Clear all" button. */ + public void setManageOrHistoryButtonDescription(@StringRes int contentDescriptionId) { + if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { + return; + } + if (mManageOrHistoryButtonDescriptionId == contentDescriptionId) { + return; // nothing changed + } + mManageOrHistoryButtonDescriptionId = contentDescriptionId; + updateManageOrHistoryButtonDescription(); + } + + private void updateManageOrHistoryButtonDescription() { + if (mManageOrHistoryButtonDescriptionId == 0) { + return; // not initialized yet + } + mManageOrHistoryButton.setContentDescription( + getContext().getString(mManageOrHistoryButtonDescriptionId)); + } + /** Set the string for a message to be shown instead of the buttons. */ public void setMessageString(@StringRes int messageId) { if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return; @@ -173,7 +212,6 @@ public class FooterView extends StackScrollerDecorView { mSeenNotifsFooterTextView.setText(messageString); } - /** Set the icon to be shown before the message (see {@link #setMessageString(int)}). */ public void setMessageIcon(@DrawableRes int iconId) { if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return; @@ -203,9 +241,11 @@ public class FooterView extends StackScrollerDecorView { protected void onFinishInflate() { super.onFinishInflate(); mClearAllButton = (FooterViewButton) findSecondaryView(); - mManageButton = findViewById(R.id.manage_text); + mManageOrHistoryButton = findViewById(R.id.manage_text); mSeenNotifsFooterTextView = findViewById(R.id.unlock_prompt_footer); - updateResources(); + if (!FooterViewRefactor.isEnabled()) { + updateResources(); + } updateContent(); updateColors(); } @@ -213,11 +253,11 @@ public class FooterView extends StackScrollerDecorView { /** Show a message instead of the footer buttons. */ public void setFooterLabelVisible(boolean isVisible) { if (isVisible) { - mManageButton.setVisibility(View.GONE); + mManageOrHistoryButton.setVisibility(View.GONE); mClearAllButton.setVisibility(View.GONE); mSeenNotifsFooterTextView.setVisibility(View.VISIBLE); } else { - mManageButton.setVisibility(View.VISIBLE); + mManageOrHistoryButton.setVisibility(View.VISIBLE); mClearAllButton.setVisibility(View.VISIBLE); mSeenNotifsFooterTextView.setVisibility(View.GONE); } @@ -225,7 +265,7 @@ public class FooterView extends StackScrollerDecorView { /** Set onClickListener for the manage/history button. */ public void setManageButtonClickListener(OnClickListener listener) { - mManageButton.setOnClickListener(listener); + mManageOrHistoryButton.setOnClickListener(listener); } /** Set onClickListener for the clear all (end) button. */ @@ -252,6 +292,7 @@ public class FooterView extends StackScrollerDecorView { /** Show "History" instead of "Manage" on the start button. */ public void showHistory(boolean showHistory) { + FooterViewRefactor.assertInLegacyMode(); if (mShowHistory == showHistory) { return; } @@ -260,17 +301,13 @@ public class FooterView extends StackScrollerDecorView { } private void updateContent() { - if (mShowHistory) { - mManageButton.setText(mManageNotificationHistoryText); - mManageButton.setContentDescription(mManageNotificationHistoryText); - } else { - mManageButton.setText(mManageNotificationText); - mManageButton.setContentDescription(mManageNotificationText); - } if (FooterViewRefactor.isEnabled()) { updateClearAllButtonText(); updateClearAllButtonDescription(); + updateManageOrHistoryButtonText(); + updateManageOrHistoryButtonDescription(); + updateMessageString(); updateMessageIcon(); } else { @@ -285,6 +322,14 @@ public class FooterView extends StackScrollerDecorView { // `updateResources`, which will eventually be removed. There are, however, still // situations in which we want to update the views even if the resource IDs didn't // change, such as configuration changes. + if (mShowHistory) { + mManageOrHistoryButton.setText(mManageNotificationHistoryText); + mManageOrHistoryButton.setContentDescription(mManageNotificationHistoryText); + } else { + mManageOrHistoryButton.setText(mManageNotificationText); + mManageOrHistoryButton.setContentDescription(mManageNotificationText); + } + mClearAllButton.setText(R.string.clear_all_notifications_text); mClearAllButton.setContentDescription( mContext.getString(R.string.accessibility_clear_all)); @@ -297,6 +342,7 @@ public class FooterView extends StackScrollerDecorView { /** Whether the start button shows "History" (true) or "Manage" (false). */ public boolean isHistoryShown() { + FooterViewRefactor.assertInLegacyMode(); return mShowHistory; } @@ -304,7 +350,9 @@ public class FooterView extends StackScrollerDecorView { protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateColors(); - updateResources(); + if (!FooterViewRefactor.isEnabled()) { + updateResources(); + } updateContent(); } @@ -328,23 +376,22 @@ public class FooterView extends StackScrollerDecorView { } mClearAllButton.setBackground(clearAllBg); mClearAllButton.setTextColor(onSurface); - mManageButton.setBackground(manageBg); - mManageButton.setTextColor(onSurface); + mManageOrHistoryButton.setBackground(manageBg); + mManageOrHistoryButton.setTextColor(onSurface); mSeenNotifsFooterTextView.setTextColor(onSurface); mSeenNotifsFooterTextView.setCompoundDrawableTintList(ColorStateList.valueOf(onSurface)); } private void updateResources() { + FooterViewRefactor.assertInLegacyMode(); mManageNotificationText = getContext().getString(R.string.manage_notifications_text); mManageNotificationHistoryText = getContext() .getString(R.string.manage_notifications_history_text); - if (!FooterViewRefactor.isEnabled()) { - int unlockIconSize = getResources() - .getDimensionPixelSize(R.dimen.notifications_unseen_footer_icon_size); - mSeenNotifsFilteredText = getContext().getString(R.string.unlock_to_see_notif_text); - mSeenNotifsFilteredIcon = getContext().getDrawable(R.drawable.ic_friction_lock_closed); - mSeenNotifsFilteredIcon.setBounds(0, 0, unlockIconSize, unlockIconSize); - } + int unlockIconSize = getResources() + .getDimensionPixelSize(R.dimen.notifications_unseen_footer_icon_size); + mSeenNotifsFilteredText = getContext().getString(R.string.unlock_to_see_notif_text); + mSeenNotifsFilteredIcon = getContext().getDrawable(R.drawable.ic_friction_lock_closed); + mSeenNotifsFilteredIcon.setBounds(0, 0, unlockIconSize, unlockIconSize); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt index e0eee96d8f0a..9fb453afb55e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt @@ -25,49 +25,136 @@ import com.android.systemui.util.ui.isAnimating import com.android.systemui.util.ui.stopAnimating import com.android.systemui.util.ui.value import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch /** Binds a [FooterView] to its [view model][FooterViewModel]. */ object FooterViewBinder { - fun bind( + fun bindWhileAttached( footer: FooterView, viewModel: FooterViewModel, clearAllNotifications: View.OnClickListener, + launchNotificationSettings: View.OnClickListener, + launchNotificationHistory: View.OnClickListener, ): DisposableHandle { - // Bind the resource IDs - footer.setMessageString(viewModel.message.messageId) - footer.setMessageIcon(viewModel.message.iconId) - footer.setClearAllButtonText(viewModel.clearAllButton.labelId) - footer.setClearAllButtonDescription(viewModel.clearAllButton.accessibilityDescriptionId) + return footer.repeatWhenAttached { + lifecycleScope.launch { + bind( + footer, + viewModel, + clearAllNotifications, + launchNotificationSettings, + launchNotificationHistory + ) + } + } + } + + suspend fun bind( + footer: FooterView, + viewModel: FooterViewModel, + clearAllNotifications: View.OnClickListener, + launchNotificationSettings: View.OnClickListener, + launchNotificationHistory: View.OnClickListener + ) = coroutineScope { + launch { + bindClearAllButton( + footer, + viewModel, + clearAllNotifications, + ) + } + launch { + bindManageOrHistoryButton( + footer, + viewModel, + launchNotificationSettings, + launchNotificationHistory + ) + } + launch { bindMessage(footer, viewModel) } + } - // Bind the click listeners + private suspend fun bindClearAllButton( + footer: FooterView, + viewModel: FooterViewModel, + clearAllNotifications: View.OnClickListener, + ) = coroutineScope { footer.setClearAllButtonClickListener(clearAllNotifications) - // Listen for visibility changes when the view is attached. - return footer.repeatWhenAttached { - lifecycleScope.launch { - viewModel.clearAllButton.isVisible.collect { isVisible -> - if (isVisible.isAnimating) { - footer.setClearAllButtonVisible( - isVisible.value, - /* animate = */ true, - ) { _ -> - isVisible.stopAnimating() - } - } else { - footer.setClearAllButtonVisible( - isVisible.value, - /* animate = */ false, - ) + launch { + viewModel.clearAllButton.labelId.collect { textId -> + footer.setClearAllButtonText(textId) + } + } + + launch { + viewModel.clearAllButton.accessibilityDescriptionId.collect { textId -> + footer.setClearAllButtonDescription(textId) + } + } + + launch { + viewModel.clearAllButton.isVisible.collect { isVisible -> + if (isVisible.isAnimating) { + footer.setClearAllButtonVisible( + isVisible.value, + /* animate = */ true, + ) { _ -> + isVisible.stopAnimating() } + } else { + footer.setClearAllButtonVisible( + isVisible.value, + /* animate = */ false, + ) } } + } + } - lifecycleScope.launch { - viewModel.message.isVisible.collect { visible -> - footer.setFooterLabelVisible(visible) + private suspend fun bindManageOrHistoryButton( + footer: FooterView, + viewModel: FooterViewModel, + launchNotificationSettings: View.OnClickListener, + launchNotificationHistory: View.OnClickListener, + ) = coroutineScope { + launch { + viewModel.manageButtonShouldLaunchHistory.collect { shouldLaunchHistory -> + if (shouldLaunchHistory) { + footer.setManageButtonClickListener(launchNotificationHistory) + } else { + footer.setManageButtonClickListener(launchNotificationSettings) } } } + + launch { + viewModel.manageOrHistoryButton.labelId.collect { textId -> + footer.setManageOrHistoryButtonText(textId) + } + } + + launch { + viewModel.clearAllButton.accessibilityDescriptionId.collect { textId -> + footer.setManageOrHistoryButtonDescription(textId) + } + } + + // NOTE: The manage/history button is always visible as long as the footer is visible, no + // need to update the visibility here. + } + + private suspend fun bindMessage( + footer: FooterView, + viewModel: FooterViewModel, + ) = coroutineScope { + // Bind the resource IDs + footer.setMessageString(viewModel.message.messageId) + footer.setMessageIcon(viewModel.message.iconId) + + launch { + viewModel.message.isVisible.collect { visible -> footer.setFooterLabelVisible(visible) } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt index 244555a3d73b..691dc4297145 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt @@ -21,7 +21,7 @@ import com.android.systemui.util.ui.AnimatedValue import kotlinx.coroutines.flow.Flow data class FooterButtonViewModel( - @StringRes val labelId: Int, - @StringRes val accessibilityDescriptionId: Int, + @StringRes val labelId: Flow<Int>, + @StringRes val accessibilityDescriptionId: Flow<Int>, val isVisible: Flow<AnimatedValue<Boolean>>, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt index e6b0abcfad65..5111c11ac584 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt @@ -19,30 +19,36 @@ package com.android.systemui.statusbar.notification.footer.ui.viewmodel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.util.kotlin.sample import com.android.systemui.util.ui.AnimatableEvent +import com.android.systemui.util.ui.AnimatedValue import com.android.systemui.util.ui.toAnimatedValueFlow import dagger.Module import dagger.Provides import java.util.Optional import javax.inject.Provider +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart /** ViewModel for [FooterView]. */ class FooterViewModel( activeNotificationsInteractor: ActiveNotificationsInteractor, + notificationSettingsInteractor: NotificationSettingsInteractor, seenNotificationsInteractor: SeenNotificationsInteractor, shadeInteractor: ShadeInteractor, ) { val clearAllButton: FooterButtonViewModel = FooterButtonViewModel( - labelId = R.string.clear_all_notifications_text, - accessibilityDescriptionId = R.string.accessibility_clear_all, + labelId = flowOf(R.string.clear_all_notifications_text), + accessibilityDescriptionId = flowOf(R.string.accessibility_clear_all), isVisible = activeNotificationsInteractor.hasClearableNotifications .sample( @@ -59,6 +65,22 @@ class FooterViewModel( .toAnimatedValueFlow(), ) + val manageButtonShouldLaunchHistory = + notificationSettingsInteractor.isNotificationHistoryEnabled + + private val manageOrHistoryButtonText: Flow<Int> = + manageButtonShouldLaunchHistory.map { shouldLaunchHistory -> + if (shouldLaunchHistory) R.string.manage_notifications_history_text + else R.string.manage_notifications_text + } + + val manageOrHistoryButton: FooterButtonViewModel = + FooterButtonViewModel( + labelId = manageOrHistoryButtonText, + accessibilityDescriptionId = manageOrHistoryButtonText, + isVisible = flowOf(AnimatedValue.NotAnimating(true)), + ) + val message: FooterMessageViewModel = FooterMessageViewModel( messageId = R.string.unlock_to_see_notif_text, @@ -73,6 +95,7 @@ object FooterViewModelModule { @SysUISingleton fun provideOptional( activeNotificationsInteractor: Provider<ActiveNotificationsInteractor>, + notificationSettingsInteractor: Provider<NotificationSettingsInteractor>, seenNotificationsInteractor: Provider<SeenNotificationsInteractor>, shadeInteractor: Provider<ShadeInteractor>, ): Optional<FooterViewModel> { @@ -80,6 +103,7 @@ object FooterViewModelModule { Optional.of( FooterViewModel( activeNotificationsInteractor.get(), + notificationSettingsInteractor.get(), seenNotificationsInteractor.get(), shadeInteractor.get() ) 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 b9afb1409d91..5e0110b8af03 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 @@ -4698,6 +4698,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable * this will return false. **/ public boolean isHistoryShown() { + FooterViewRefactor.assertInLegacyMode(); return mFooterView != null && mFooterView.isHistoryShown(); } @@ -4710,10 +4711,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } mFooterView = footerView; addView(mFooterView, index); - if (mManageButtonClickListener != null) { - mFooterView.setManageButtonClickListener(mManageButtonClickListener); - } if (!FooterViewRefactor.isEnabled()) { + if (mManageButtonClickListener != null) { + mFooterView.setManageButtonClickListener(mManageButtonClickListener); + } mFooterView.setClearAllButtonClickListener(v -> { if (mFooterClearAllListener != null) { mFooterClearAllListener.onClearAll(); @@ -4794,8 +4795,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } boolean animate = mIsExpanded && mAnimationsEnabled; mFooterView.setVisible(visible, animate); - mFooterView.showHistory(showHistory); if (!FooterViewRefactor.isEnabled()) { + mFooterView.showHistory(showHistory); mFooterView.setClearAllButtonVisible(showDismissView, animate); mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications); } @@ -5490,6 +5491,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable * Register a {@link View.OnClickListener} to be invoked when the Manage button is clicked. */ public void setManageButtonClickListener(@Nullable OnClickListener listener) { + FooterViewRefactor.assertInLegacyMode(); mManageButtonClickListener = listener; if (mFooterView != null) { mFooterView.setManageButtonClickListener(mManageButtonClickListener); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index ed266772ac15..a2ff406b9599 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -21,8 +21,9 @@ import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_N import static com.android.app.animation.Interpolators.STANDARD; import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING; +import static com.android.server.notification.Flags.screenshareNotificationHiding; import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME; -import static com.android.systemui.Flags.screenshareNotificationHiding; +import static com.android.systemui.Flags.nsslFalsingFix; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnOverscrollTopChangedListener; @@ -845,11 +846,13 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.setKeyguardBypassEnabled(mKeyguardBypassController.getBypassEnabled()); mKeyguardBypassController .registerOnBypassStateChangedListener(mView::setKeyguardBypassEnabled); - mView.setManageButtonClickListener(v -> { - if (mNotificationActivityStarter != null) { - mNotificationActivityStarter.startHistoryIntent(v, mView.isHistoryShown()); - } - }); + if (!FooterViewRefactor.isEnabled()) { + mView.setManageButtonClickListener(v -> { + if (mNotificationActivityStarter != null) { + mNotificationActivityStarter.startHistoryIntent(v, mView.isHistoryShown()); + } + }); + } mHeadsUpManager.addListener(mOnHeadsUpChangedListener); mHeadsUpManager.setAnimationStateHandler(mView::setHeadsUpGoingAwayAnimationsAllowed); @@ -2052,7 +2055,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } boolean horizontalSwipeWantsIt = false; boolean scrollerWantsIt = false; - if (KeyguardShadeMigrationNssl.isEnabled()) { + if (nsslFalsingFix() || KeyguardShadeMigrationNssl.isEnabled()) { // Reverse the order relative to the else statement. onScrollTouch will reset on an // UP event, causing horizontalSwipeWantsIt to be set to true on vertical swipes. if (mLongPressedView == null && !mView.isBeingDragged() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index 44a7e7e27455..4d65b9d9c792 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -29,6 +29,7 @@ import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.statusbar.NotificationShelf +import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder @@ -45,6 +46,7 @@ import com.android.systemui.statusbar.phone.NotificationIconAreaController import com.android.systemui.util.kotlin.getOrNull import java.util.Optional import javax.inject.Inject +import javax.inject.Provider import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @@ -58,9 +60,11 @@ constructor( private val configuration: ConfigurationState, private val falsingManager: FalsingManager, private val iconAreaController: NotificationIconAreaController, + private val loggerOptional: Optional<NotificationStatsLogger>, private val metricsLogger: MetricsLogger, private val nicBinder: NotificationIconContainerShelfViewBinder, - private val loggerOptional: Optional<NotificationStatsLogger>, + // Using a provider to avoid a circular dependency. + private val notificationActivityStarter: Provider<NotificationActivityStarter>, private val viewModel: NotificationListViewModel, ) { @@ -115,7 +119,7 @@ constructor( ) { footerView: FooterView -> traceSection("bind FooterView") { val disposableHandle = - FooterViewBinder.bind( + FooterViewBinder.bindWhileAttached( footerView, footerViewModel, clearAllNotifications = { @@ -124,6 +128,16 @@ constructor( ) parentView.clearAllNotifications() }, + launchNotificationSettings = { view -> + notificationActivityStarter + .get() + .startHistoryIntent(view, /* showHistory = */ false) + }, + launchNotificationHistory = { view -> + notificationActivityStarter + .get() + .startHistoryIntent(view, /* showHistory = */ true) + }, ) parentView.setFooterView(footerView) return@reinflateAndBindLatest disposableHandle diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java index 3c4ca4465874..11e374f24f9c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java @@ -16,12 +16,13 @@ package com.android.systemui.statusbar.policy; -import static com.android.systemui.Flags.screenshareNotificationHiding; +import static com.android.server.notification.Flags.screenshareNotificationHiding; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; import android.os.Handler; import android.os.Trace; +import android.service.notification.StatusBarNotification; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.SysUISingleton; @@ -46,8 +47,9 @@ public class SensitiveNotificationProtectionControllerImpl public void onStart(MediaProjectionInfo info) { Trace.beginSection( "SNPC.onProjectionStart"); - mProjection = info; - mListeners.forEach(Runnable::run); + // Only enable sensitive content protection if sharing full screen + // Launch cookie only set (non-null) if sharing single app/task + updateProjectionState((info.getLaunchCookie() == null) ? info : null); Trace.endSection(); } @@ -55,10 +57,22 @@ public class SensitiveNotificationProtectionControllerImpl public void onStop(MediaProjectionInfo info) { Trace.beginSection( "SNPC.onProjectionStop"); - mProjection = null; - mListeners.forEach(Runnable::run); + updateProjectionState(null); Trace.endSection(); } + + private void updateProjectionState(MediaProjectionInfo info) { + // capture previous state + boolean wasSensitive = isSensitiveStateActive(); + + // update internal state + mProjection = info; + + // if either previous or new state is sensitive, notify listeners. + if (wasSensitive || isSensitiveStateActive()) { + mListeners.forEach(Runnable::run); + } + } }; @Inject @@ -86,7 +100,6 @@ public class SensitiveNotificationProtectionControllerImpl public boolean isSensitiveStateActive() { // TODO(b/316955558): Add disabled by developer option // TODO(b/316955306): Add feature exemption for sysui and bug handlers - // TODO(b/316955346): Add feature exemption for single app screen sharing return mProjection != null; } @@ -96,9 +109,18 @@ public class SensitiveNotificationProtectionControllerImpl return false; } + MediaProjectionInfo projection = mProjection; + if (projection == null) { + return false; + } + // Exempt foreground service notifications from protection in effort to keep screen share // stop actions easily accessible - // TODO(b/316955208): Exempt FGS notifications only for app that started projection - return !entry.getSbn().getNotification().isFgsOrUij(); + StatusBarNotification sbn = entry.getSbn(); + if (sbn.getNotification().isFgsOrUij()) { + return !sbn.getPackageName().equals(projection.getPackageName()); + } + + return true; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt new file mode 100644 index 000000000000..32cf86d8e390 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt @@ -0,0 +1,40 @@ +/* + * 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.statusbar.policy.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/** Encapsulates device-provisioning related business logic. */ +@SysUISingleton +class DeviceProvisioningInteractor +@Inject +constructor( + repository: DeviceProvisioningRepository, +) { + /** + * Whether this device has been provisioned. + * + * @see android.provider.Settings.Global.DEVICE_PROVISIONED + */ + val isDeviceProvisioned: Flow<Boolean> = repository.isDeviceProvisioned + + /** Whether Factory Reset Protection (FRP) is currently active, locking the device. */ + val isFactoryResetProtectionActive: Flow<Boolean> = repository.isFactoryResetProtectionActive +} diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/SharedPreferencesExt.kt index b09bfe21e014..ab6a37bccc11 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerExt.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/SharedPreferencesExt.kt @@ -12,32 +12,28 @@ * 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.settings +package com.android.systemui.util.kotlin -import android.annotation.UserIdInt -import android.content.Context import android.content.SharedPreferences import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapNotNull -/** Extension functions for [UserFileManager]. */ -object UserFileManagerExt { - - /** Returns a flow of [Unit] that is invoked each time the shared preference is updated. */ - fun UserFileManager.observeSharedPreferences( - fileName: String, - @Context.PreferencesMode mode: Int, - @UserIdInt userId: Int - ): Flow<Unit> = conflatedCallbackFlow { - val sharedPrefs = getSharedPreferences(fileName, mode, userId) - - val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> trySend(Unit) } - - sharedPrefs.registerOnSharedPreferenceChangeListener(listener) - awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) } - } +object SharedPreferencesExt { + /** + * Returns a flow of [Unit] that is invoked each time shared preference is updated. + * + * @param key Optional key to limit updates to a particular key. + */ + fun SharedPreferences.observe(key: String? = null): Flow<Unit> = + conflatedCallbackFlow { + val listener = + SharedPreferences.OnSharedPreferenceChangeListener { _, key -> trySend(key) } + registerOnSharedPreferenceChangeListener(listener) + awaitClose { unregisterOnSharedPreferenceChangeListener(listener) } + } + .mapNotNull { changedKey -> if ((key ?: changedKey) == changedKey) Unit else null } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt index 8d5e55a2917e..ff1daea4816e 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.dagger +import android.content.Context import android.media.AudioManager import com.android.settingslib.volume.data.repository.AudioRepository import com.android.settingslib.volume.data.repository.AudioRepositoryImpl @@ -35,10 +36,12 @@ interface AudioModule { @Provides fun provideAudioRepository( + @Application context: Context, audioManager: AudioManager, @Background coroutineContext: CoroutineContext, @Application coroutineScope: CoroutineScope, - ): AudioRepository = AudioRepositoryImpl(audioManager, coroutineContext, coroutineScope) + ): AudioRepository = + AudioRepositoryImpl(context, audioManager, coroutineContext, coroutineScope) @Provides fun provideAudioModeInteractor(repository: AudioRepository): AudioModeInteractor = diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java index 8299acbc2d52..375ebe86ae29 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -51,6 +51,7 @@ import android.view.WindowManagerGlobal; import android.view.accessibility.IRemoteMagnificationAnimationCallback; import android.view.animation.AccelerateInterpolator; +import androidx.test.filters.FlakyTest; import androidx.test.filters.LargeTest; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; @@ -79,6 +80,7 @@ import java.util.function.Supplier; @LargeTest @RunWith(AndroidTestingRunner.class) +@FlakyTest(bugId = 308501761) public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { @Rule diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt index 8f0e910271c7..8fbeb6f93360 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.biometrics.data.repository import android.hardware.biometrics.AuthenticationStateListener +import android.hardware.biometrics.BiometricFingerprintConstants import android.hardware.biometrics.BiometricManager import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_BP import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD @@ -24,11 +25,13 @@ import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_OTHER import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR +import android.hardware.biometrics.BiometricSourceType import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat @@ -167,6 +170,28 @@ class BiometricStatusRepositoryTest : SysuiTestCase() { listener.onAuthenticationStopped() assertThat(fingerprintAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning) } + + @Test + fun updatesFingerprintAcquiredStatusWhenBiometricPromptAuthenticationAcquired() = + testScope.runTest { + val fingerprintAcquiredStatus by collectLastValue(underTest.fingerprintAcquiredStatus) + runCurrent() + + val listener = biometricManager.captureListener() + listener.onAuthenticationAcquired( + BiometricSourceType.FINGERPRINT, + REASON_AUTH_BP, + BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START + ) + + assertThat(fingerprintAcquiredStatus) + .isEqualTo( + AcquiredFingerprintAuthenticationStatus( + AuthenticationReason.BiometricPromptAuthentication, + BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START + ) + ) + } } private fun BiometricManager.captureListener() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt index d7b7d79425c8..5c34fd9a1bc5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt @@ -19,12 +19,14 @@ package com.android.systemui.biometrics.domain.interactor import android.app.ActivityManager import android.app.ActivityTaskManager import android.content.ComponentName +import android.hardware.biometrics.BiometricFingerprintConstants import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -162,6 +164,27 @@ class BiometricStatusInteractorImplTest : SysuiTestCase() { ) assertThat(sfpsAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning) } + + @Test + fun updatesFingerprintAcquiredStatusWhenBiometricPromptAuthenticationAcquired() = + testScope.runTest { + val fingerprintAcquiredStatus by collectLastValue(underTest.fingerprintAcquiredStatus) + runCurrent() + + biometricStatusRepository.setFingerprintAcquiredStatus( + AcquiredFingerprintAuthenticationStatus( + AuthenticationReason.BiometricPromptAuthentication, + BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START + ) + ) + assertThat(fingerprintAcquiredStatus) + .isEqualTo( + AcquiredFingerprintAuthenticationStatus( + AuthenticationReason.BiometricPromptAuthentication, + BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START + ) + ) + } } private fun fpSettingsTask() = settingsTask(".biometrics.fingerprint.FingerprintSettings") diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt index 3603c3c6c46a..5509c048b0da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt @@ -58,6 +58,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository @@ -100,6 +101,7 @@ import org.junit.runners.JUnit4 import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.any +import org.mockito.Mockito.inOrder import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.spy @@ -253,7 +255,8 @@ class SideFpsOverlayViewBinderTest : SysuiTestCase() { sideFpsProgressBarViewModel = SideFpsProgressBarViewModel( mContext, - mock(), + biometricStatusInteractor, + kosmos.deviceEntryFingerprintAuthInteractor, sfpsSensorInteractor, kosmos.dozeServiceHost, kosmos.keyguardInteractor, @@ -426,6 +429,54 @@ class SideFpsOverlayViewBinderTest : SysuiTestCase() { } } + // On progress bar shown - hide indicator + // On progress bar hidden - show indicator + @Test + fun verifyIndicatorProgressBarInteraction() { + testScope.runTest { + // Pre-auth conditions + setupTestConfiguration( + DeviceConfig.X_ALIGNED, + rotation = DisplayRotation.ROTATION_0, + isInRearDisplayMode = false + ) + biometricStatusRepository.setFingerprintAuthenticationReason( + AuthenticationReason.NotRunning + ) + sideFpsProgressBarViewModel.setVisible(false) + + // Show primary bouncer + updatePrimaryBouncer( + isShowing = true, + isAnimatingAway = false, + fpsDetectionRunning = true, + isUnlockingWithFpAllowed = true + ) + runCurrent() + + val inOrder = inOrder(windowManager) + + // Verify indicator shown + inOrder.verify(windowManager).addView(any(), any()) + + // Set progress bar visible + sideFpsProgressBarViewModel.setVisible(true) + + runCurrent() + + // Verify indicator hidden + inOrder.verify(windowManager).removeView(any()) + + // Set progress bar invisible + sideFpsProgressBarViewModel.setVisible(false) + + runCurrent() + + // Verify indicator shown + inOrder.verify(windowManager).addView(any(), any()) + } + } + private fun updatePrimaryBouncer( isShowing: Boolean, isAnimatingAway: Boolean, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 6a9c88151dd0..2e94d381b8dc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -81,6 +81,7 @@ private const val USER_ID = 4 private const val CHALLENGE = 2L private const val DELAY = 1000L private const val OP_PACKAGE_NAME = "biometric.testapp" +private const val OP_PACKAGE_NAME_NO_ICON = "biometric.testapp.noicon" @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -1246,6 +1247,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test + fun logoIsNullIfPackageNameNotFound() = + runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + val logo by collectLastValue(viewModel.logo) + assertThat(logo).isNull() + } + + @Test fun defaultLogoIfNoLogoSet() = runGenericTest { mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) val logo by collectLastValue(viewModel.logo) @@ -1291,7 +1300,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa contentView: PromptContentView? = null, logoRes: Int = -1, logoBitmap: Bitmap? = null, - block: suspend TestScope.() -> Unit + packageName: String = OP_PACKAGE_NAME, + block: suspend TestScope.() -> Unit, ) { selector.initializePrompt( requireConfirmation = testCase.confirmationRequested, @@ -1302,6 +1312,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa contentViewFromApp = contentView, logoResFromApp = logoRes, logoBitmapFromApp = logoBitmap, + packageName = packageName, ) // put the view model in the initial authenticating state, unless explicitly skipped @@ -1481,6 +1492,7 @@ private fun PromptSelectorInteractor.initializePrompt( contentViewFromApp: PromptContentView? = null, logoResFromApp: Int = -1, logoBitmapFromApp: Bitmap? = null, + packageName: String = OP_PACKAGE_NAME, ) { val info = PromptInfo().apply { @@ -1500,7 +1512,7 @@ private fun PromptSelectorInteractor.initializePrompt( USER_ID, CHALLENGE, BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face), - OP_PACKAGE_NAME, + packageName, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt index 3c430316ffbe..2014755bd964 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.biometrics.ui.viewmodel -import android.app.ActivityTaskManager import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.graphics.Color @@ -39,10 +38,10 @@ import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository -import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor -import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractorImpl +import com.android.systemui.biometrics.data.repository.biometricStatusRepository import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor +import com.android.systemui.biometrics.domain.interactor.biometricStatusInteractor import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.FingerprintSensorType @@ -80,7 +79,6 @@ import com.android.systemui.testKosmos import com.android.systemui.unfold.compat.ScreenSizeFoldProvider import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat @@ -109,7 +107,6 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule() - @Mock private lateinit var activityTaskManager: ActivityTaskManager @Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor @Mock private lateinit var fingerprintInteractiveToAuthProvider: FingerprintInteractiveToAuthProvider @@ -147,7 +144,6 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { context.getColor(com.android.settingslib.color.R.color.settingslib_color_blue400) private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor - private lateinit var biometricStatusInteractor: BiometricStatusInteractor private lateinit var deviceEntrySideFpsOverlayInteractor: DeviceEntrySideFpsOverlayInteractor private lateinit var displayStateInteractor: DisplayStateInteractorImpl private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor @@ -184,6 +180,7 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { .thenReturn( Display(mock(DisplayManagerGlobal::class.java), 1, contextDisplayInfo, resources) ) + kosmos.biometricStatusRepository = biometricStatusRepository alternateBouncerInteractor = AlternateBouncerInteractor( @@ -197,9 +194,6 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { testScope.backgroundScope, ) - biometricStatusInteractor = - BiometricStatusInteractorImpl(activityTaskManager, biometricStatusRepository) - displayStateInteractor = DisplayStateInteractorImpl( testScope.backgroundScope, @@ -256,6 +250,7 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { sideFpsProgressBarViewModel = SideFpsProgressBarViewModel( mContext, + kosmos.biometricStatusInteractor, kosmos.deviceEntryFingerprintAuthInteractor, sfpsSensorInteractor, kosmos.dozeServiceHost, @@ -263,13 +258,13 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { displayStateInteractor, kosmos.testDispatcher, testScope.backgroundScope, - kosmos.powerInteractor, + kosmos.powerInteractor ) underTest = SideFpsOverlayViewModel( mContext, - biometricStatusInteractor, + kosmos.biometricStatusInteractor, deviceEntrySideFpsOverlayInteractor, displayStateInteractor, sfpsSensorInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt index 9c5cd713dffb..20dd913550d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt @@ -77,6 +77,18 @@ class CommunalWidgetDaoTest : SysuiTestCase() { } @Test + fun deleteWidget_notInDb_returnsFalse() = + testScope.runTest { + val (widgetId, provider, priority) = widgetInfo1 + communalWidgetDao.addWidget( + widgetId = widgetId, + provider = provider, + priority = priority, + ) + assertThat(communalWidgetDao.deleteWidgetById(widgetId = 123)).isFalse() + } + + @Test fun addWidget_emitsActiveWidgetsInDb(): Unit = testScope.runTest { val widgetsToAdd = listOf(widgetInfo1, widgetInfo2) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt index c98d5374311d..de455f6374f1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt @@ -34,11 +34,12 @@ import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.panels.AuthorizedPanelsRepository -import com.android.systemui.controls.panels.FakeSelectedComponentRepository +import com.android.systemui.controls.panels.selectedComponentRepository import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dump.DumpManager import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker +import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock @@ -69,12 +70,13 @@ import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import java.io.File -import java.util.* +import java.util.Optional import java.util.function.Consumer @SmallTest @RunWith(AndroidTestingRunner::class) class ControlsControllerImplTest : SysuiTestCase() { + private val kosmos = testKosmos() @Mock private lateinit var uiController: ControlsUiController @@ -109,8 +111,6 @@ class ControlsControllerImplTest : SysuiTestCase() { private lateinit var listingCallbackCaptor: ArgumentCaptor<ControlsListingController.ControlsListingCallback> - private val preferredPanelRepository = FakeSelectedComponentRepository() - private lateinit var delayableExecutor: FakeExecutor private lateinit var controller: ControlsControllerImpl private lateinit var canceller: DidRunRunnable @@ -171,7 +171,7 @@ class ControlsControllerImplTest : SysuiTestCase() { wrapper, delayableExecutor, uiController, - preferredPanelRepository, + kosmos.selectedComponentRepository, bindingController, listingController, userFileManager, @@ -225,7 +225,7 @@ class ControlsControllerImplTest : SysuiTestCase() { mContext, delayableExecutor, uiController, - preferredPanelRepository, + kosmos.selectedComponentRepository, bindingController, listingController, userFileManager, @@ -245,7 +245,7 @@ class ControlsControllerImplTest : SysuiTestCase() { mContext, delayableExecutor, uiController, - preferredPanelRepository, + kosmos.selectedComponentRepository, bindingController, listingController, userFileManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt index 4828ba37ecb5..18ce4a8e1b7a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt @@ -18,36 +18,40 @@ package com.android.systemui.controls.panels import android.content.SharedPreferences +import android.content.pm.UserInfo import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.UserFileManager -import com.android.systemui.settings.UserTracker +import com.android.systemui.settings.fakeUserTracker +import com.android.systemui.testKosmos import com.android.systemui.util.FakeSharedPreferences -import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import java.io.File +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() { + val kosmos = testKosmos() + val testScope = kosmos.testScope - @Mock private lateinit var userTracker: UserTracker + private lateinit var userTracker: FakeUserTracker @Before fun setUp() { - MockitoAnnotations.initMocks(this) mContext.orCreateTestableResources.addOverride( R.array.config_controlsPreferredPackages, arrayOf<String>() ) - whenever(userTracker.userId).thenReturn(0) + userTracker = kosmos.fakeUserTracker.apply { set(listOf(PRIMARY_USER, SECONDARY_USER), 0) } } @Test @@ -91,7 +95,7 @@ class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() { val repository = createRepository(fileManager) assertThat(repository.getAuthorizedPanels()).containsExactly(TEST_PACKAGE) - whenever(userTracker.userId).thenReturn(1) + userTracker.set(listOf(SECONDARY_USER), 0) assertThat(repository.getAuthorizedPanels()).isEmpty() } @@ -127,6 +131,51 @@ class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() { assertThat(sharedPrefs.getStringSet(KEY, null)).isEmpty() } + @Test + fun observeAuthorizedPanels() = + testScope.runTest { + val sharedPrefs = FakeSharedPreferences() + val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs)) + val repository = createRepository(fileManager) + + val authorizedPanels by + collectLastValue(repository.observeAuthorizedPanels(PRIMARY_USER.userHandle)) + assertThat(authorizedPanels).isEmpty() + + repository.addAuthorizedPanels(setOf(TEST_PACKAGE)) + assertThat(authorizedPanels).containsExactly(TEST_PACKAGE) + + repository.removeAuthorizedPanels(setOf(TEST_PACKAGE)) + assertThat(authorizedPanels).isEmpty() + } + + @Test + fun observeAuthorizedPanelsForAnotherUser() = + testScope.runTest { + val fileManager = + FakeUserFileManager( + mapOf( + 0 to FakeSharedPreferences(), + 1 to FakeSharedPreferences(), + ) + ) + val repository = createRepository(fileManager) + + val authorizedPanels by + collectLastValue(repository.observeAuthorizedPanels(SECONDARY_USER.userHandle)) + assertThat(authorizedPanels).isEmpty() + + // Primary user is active, add authorized panels. + repository.addAuthorizedPanels(setOf(TEST_PACKAGE)) + assertThat(authorizedPanels).isEmpty() + + // Make secondary user active and add authorized panels again. + userTracker.set(listOf(PRIMARY_USER, SECONDARY_USER), 1) + assertThat(authorizedPanels).isEmpty() + repository.addAuthorizedPanels(setOf(TEST_PACKAGE)) + assertThat(authorizedPanels).containsExactly(TEST_PACKAGE) + } + private fun createRepository(userFileManager: UserFileManager): AuthorizedPanelsRepositoryImpl { return AuthorizedPanelsRepositoryImpl(mContext, userFileManager, userTracker) } @@ -153,5 +202,9 @@ class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() { private const val FILE_NAME = "controls_prefs" private const val KEY = "authorized_panels" private const val TEST_PACKAGE = "package" + private val PRIMARY_USER = + UserInfo(/* id= */ 0, /* name= */ "primary user", /* flags= */ UserInfo.FLAG_MAIN) + private val SECONDARY_USER = + UserInfo(/* id= */ 1, /* name= */ "secondary user", /* flags= */ 0) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt index b463adf40a91..a7e7ba97b5e5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt @@ -23,8 +23,6 @@ import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.settings.UserFileManager @@ -74,7 +72,6 @@ class SelectedComponentRepositoryTest : SysuiTestCase() { @Mock private lateinit var userTracker: UserTracker private lateinit var userFileManager: UserFileManager - private val featureFlags = FakeFeatureFlags() // under test private lateinit var repository: SelectedComponentRepository @@ -95,11 +92,9 @@ class SelectedComponentRepositoryTest : SysuiTestCase() { ) repository = SelectedComponentRepositoryImpl( - userFileManager, - userTracker, - featureFlags, + userFileManager = userFileManager, + userTracker = userTracker, bgDispatcher = testDispatcher, - applicationScope = applicationCoroutineScope ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt index bcef67e76ea1..94ea799bc90b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt @@ -38,8 +38,8 @@ import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.panels.AuthorizedPanelsRepository -import com.android.systemui.controls.panels.FakeSelectedComponentRepository import com.android.systemui.controls.panels.SelectedComponentRepository +import com.android.systemui.controls.panels.selectedComponentRepository import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher @@ -87,7 +87,7 @@ class ControlsStartableTest : SysuiTestCase() { @Mock private lateinit var userManager: UserManager @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher - private lateinit var preferredPanelsRepository: FakeSelectedComponentRepository + private lateinit var preferredPanelsRepository: SelectedComponentRepository private lateinit var fakeExecutor: FakeExecutor @@ -99,7 +99,7 @@ class ControlsStartableTest : SysuiTestCase() { whenever(userTracker.userHandle).thenReturn(UserHandle.of(1)) fakeExecutor = FakeExecutor(FakeSystemClock()) - preferredPanelsRepository = FakeSelectedComponentRepository() + preferredPanelsRepository = kosmos.selectedComponentRepository } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index 36ae0c740c48..8f3813d49b8f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -43,8 +43,8 @@ import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.controls.panels.AuthorizedPanelsRepository -import com.android.systemui.controls.panels.FakeSelectedComponentRepository import com.android.systemui.controls.panels.SelectedComponentRepository +import com.android.systemui.controls.panels.selectedComponentRepository import com.android.systemui.controls.settings.FakeControlsSettingsRepository import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags @@ -53,6 +53,7 @@ import com.android.systemui.res.R import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.testKosmos import com.android.systemui.util.FakeSystemUIDialogController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any @@ -85,6 +86,8 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class ControlsUiControllerImplTest : SysuiTestCase() { + private val kosmos = testKosmos() + @Mock lateinit var controlsController: ControlsController @Mock lateinit var controlsListingController: ControlsListingController @Mock lateinit var controlActionCoordinator: ControlActionCoordinator @@ -100,7 +103,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Mock lateinit var packageManager: PackageManager @Mock lateinit var systemUIDialogFactory: SystemUIDialog.Factory - private val preferredPanelRepository = FakeSelectedComponentRepository() + private val preferredPanelRepository = kosmos.selectedComponentRepository private lateinit var fakeDialogController: FakeSystemUIDialogController private val uiExecutor = FakeExecutor(FakeSystemClock()) private val bgExecutor = FakeExecutor(FakeSystemClock()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt index 437a35f2fab5..e3c4c2858b3d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt @@ -475,10 +475,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { // Then the device name is the PhoneMediaDevice string val data = captureDeviceData(KEY) - assertThat(data.name) - .isEqualTo( - context.getString(com.android.settingslib.R.string.media_transfer_this_device_name) - ) + assertThat(data.name).isEqualTo(PhoneMediaDevice.getMediaTransferThisDeviceName(context)) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt index 239bf659b1b4..edba902d5886 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt @@ -30,6 +30,7 @@ import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.dream.MediaDreamComplication @@ -52,8 +53,6 @@ import com.android.systemui.utils.os.FakeHandler import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertNotNull @@ -106,8 +105,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { private lateinit var dreamOverlayCallback: ArgumentCaptor<(DreamOverlayStateController.Callback)> @JvmField @Rule val mockito = MockitoJUnit.rule() - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) + private val testScope = kosmos.testScope private lateinit var mediaHierarchyManager: MediaHierarchyManager private lateinit var isQsBypassingShade: MutableStateFlow<Boolean> private lateinit var mediaFrame: ViewGroup diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java index db5bd9b13dd0..0d1e87433c60 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java @@ -182,6 +182,8 @@ public class NavigationBarTest extends SysuiTestCase { @Mock private UiEventLogger mUiEventLogger; @Mock + private NavBarButtonClickLogger mNavBarButtonClickLogger; + @Mock private ViewTreeObserver mViewTreeObserver; NavBarHelper mNavBarHelper; @Mock @@ -596,7 +598,8 @@ public class NavigationBarTest extends SysuiTestCase { mUserContextProvider, mWakefulnessLifecycle, mTaskStackChangeListeners, - new FakeDisplayTracker(mContext))); + new FakeDisplayTracker(mContext), + mNavBarButtonClickLogger)); } private void processAllMessages() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index a6e240b1b701..08319719bee6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -44,6 +44,7 @@ import org.junit.Assert.assertThrows import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.BeforeClass +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock @@ -51,6 +52,7 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +@Ignore("b/323053208") @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index b3e386e69905..cc27cbd9d809 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -189,7 +189,6 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.KeyguardUserSwitcherController; import com.android.systemui.statusbar.policy.KeyguardUserSwitcherView; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; -import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository; import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository; import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.unfold.SysUIUnfoldComponent; @@ -411,7 +410,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mShadeInteractor = new ShadeInteractorImpl( mTestScope.getBackgroundScope(), - new FakeDeviceProvisioningRepository(), + mKosmos.getDeviceProvisioningInteractor(), new FakeDisableFlagsRepository(), mDozeParameters, mFakeKeyguardRepository, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index 461db8e6166c..7f4508a11c0e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -99,7 +99,6 @@ import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; -import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository; import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import com.android.systemui.user.domain.interactor.UserSwitcherInteractor; @@ -260,7 +259,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mShadeInteractor = new ShadeInteractorImpl( mTestScope.getBackgroundScope(), - new FakeDeviceProvisioningRepository(), + mKosmos.getDeviceProvisioningInteractor(), new FakeDisableFlagsRepository(), mock(DozeParameters.class), keyguardRepository, @@ -452,11 +451,11 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { } @Test - public void setCommunalShowing_userTimeout() { + public void setCommunalVisible_userTimeout() { setKeyguardShowing(); clearInvocations(mWindowManager); - mNotificationShadeWindowController.onCommunalShowingChanged(true); + mNotificationShadeWindowController.onCommunalVisibleChanged(true); verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); assertThat(mLayoutParameters.getValue().userActivityTimeout) .isEqualTo(CommunalInteractor.AWAKE_INTERVAL_MS); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt index 697b05aa9add..c2261211b339 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt @@ -28,8 +28,6 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.fragments.FragmentService import com.android.systemui.navigationbar.NavigationModeController @@ -94,7 +92,6 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { lateinit var underTest: NotificationsQSContainerController - private lateinit var featureFlags: FakeFeatureFlags private lateinit var navigationModeCallback: ModeChangedListener private lateinit var taskbarVisibilityCallback: OverviewProxyListener private lateinit var windowInsetsCallback: Consumer<WindowInsets> @@ -106,7 +103,6 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) fakeSystemClock = FakeSystemClock() delayableExecutor = FakeExecutor(fakeSystemClock) - featureFlags = FakeFeatureFlags().apply { set(Flags.QS_CONTAINER_GRAPH_OPTIMIZER, false) } mContext.ensureTestableResources() whenever(view.context).thenReturn(mContext) whenever(view.resources).thenReturn(mContext.resources) @@ -123,7 +119,6 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { shadeInteractor, fragmentService, delayableExecutor, - featureFlags, notificationStackScrollLayoutController, ResourcesSplitShadeStateController(), largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } @@ -536,7 +531,6 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { shadeInteractor, fragmentService, delayableExecutor, - featureFlags, notificationStackScrollLayoutController, ResourcesSplitShadeStateController(), largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt index e66251a030a3..c32635020ddc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt @@ -28,8 +28,6 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.fragments.FragmentService import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl @@ -91,7 +89,6 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { lateinit var underTest: NotificationsQSContainerController - private lateinit var featureFlags: FakeFeatureFlags private lateinit var navigationModeCallback: ModeChangedListener private lateinit var taskbarVisibilityCallback: OverviewProxyListener private lateinit var windowInsetsCallback: Consumer<WindowInsets> @@ -104,7 +101,6 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { fakeSystemClock = FakeSystemClock() delayableExecutor = FakeExecutor(fakeSystemClock) mSetFlagsRule.enableFlags(KeyguardShadeMigrationNssl.FLAG_NAME) - featureFlags = FakeFeatureFlags().apply { set(Flags.QS_CONTAINER_GRAPH_OPTIMIZER, true) } mContext.ensureTestableResources() whenever(view.context).thenReturn(mContext) whenever(view.resources).thenReturn(mContext.resources) @@ -122,7 +118,6 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { shadeInteractor, fragmentService, delayableExecutor, - featureFlags, notificationStackScrollLayoutController, ResourcesSplitShadeStateController(), largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } @@ -513,7 +508,6 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { shadeInteractor, fragmentService, delayableExecutor, - featureFlags, notificationStackScrollLayoutController, ResourcesSplitShadeStateController(), largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java index 3e0a647d464e..7bd9d92baf59 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java @@ -103,7 +103,6 @@ import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; -import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository; import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import com.android.systemui.user.domain.interactor.UserSwitcherInteractor; @@ -205,9 +204,7 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { mStatusBarStateController = mKosmos.getStatusBarStateController(); mInteractionJankMonitor = mKosmos.getInteractionJankMonitor(); - FakeDeviceProvisioningRepository deviceProvisioningRepository = - new FakeDeviceProvisioningRepository(); - deviceProvisioningRepository.setDeviceProvisioned(true); + mKosmos.getFakeDeviceProvisioningRepository().setDeviceProvisioned(true); FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic(); FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository(); @@ -294,7 +291,7 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { mShadeInteractor = new ShadeInteractorImpl( mTestScope.getBackgroundScope(), - deviceProvisioningRepository, + mKosmos.getDeviceProvisioningInteractor(), mDisableFlagsRepository, mDozeParameters, mKeyguardRepository, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt index 50349bea390f..0dd988d424b9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt @@ -82,4 +82,22 @@ class NotificationSettingsRepositoryTest : SysuiTestCase() { underTest.setShowNotificationsOnLockscreenEnabled(false) assertThat(showNotifs).isEqualTo(false) } + + @Test + fun testGetIsNotificationHistoryEnabled() = + testScope.runTest { + val historyEnabled by collectLastValue(underTest.isNotificationHistoryEnabled) + + secureSettingsRepository.setInt( + name = Settings.Secure.NOTIFICATION_HISTORY_ENABLED, + value = 1, + ) + assertThat(historyEnabled).isEqualTo(true) + + secureSettingsRepository.setInt( + name = Settings.Secure.NOTIFICATION_HISTORY_ENABLED, + value = 0, + ) + assertThat(historyEnabled).isEqualTo(false) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt index 8cb064dd39a6..545053711179 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt @@ -56,8 +56,8 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController -import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository +import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import kotlinx.coroutines.flow.emptyFlow @@ -192,7 +192,7 @@ class StatusBarStateControllerImplTest : SysuiTestCase() { shadeInteractor = ShadeInteractorImpl( testScope.backgroundScope, - FakeDeviceProvisioningRepository(), + kosmos.deviceProvisioningInteractor, FakeDisableFlagsRepository(), mock(), keyguardRepository, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt index 350ed2d9ff22..7d99d05e3f0b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt @@ -21,7 +21,7 @@ import android.platform.test.annotations.EnableFlags import android.service.notification.StatusBarNotification import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUpdateMonitor -import com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING +import com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING import com.android.systemui.SysuiTestCase import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.NotificationLockscreenUserManager diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java index 57dac3ac19a6..cac4a8d2d37f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java @@ -17,10 +17,13 @@ package com.android.systemui.statusbar.notification.footer.ui.view; import static com.android.systemui.log.LogAssertKt.assertLogsWtf; + import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; + import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; @@ -95,6 +98,7 @@ public class FooterViewTest extends SysuiTestCase { } @Test + @DisableFlags(FooterViewRefactor.FLAG_NAME) public void setHistoryShown() { mView.showHistory(true); assertTrue(mView.isHistoryShown()); @@ -103,6 +107,7 @@ public class FooterViewTest extends SysuiTestCase { } @Test + @DisableFlags(FooterViewRefactor.FLAG_NAME) public void setHistoryNotShown() { mView.showHistory(false); assertFalse(mView.isHistoryShown()); @@ -128,6 +133,62 @@ public class FooterViewTest extends SysuiTestCase { @Test @EnableFlags(FooterViewRefactor.FLAG_NAME) + public void testSetManageOrHistoryButtonText_resourceOnlyFetchedOnce() { + int resId = R.string.manage_notifications_history_text; + mView.setManageOrHistoryButtonText(resId); + verify(mSpyContext).getString(eq(resId)); + + clearInvocations(mSpyContext); + + assertThat(((TextView) mView.findViewById(R.id.manage_text)) + .getText().toString()).contains("History"); + + // Set it a few more times, it shouldn't lead to the resource being fetched again + mView.setManageOrHistoryButtonText(resId); + mView.setManageOrHistoryButtonText(resId); + + verify(mSpyContext, never()).getString(anyInt()); + } + + @Test + @DisableFlags(FooterViewRefactor.FLAG_NAME) + public void testSetManageOrHistoryButtonText_expectsFlagEnabled() { + clearInvocations(mSpyContext); + int resId = R.string.manage_notifications_history_text; + assertLogsWtf(() -> mView.setManageOrHistoryButtonText(resId)); + verify(mSpyContext, never()).getString(anyInt()); + } + + @Test + @EnableFlags(FooterViewRefactor.FLAG_NAME) + public void testSetManageOrHistoryButtonDescription_resourceOnlyFetchedOnce() { + int resId = R.string.manage_notifications_history_text; + mView.setManageOrHistoryButtonDescription(resId); + verify(mSpyContext).getString(eq(resId)); + + clearInvocations(mSpyContext); + + assertThat(((TextView) mView.findViewById(R.id.manage_text)) + .getContentDescription().toString()).contains("History"); + + // Set it a few more times, it shouldn't lead to the resource being fetched again + mView.setManageOrHistoryButtonDescription(resId); + mView.setManageOrHistoryButtonDescription(resId); + + verify(mSpyContext, never()).getString(anyInt()); + } + + @Test + @DisableFlags(FooterViewRefactor.FLAG_NAME) + public void testSetManageOrHistoryButtonDescription_expectsFlagEnabled() { + clearInvocations(mSpyContext); + int resId = R.string.accessibility_clear_all; + assertLogsWtf(() -> mView.setManageOrHistoryButtonDescription(resId)); + verify(mSpyContext, never()).getString(anyInt()); + } + + @Test + @EnableFlags(FooterViewRefactor.FLAG_NAME) public void testSetClearAllButtonText_resourceOnlyFetchedOnce() { int resId = R.string.clear_all_notifications_text; mView.setClearAllButtonText(resId); @@ -150,7 +211,7 @@ public class FooterViewTest extends SysuiTestCase { public void testSetClearAllButtonText_expectsFlagEnabled() { clearInvocations(mSpyContext); int resId = R.string.clear_all_notifications_text; - assertLogsWtf(()-> mView.setClearAllButtonText(resId)); + assertLogsWtf(() -> mView.setClearAllButtonText(resId)); verify(mSpyContext, never()).getString(anyInt()); } @@ -178,7 +239,7 @@ public class FooterViewTest extends SysuiTestCase { public void testSetClearAllButtonDescription_expectsFlagEnabled() { clearInvocations(mSpyContext); int resId = R.string.accessibility_clear_all; - assertLogsWtf(()-> mView.setClearAllButtonDescription(resId)); + assertLogsWtf(() -> mView.setClearAllButtonDescription(resId)); verify(mSpyContext, never()).getString(anyInt()); } @@ -206,7 +267,7 @@ public class FooterViewTest extends SysuiTestCase { public void testSetMessageString_expectsFlagEnabled() { clearInvocations(mSpyContext); int resId = R.string.unlock_to_see_notif_text; - assertLogsWtf(()-> mView.setMessageString(resId)); + assertLogsWtf(() -> mView.setMessageString(resId)); verify(mSpyContext, never()).getString(anyInt()); } @@ -231,7 +292,7 @@ public class FooterViewTest extends SysuiTestCase { public void testSetMessageIcon_expectsFlagEnabled() { clearInvocations(mSpyContext); int resId = R.drawable.ic_friction_lock_closed; - assertLogsWtf(()-> mView.setMessageIcon(resId)); + assertLogsWtf(() -> mView.setMessageIcon(resId)); verify(mSpyContext, never()).getDrawable(anyInt()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt index 8ab13f5aa720..620d97275949 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt @@ -14,109 +14,61 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.statusbar.notification.footer.ui.viewmodel import android.platform.test.annotations.EnableFlags +import android.provider.Settings import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest -import com.android.systemui.SysUITestComponent -import com.android.systemui.SysUITestModule import com.android.systemui.SysuiTestCase -import com.android.systemui.TestMocksModule -import com.android.systemui.collectLastValue -import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.flags.FakeFeatureFlagsClassicModule -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.shared.model.StatusBarState -import com.android.systemui.power.data.repository.FakePowerRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.data.repository.powerRepository import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessState -import com.android.systemui.runCurrent -import com.android.systemui.runTest -import com.android.systemui.shade.data.repository.FakeShadeRepository +import com.android.systemui.res.R +import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository import com.android.systemui.statusbar.notification.collection.render.NotifStats -import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor -import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule -import com.android.systemui.statusbar.phone.DozeParameters -import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule -import com.android.systemui.util.mockito.mock +import com.android.systemui.testKosmos import com.android.systemui.util.ui.isAnimating import com.android.systemui.util.ui.value import com.google.common.truth.Truth.assertThat -import dagger.BindsInstance -import dagger.Component -import java.util.Optional -import org.junit.Before +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith -import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest @EnableFlags(FooterViewRefactor.FLAG_NAME) class FooterViewModelTest : SysuiTestCase() { - private lateinit var footerViewModel: FooterViewModel - - @SysUISingleton - @Component( - modules = - [ - SysUITestModule::class, - ActivatableNotificationViewModelModule::class, - FooterViewModelModule::class, - HeadlessSystemUserModeModule::class, - ] - ) - interface TestComponent : SysUITestComponent<Optional<FooterViewModel>> { - val activeNotificationListRepository: ActiveNotificationListRepository - val configurationRepository: FakeConfigurationRepository - val keyguardRepository: FakeKeyguardRepository - val keyguardTransitionRepository: FakeKeyguardTransitionRepository - val shadeRepository: FakeShadeRepository - val powerRepository: FakePowerRepository - - @Component.Factory - interface Factory { - fun create( - @BindsInstance test: SysuiTestCase, - featureFlags: FakeFeatureFlagsClassicModule, - mocks: TestMocksModule, - ): TestComponent + private val kosmos = + testKosmos().apply { + fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) } } - } - - private val dozeParameters: DozeParameters = mock() - - private val testComponent: TestComponent = - DaggerFooterViewModelTest_TestComponent.factory() - .create( - test = this, - featureFlags = - FakeFeatureFlagsClassicModule { - set(com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER, true) - }, - mocks = - TestMocksModule( - dozeParameters = dozeParameters, - ) - ) + private val testScope = kosmos.testScope + private val activeNotificationListRepository = kosmos.activeNotificationListRepository + private val fakeKeyguardRepository = kosmos.fakeKeyguardRepository + private val shadeRepository = kosmos.shadeRepository + private val powerRepository = kosmos.powerRepository + private val fakeSecureSettingsRepository = kosmos.fakeSecureSettingsRepository - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - - // The underTest in the component is Optional, because that matches the provider we - // currently have for the footer view model. - footerViewModel = testComponent.underTest.get() - } + val underTest = kosmos.footerViewModel @Test fun testMessageVisible_whenFilteredNotifications() = - testComponent.runTest { - val visible by collectLastValue(footerViewModel.message.isVisible) + testScope.runTest { + val visible by collectLastValue(underTest.message.isVisible) activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true @@ -125,8 +77,8 @@ class FooterViewModelTest : SysuiTestCase() { @Test fun testMessageVisible_whenNoFilteredNotifications() = - testComponent.runTest { - val visible by collectLastValue(footerViewModel.message.isVisible) + testScope.runTest { + val visible by collectLastValue(underTest.message.isVisible) activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false @@ -135,8 +87,8 @@ class FooterViewModelTest : SysuiTestCase() { @Test fun testClearAllButtonVisible_whenHasClearableNotifs() = - testComponent.runTest { - val visible by collectLastValue(footerViewModel.clearAllButton.isVisible) + testScope.runTest { + val visible by collectLastValue(underTest.clearAllButton.isVisible) activeNotificationListRepository.notifStats.value = NotifStats( @@ -153,8 +105,8 @@ class FooterViewModelTest : SysuiTestCase() { @Test fun testClearAllButtonVisible_whenHasNoClearableNotifs() = - testComponent.runTest { - val visible by collectLastValue(footerViewModel.clearAllButton.isVisible) + testScope.runTest { + val visible by collectLastValue(underTest.clearAllButton.isVisible) activeNotificationListRepository.notifStats.value = NotifStats( @@ -171,12 +123,12 @@ class FooterViewModelTest : SysuiTestCase() { @Test fun testClearAllButtonAnimating_whenShadeExpandedAndTouchable() = - testComponent.runTest { - val visible by collectLastValue(footerViewModel.clearAllButton.isVisible) + testScope.runTest { + val visible by collectLastValue(underTest.clearAllButton.isVisible) runCurrent() // WHEN shade is expanded - keyguardRepository.setStatusBarState(StatusBarState.SHADE) + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) shadeRepository.setLegacyShadeExpansion(1f) // AND QS not expanded shadeRepository.setQsExpansion(0f) @@ -205,12 +157,12 @@ class FooterViewModelTest : SysuiTestCase() { @Test fun testClearAllButtonAnimating_whenShadeNotExpanded() = - testComponent.runTest { - val visible by collectLastValue(footerViewModel.clearAllButton.isVisible) + testScope.runTest { + val visible by collectLastValue(underTest.clearAllButton.isVisible) runCurrent() // WHEN shade is collapsed - keyguardRepository.setStatusBarState(StatusBarState.SHADE) + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) shadeRepository.setLegacyShadeExpansion(0f) // AND QS not expanded shadeRepository.setQsExpansion(0f) @@ -236,4 +188,30 @@ class FooterViewModelTest : SysuiTestCase() { // THEN button visibility should not animate assertThat(visible?.isAnimating).isFalse() } + + @Test + fun testManageButton_whenHistoryDisabled() = + testScope.runTest { + val buttonLabel by collectLastValue(underTest.manageOrHistoryButton.labelId) + runCurrent() + + // WHEN notification history is disabled + fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0) + + // THEN label is "Manage" + assertThat(buttonLabel).isEqualTo(R.string.manage_notifications_text) + } + + @Test + fun testHistoryButton_whenHistoryEnabled() = + testScope.runTest { + val buttonLabel by collectLastValue(underTest.manageOrHistoryButton.labelId) + runCurrent() + + // WHEN notification history is disabled + fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 1) + + // THEN label is "History" + assertThat(buttonLabel).isEqualTo(R.string.manage_notifications_history_text) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index dbe63f290407..7589a49e1963 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.notification.stack; -import static com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING; +import static com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING; import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt index 4188c5d34d98..88e4f5ab8d55 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt @@ -23,36 +23,27 @@ import android.platform.test.annotations.EnableFlags import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.SysUITestComponent -import com.android.systemui.SysUITestModule import com.android.systemui.SysuiTestCase -import com.android.systemui.TestMocksModule -import com.android.systemui.collectLastValue -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.flags.FakeFeatureFlagsClassicModule -import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testScope import com.android.systemui.res.R -import com.android.systemui.runCurrent -import com.android.systemui.runTest -import com.android.systemui.shade.data.repository.FakeShadeRepository -import com.android.systemui.statusbar.notification.dagger.NotificationStatsLoggerModule -import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository +import com.android.systemui.shade.data.repository.fakeShadeRepository +import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor -import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModelModule -import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule -import com.android.systemui.statusbar.policy.FakeConfigurationController -import com.android.systemui.statusbar.policy.data.repository.FakeZenModeRepository -import com.android.systemui.unfold.UnfoldTransitionModule -import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule +import com.android.systemui.statusbar.policy.data.repository.zenModeRepository +import com.android.systemui.statusbar.policy.fakeConfigurationController +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat -import dagger.BindsInstance -import dagger.Component import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -62,46 +53,18 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) @EnableFlags(FooterViewRefactor.FLAG_NAME) class NotificationListViewModelTest : SysuiTestCase() { - - @SysUISingleton - @Component( - modules = - [ - SysUITestModule::class, - ActivatableNotificationViewModelModule::class, - FooterViewModelModule::class, - HeadlessSystemUserModeModule::class, - UnfoldTransitionModule.Bindings::class, - NotificationStatsLoggerModule::class, - ] - ) - interface TestComponent : SysUITestComponent<NotificationListViewModel> { - val activeNotificationListRepository: ActiveNotificationListRepository - val keyguardTransitionRepository: FakeKeyguardTransitionRepository - val shadeRepository: FakeShadeRepository - val zenModeRepository: FakeZenModeRepository - val configurationController: FakeConfigurationController - - @Component.Factory - interface Factory { - fun create( - @BindsInstance test: SysuiTestCase, - featureFlags: FakeFeatureFlagsClassicModule, - mocks: TestMocksModule, - ): TestComponent + private val kosmos = + testKosmos().apply { + fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) } } - } + private val testScope = kosmos.testScope + private val activeNotificationListRepository = kosmos.activeNotificationListRepository + private val fakeKeyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + private val fakeShadeRepository = kosmos.fakeShadeRepository + private val zenModeRepository = kosmos.zenModeRepository + private val fakeConfigurationController = kosmos.fakeConfigurationController - private val testComponent: TestComponent = - DaggerNotificationListViewModelTest_TestComponent.factory() - .create( - test = this, - featureFlags = - FakeFeatureFlagsClassicModule { - set(com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER, true) - }, - mocks = TestMocksModule() - ) + val underTest = kosmos.notificationListViewModel @Before fun setUp() { @@ -110,11 +73,11 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testIsImportantForAccessibility_falseWhenNoNotifs() = - testComponent.runTest { + testScope.runTest { val important by collectLastValue(underTest.isImportantForAccessibility) // WHEN on lockscreen - keyguardTransitionRepository.sendTransitionSteps( + fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN, testScope, @@ -129,11 +92,11 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testIsImportantForAccessibility_trueWhenNotifs() = - testComponent.runTest { + testScope.runTest { val important by collectLastValue(underTest.isImportantForAccessibility) // WHEN on lockscreen - keyguardTransitionRepository.sendTransitionSteps( + fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN, testScope, @@ -148,11 +111,11 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testIsImportantForAccessibility_trueWhenNotKeyguard() = - testComponent.runTest { + testScope.runTest { val important by collectLastValue(underTest.isImportantForAccessibility) // WHEN not on lockscreen - keyguardTransitionRepository.sendTransitionSteps( + fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE, testScope, @@ -167,7 +130,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testShouldShowEmptyShadeView_trueWhenNoNotifs() = - testComponent.runTest { + testScope.runTest { val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has no notifs @@ -180,7 +143,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testShouldShowEmptyShadeView_falseWhenNotifs() = - testComponent.runTest { + testScope.runTest { val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has notifs @@ -193,13 +156,13 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testShouldShowEmptyShadeView_falseWhenQsExpandedDefault() = - testComponent.runTest { + testScope.runTest { val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) // AND quick settings are expanded - shadeRepository.legacyQsFullscreen.value = true + fakeShadeRepository.legacyQsFullscreen.value = true runCurrent() // THEN should not show @@ -208,16 +171,16 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testShouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() = - testComponent.runTest { + testScope.runTest { val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) // AND quick settings are expanded - shadeRepository.setQsExpansion(1f) + fakeShadeRepository.setQsExpansion(1f) // AND split shade is enabled overrideResource(R.bool.config_use_split_notification_shade, true) - configurationController.notifyConfigurationChanged() + fakeConfigurationController.notifyConfigurationChanged() runCurrent() // THEN should show @@ -226,13 +189,13 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testShouldShowEmptyShadeView_falseWhenTransitioningToAOD() = - testComponent.runTest { + testScope.runTest { val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) // AND transitioning to AOD - keyguardTransitionRepository.sendTransitionStep( + fakeKeyguardTransitionRepository.sendTransitionStep( TransitionStep( transitionState = TransitionState.STARTED, from = KeyguardState.LOCKSCREEN, @@ -248,13 +211,13 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testShouldShowEmptyShadeView_falseWhenBouncerShowing() = - testComponent.runTest { + testScope.runTest { val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) // AND is on bouncer - keyguardTransitionRepository.sendTransitionSteps( + fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.LOCKSCREEN, to = KeyguardState.PRIMARY_BOUNCER, testScope, @@ -267,7 +230,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testAreNotificationsHiddenInShade_true() = - testComponent.runTest { + testScope.runTest { val hidden by collectLastValue(underTest.areNotificationsHiddenInShade) zenModeRepository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST) @@ -279,7 +242,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testAreNotificationsHiddenInShade_false() = - testComponent.runTest { + testScope.runTest { val hidden by collectLastValue(underTest.areNotificationsHiddenInShade) zenModeRepository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST) @@ -291,7 +254,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testHasFilteredOutSeenNotifications_true() = - testComponent.runTest { + testScope.runTest { val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications) activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true @@ -302,7 +265,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testHasFilteredOutSeenNotifications_false() = - testComponent.runTest { + testScope.runTest { val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications) activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt index cd5d5ed0d08e..9919c6b78aab 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.policy +import android.app.ActivityOptions import android.app.Notification import android.media.projection.MediaProjectionInfo import android.media.projection.MediaProjectionManager @@ -23,7 +24,7 @@ import android.os.Handler import android.service.notification.StatusBarNotification import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest -import com.android.systemui.Flags +import com.android.server.notification.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.notification.collection.NotificationEntry import org.junit.Assert.assertFalse @@ -69,6 +70,8 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) mSetFlagsRule.enableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) + setShareFullScreen() + controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler) // Obtain useful MediaProjectionCallback @@ -195,6 +198,14 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { } @Test + fun isSensitiveStateActive_projectionActive_singleActivity_false() { + setShareSingleApp() + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + assertFalse(controller.isSensitiveStateActive) + } + + @Test fun shouldProtectNotification_projectionInactive_false() { val notificationEntry = mock(NotificationEntry::class.java) @@ -202,30 +213,74 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { } @Test - fun shouldProtectNotification_projectionActive_fgsNotification_false() { + fun shouldProtectNotification_projectionActive_singleActivity_false() { + setShareSingleApp() mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) - val notificationEntry = mock(NotificationEntry::class.java) - val sbn = mock(StatusBarNotification::class.java) - val notification = mock(Notification::class.java) - `when`(notificationEntry.sbn).thenReturn(sbn) - `when`(sbn.notification).thenReturn(notification) - `when`(notification.isFgsOrUij).thenReturn(true) + val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME) + + assertFalse(controller.shouldProtectNotification(notificationEntry)) + } + + @Test + fun shouldProtectNotification_projectionActive_fgsNotificationFromProjectionApp_false() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + val notificationEntry = setupFgsNotificationEntry(TEST_PROJECTION_PACKAGE_NAME) assertFalse(controller.shouldProtectNotification(notificationEntry)) } @Test + fun shouldProtectNotification_projectionActive_fgsNotificationNotFromProjectionApp_true() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + val notificationEntry = setupFgsNotificationEntry(TEST_PACKAGE_NAME) + + assertTrue(controller.shouldProtectNotification(notificationEntry)) + } + + @Test fun shouldProtectNotification_projectionActive_notFgsNotification_true() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + val notificationEntry = setupNotificationEntry(TEST_PROJECTION_PACKAGE_NAME) + + assertTrue(controller.shouldProtectNotification(notificationEntry)) + } + + private fun setShareFullScreen() { + `when`(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME) + `when`(mediaProjectionInfo.launchCookie).thenReturn(null) + } + + private fun setShareSingleApp() { + `when`(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME) + `when`(mediaProjectionInfo.launchCookie).thenReturn(ActivityOptions.LaunchCookie()) + } + + private fun setupNotificationEntry( + packageName: String, + isFgs: Boolean = false + ): NotificationEntry { val notificationEntry = mock(NotificationEntry::class.java) val sbn = mock(StatusBarNotification::class.java) val notification = mock(Notification::class.java) `when`(notificationEntry.sbn).thenReturn(sbn) + `when`(sbn.packageName).thenReturn(packageName) `when`(sbn.notification).thenReturn(notification) - `when`(notification.isFgsOrUij).thenReturn(false) + `when`(notification.isFgsOrUij).thenReturn(isFgs) - assertTrue(controller.shouldProtectNotification(notificationEntry)) + return notificationEntry + } + + private fun setupFgsNotificationEntry(packageName: String): NotificationEntry { + return setupNotificationEntry(packageName, /* isFgs= */ true) + } + + companion object { + private const val TEST_PROJECTION_PACKAGE_NAME = + "com.android.systemui.statusbar.policy.projectionpackage" + private const val TEST_PACKAGE_NAME = "com.android.systemui.statusbar.policy.testpackage" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 925ac2a2d067..9ea4142c9cff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -494,7 +494,7 @@ public class BubblesTest extends SysuiTestCase { mShadeInteractor = new ShadeInteractorImpl( mTestScope.getBackgroundScope(), - deviceProvisioningRepository, + mKosmos.getDeviceProvisioningInteractor(), new FakeDisableFlagsRepository(), mDozeParameters, keyguardRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryKosmos.kt index 961022f0f426..a4f28f395a63 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryKosmos.kt @@ -20,4 +20,4 @@ package com.android.systemui.biometrics.data.repository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -val Kosmos.biometricStatusRepository by Fixture { FakeBiometricStatusRepository() } +var Kosmos.biometricStatusRepository by Fixture { FakeBiometricStatusRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeBiometricStatusRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeBiometricStatusRepository.kt index 1c8bd3b58dfe..e9b7a69d1421 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeBiometricStatusRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeBiometricStatusRepository.kt @@ -18,9 +18,12 @@ package com.android.systemui.biometrics.data.repository import com.android.systemui.biometrics.shared.model.AuthenticationReason +import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull class FakeBiometricStatusRepository : BiometricStatusRepository { private val _fingerprintAuthenticationReason = @@ -28,7 +31,16 @@ class FakeBiometricStatusRepository : BiometricStatusRepository { override val fingerprintAuthenticationReason: StateFlow<AuthenticationReason> = _fingerprintAuthenticationReason.asStateFlow() + private val _fingerprintAcquiredStatus = + MutableStateFlow<FingerprintAuthenticationStatus?>(null) + override val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> = + _fingerprintAcquiredStatus.asStateFlow().filterNotNull() + fun setFingerprintAuthenticationReason(reason: AuthenticationReason) { _fingerprintAuthenticationReason.value = reason } + + fun setFingerprintAcquiredStatus(status: FingerprintAuthenticationStatus) { + _fingerprintAcquiredStatus.value = status + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt index 3ea3ccfb2909..1884a3264ed6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt @@ -28,7 +28,7 @@ class FakeCommunalMediaRepository : CommunalMediaRepository { fun mediaActive(timestamp: Long = 0L) { _mediaModel.value = CommunalMediaModel( - hasAnyMediaOrRecommendation = true, + hasActiveMediaOrRecommendation = true, createdTimestampMillis = timestamp, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index fab64e38e1f8..7301404eada0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -36,7 +36,7 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : } } - override fun deleteWidget(widgetId: Int) { + override fun deleteWidgetFromDb(widgetId: Int) { if (_communalWidgets.value.none { it.appWidgetId == widgetId }) { return } @@ -44,6 +44,10 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : _communalWidgets.value = _communalWidgets.value.filter { it.appWidgetId != widgetId } } + override fun deleteWidgetFromHost(widgetId: Int) { + deleteWidgetFromDb(widgetId) + } + private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) { _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority)) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index c818e9c8971c..c47f020a3b83 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -24,12 +24,15 @@ import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.smartspace.data.repository.smartspaceRepository import com.android.systemui.user.data.repository.userRepository import com.android.systemui.util.mockito.mock val Kosmos.communalInteractor by Fixture { CommunalInteractor( + applicationScope = applicationCoroutineScope, communalRepository = communalRepository, widgetRepository = communalWidgetRepository, mediaRepository = communalMediaRepository, @@ -39,6 +42,8 @@ val Kosmos.communalInteractor by Fixture { appWidgetHost = mock(), keyguardInteractor = keyguardInteractor, editWidgetsActivityStarter = editWidgetsActivityStarter, + logBuffer = logcatLogBuffer("CommunalInteractor"), + tableLogBuffer = mock(), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt index adaea7cbf64d..9776b436555d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.communal.data.repository.communalTutorialRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.util.mockito.mock val Kosmos.communalTutorialInteractor by Kosmos.Fixture { @@ -30,5 +31,6 @@ val Kosmos.communalTutorialInteractor by keyguardInteractor = keyguardInteractor, communalRepository = communalRepository, communalInteractor = communalInteractor, + tableLogBuffer = mock(), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryKosmos.kt new file mode 100644 index 000000000000..109e113611c6 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryKosmos.kt @@ -0,0 +1,27 @@ +/* + * 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.controls.panels + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.settings.fakeUserFileManager +import com.android.systemui.settings.fakeUserTracker + +var Kosmos.authorizedPanelsRepository: AuthorizedPanelsRepository by + Kosmos.Fixture { + AuthorizedPanelsRepositoryImpl(applicationContext, fakeUserFileManager, fakeUserTracker) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt deleted file mode 100644 index a231212518ec..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.controls.panels - -import android.os.UserHandle -import com.android.systemui.kosmos.Kosmos -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow - -class FakeSelectedComponentRepository : SelectedComponentRepository { - private var shouldAddDefaultPanel: Boolean = true - private val _selectedComponentFlows = - mutableMapOf<UserHandle, MutableStateFlow<SelectedComponentRepository.SelectedComponent?>>() - private var currentUserHandle: UserHandle = UserHandle.of(0) - - override fun selectedComponentFlow( - userHandle: UserHandle - ): Flow<SelectedComponentRepository.SelectedComponent?> { - // Return an existing flow for the user or create a new one - return _selectedComponentFlows.getOrPut(getUserHandle(userHandle)) { - MutableStateFlow(null) - } - } - - override fun getSelectedComponent( - userHandle: UserHandle - ): SelectedComponentRepository.SelectedComponent? { - return _selectedComponentFlows[getUserHandle(userHandle)]?.value - } - - override fun setSelectedComponent( - selectedComponent: SelectedComponentRepository.SelectedComponent - ) { - val flow = _selectedComponentFlows.getOrPut(currentUserHandle) { MutableStateFlow(null) } - flow.value = selectedComponent - } - - override fun removeSelectedComponent() { - _selectedComponentFlows[currentUserHandle]?.value = null - } - override fun shouldAddDefaultComponent(): Boolean = shouldAddDefaultPanel - - override fun setShouldAddDefaultComponent(shouldAdd: Boolean) { - shouldAddDefaultPanel = shouldAdd - } - - fun setCurrentUserHandle(userHandle: UserHandle) { - currentUserHandle = userHandle - } - private fun getUserHandle(userHandle: UserHandle): UserHandle { - return if (userHandle == UserHandle.CURRENT) { - currentUserHandle - } else { - userHandle - } - } -} - -val Kosmos.selectedComponentRepository by - Kosmos.Fixture<FakeSelectedComponentRepository> { FakeSelectedComponentRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/SelectedComponentRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/SelectedComponentRepositoryKosmos.kt new file mode 100644 index 000000000000..ee5b7e560329 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/SelectedComponentRepositoryKosmos.kt @@ -0,0 +1,27 @@ +/* + * 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.controls.panels + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.settings.fakeUserFileManager +import com.android.systemui.settings.fakeUserTracker + +var Kosmos.selectedComponentRepository: SelectedComponentRepository by + Kosmos.Fixture { + SelectedComponentRepositoryImpl(fakeUserFileManager, fakeUserTracker, testDispatcher) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 083de107c971..b9a3d383fc13 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -43,6 +43,8 @@ import com.android.systemui.scene.sceneContainerConfig import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.statusbar.phone.screenOffAnimationController import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository +import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor import com.android.systemui.util.time.systemClock import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -80,6 +82,8 @@ class KosmosJavaAdapter( val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor } val communalInteractor by lazy { kosmos.communalInteractor } val sceneContainerPlugin by lazy { kosmos.sceneContainerPlugin } + val deviceProvisioningInteractor by lazy { kosmos.deviceProvisioningInteractor } + val fakeDeviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository } init { kosmos.applicationContext = testCase.context diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/fontscaling/FontScalingTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/fontscaling/FontScalingTileKosmos.kt new file mode 100644 index 000000000000..9410ce6d657a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/fontscaling/FontScalingTileKosmos.kt @@ -0,0 +1,24 @@ +/* + * 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.tiles.impl.fontscaling + +import com.android.systemui.accessibility.qs.QSAccessibilityModule +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.qsEventLogger + +val Kosmos.qsFontScalingTileConfig by + Kosmos.Fixture { QSAccessibilityModule.provideFontScalingTileConfig(qsEventLogger) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserFileManager.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserFileManager.kt new file mode 100644 index 000000000000..207c3f7711b7 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserFileManager.kt @@ -0,0 +1,36 @@ +/* + * 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.settings + +import android.content.SharedPreferences +import com.android.systemui.util.FakeSharedPreferences +import java.io.File + +class FakeUserFileManager : UserFileManager { + private val sharedPreferences = mutableMapOf<SharedPrefKey, FakeSharedPreferences>() + + override fun getFile(fileName: String, userId: Int): File { + throw UnsupportedOperationException("getFile not implemented in fake") + } + + override fun getSharedPreferences(fileName: String, mode: Int, userId: Int): SharedPreferences { + val key = SharedPrefKey(fileName, mode, userId) + return sharedPreferences.getOrPut(key) { FakeSharedPreferences() } + } + + private data class SharedPrefKey(val fileName: String, val mode: Int, val userId: Int) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/UserFileManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/UserFileManagerKosmos.kt new file mode 100644 index 000000000000..4d7a40ab3380 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/UserFileManagerKosmos.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.settings + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.fakeUserFileManager by Kosmos.Fixture { FakeUserFileManager() } +var Kosmos.userFileManager: UserFileManager by Kosmos.Fixture { fakeUserFileManager } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt index e13fa5207b33..82e0b8e83f24 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt @@ -24,6 +24,7 @@ import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.log.LogBuffer import com.android.systemui.plugins.statusbar.statusBarStateController import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -44,6 +45,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.shadeControllerSceneImpl by Kosmos.Fixture { ShadeControllerSceneImpl( + mainDispatcher = testDispatcher, scope = applicationCoroutineScope, shadeInteractor = shadeInteractor, sceneInteractor = sceneInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt index afd37b3f92dc..2bd76bec6852 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt @@ -29,8 +29,8 @@ import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.statusbar.phone.dozeParameters -import com.android.systemui.statusbar.policy.data.repository.deviceProvisioningRepository import com.android.systemui.statusbar.policy.data.repository.userSetupRepository +import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor import com.android.systemui.user.domain.interactor.userSwitcherInteractor var Kosmos.baseShadeInteractor: BaseShadeInteractor by @@ -63,7 +63,7 @@ val Kosmos.shadeInteractorImpl by Kosmos.Fixture { ShadeInteractorImpl( scope = applicationCoroutineScope, - deviceProvisioningRepository = deviceProvisioningRepository, + deviceProvisioningInteractor = deviceProvisioningInteractor, disableFlagsRepository = disableFlagsRepository, dozeParams = dozeParameters, keyguardRepository = fakeKeyguardRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt index ff22ca00a0a6..01cac4c1e030 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt @@ -19,12 +19,14 @@ package com.android.systemui.statusbar.notification.footer.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shared.notifications.domain.interactor.notificationSettingsInteractor import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor val Kosmos.footerViewModel by Fixture { FooterViewModel( activeNotificationsInteractor = activeNotificationsInteractor, + notificationSettingsInteractor = notificationSettingsInteractor, seenNotificationsInteractor = seenNotificationsInteractor, shadeInteractor = shadeInteractor, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt index 748d04de1540..489598c4dba9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt @@ -23,6 +23,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher import com.android.systemui.statusbar.notification.icon.ui.viewbinder.notificationIconContainerShelfViewBinder +import com.android.systemui.statusbar.notification.notificationActivityStarter import com.android.systemui.statusbar.notification.stack.ui.view.notificationStatsLogger import com.android.systemui.statusbar.notification.stack.displaySwitchNotificationsHiderTracker import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel @@ -36,9 +37,10 @@ val Kosmos.notificationListViewBinder by Fixture { configuration = configurationState, falsingManager = falsingManager, iconAreaController = notificationIconAreaController, + loggerOptional = Optional.of(notificationStatsLogger), metricsLogger = metricsLogger, hiderTracker = displaySwitchNotificationsHiderTracker, nicBinder = notificationIconContainerShelfViewBinder, - loggerOptional = Optional.of(notificationStatsLogger), + notificationActivityStarter = { notificationActivityStarter }, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt index 33ed7e61de4d..d4e9bfbd1500 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt @@ -17,8 +17,8 @@ package com.android.systemui.statusbar.policy import com.android.systemui.kosmos.Kosmos -import com.android.systemui.util.mockito.mock -val Kosmos.configurationController by Kosmos.Fixture { mock<ConfigurationController>() } +var Kosmos.configurationController: ConfigurationController by + Kosmos.Fixture { fakeConfigurationController } val Kosmos.fakeConfigurationController: FakeConfigurationController by Kosmos.Fixture { FakeConfigurationController() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt index 300229954044..fc6a800e186c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt @@ -24,7 +24,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @SysUISingleton class FakeDeviceProvisioningRepository @Inject constructor() : DeviceProvisioningRepository { - private val _isDeviceProvisioned = MutableStateFlow(false) + private val _isDeviceProvisioned = MutableStateFlow(true) override val isDeviceProvisioned: Flow<Boolean> = _isDeviceProvisioned private val _isFactoryResetProtectionActive = MutableStateFlow(false) override val isFactoryResetProtectionActive: Flow<Boolean> = _isFactoryResetProtectionActive diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractorKosmos.kt new file mode 100644 index 000000000000..84bd3e8ff08d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractorKosmos.kt @@ -0,0 +1,27 @@ +/* + * 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.statusbar.policy.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.statusbar.policy.data.repository.deviceProvisioningRepository + +val Kosmos.deviceProvisioningInteractor by Fixture { + DeviceProvisioningInteractor( + repository = deviceProvisioningRepository, + ) +} diff --git a/ravenwood/README-ravenwood+mockito.md b/ravenwood/README-ravenwood+mockito.md deleted file mode 100644 index 6adb61441bb1..000000000000 --- a/ravenwood/README-ravenwood+mockito.md +++ /dev/null @@ -1,24 +0,0 @@ -# Ravenwood and Mockito - -Last update: 2023-11-13 - -- As of 2023-11-13, `external/mockito` is based on version 2.x. -- Mockito didn't support static mocking before 3.4.0. - See: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#48 - -- Latest Mockito is 5.*. According to https://github.com/mockito/mockito: - `Mockito 3 does not introduce any breaking API changes, but now requires Java 8 over Java 6 for Mockito 2. Mockito 4 removes deprecated API. Mockito 5 switches the default mockmaker to mockito-inline, and now requires Java 11.` - -- Mockito now supports Android natively. - See: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.1 - - But it's unclear at this point to omakoto@ how the `mockito-android` module is built. - -- Potential plan: - - Ideal option: - - If we can update `external/mockito`, that'd be great, but it may not work because - Mockito has removed the deprecated APIs. - - Second option: - - Import the latest mockito as `external/mockito-new`, and require ravenwood - to use this one. - - The latest mockito needs be exposed to all of 1) device tests, 2) host tests, and 3) ravenwood tests. - - This probably will require the latest `bytebuddy` and `objenesis`.
\ No newline at end of file diff --git a/ravenwood/bulk_enable.py b/ravenwood/bulk_enable.py index 36d398cc160c..83fda9e72c3c 100644 --- a/ravenwood/bulk_enable.py +++ b/ravenwood/bulk_enable.py @@ -34,6 +34,8 @@ import sys re_result = re.compile("I/ModuleListener.+?null-device-0 (.+?)#(.+?) ([A-Z_]+)(.*)$") +DRY_RUN = "-n" in sys.argv + ANNOTATION = "@android.platform.test.annotations.EnabledOnRavenwood" SED_ARG = "s/^((public )?class )/%s\\n\\1/g" % (ANNOTATION) @@ -46,7 +48,7 @@ stats_total = collections.defaultdict(int) stats_class = collections.defaultdict(lambda: collections.defaultdict(int)) stats_method = collections.defaultdict() -with open(sys.argv[1]) as f: +with open(sys.argv[-1]) as f: for line in f.readlines(): result = re_result.search(line) if result: @@ -67,7 +69,7 @@ for clazz in stats_class.keys(): clazz_match = re.compile("%s\.(kt|java)" % (clazz.split(".")[-1])) for root, dirs, files in os.walk("."): for f in files: - if clazz_match.match(f): + if clazz_match.match(f) and not DRY_RUN: path = os.path.join(root, f) subprocess.run(["sed", "-i", "-E", SED_ARG, path]) diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index 3670459c0300..8af561fbd273 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -16,7 +16,9 @@ package android.platform.test.ravenwood; +import android.app.ActivityManager; import android.app.Instrumentation; +import android.os.Build; import android.os.Bundle; import android.os.HandlerThread; import android.os.Looper; @@ -30,10 +32,12 @@ import org.junit.runner.Description; import java.io.PrintStream; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; public class RavenwoodRuleImpl { private static final String MAIN_THREAD_NAME = "RavenwoodMain"; @@ -50,11 +54,27 @@ public class RavenwoodRuleImpl { private static ScheduledFuture<?> sPendingTimeout; + /** + * When set, an unhandled exception was discovered (typically on a background thread), and we + * capture it here to ensure it's reported as a test failure. + */ + private static final AtomicReference<Throwable> sPendingUncaughtException = + new AtomicReference<>(); + + private static final Thread.UncaughtExceptionHandler sUncaughtExceptionHandler = + (thread, throwable) -> { + // Remember the first exception we discover + sPendingUncaughtException.compareAndSet(null, throwable); + }; + public static boolean isOnRavenwood() { return true; } public static void init(RavenwoodRule rule) { + maybeThrowPendingUncaughtException(false); + Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler); + RuntimeInit.redirectLogStreams(); android.os.Process.init$ravenwood(rule.mUid, rule.mPid); @@ -64,6 +84,8 @@ public class RavenwoodRuleImpl { rule.mSystemProperties.getKeyReadablePredicate(), rule.mSystemProperties.getKeyWritablePredicate()); + ActivityManager.init$ravenwood(rule.mCurrentUser); + com.android.server.LocalServices.removeAllServicesForTest(); if (rule.mProvideMainThread) { @@ -78,6 +100,10 @@ public class RavenwoodRuleImpl { sPendingTimeout = sTimeoutExecutor.schedule(RavenwoodRuleImpl::dumpStacks, TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); } + + // Touch some references early to ensure they're <clinit>'ed + Objects.requireNonNull(Build.IS_USERDEBUG); + Objects.requireNonNull(Build.VERSION.SDK); } public static void reset(RavenwoodRule rule) { @@ -94,9 +120,13 @@ public class RavenwoodRuleImpl { com.android.server.LocalServices.removeAllServicesForTest(); + ActivityManager.reset$ravenwood(); + android.os.SystemProperties.reset$ravenwood(); android.os.Binder.reset$ravenwood(); android.os.Process.reset$ravenwood(); + + maybeThrowPendingUncaughtException(true); } public static void logTestRunner(String label, Description description) { @@ -120,4 +150,21 @@ public class RavenwoodRuleImpl { } out.println("-----END ALL THREAD STACKS-----"); } + + /** + * If there's a pending uncaught exception, consume and throw it now. Typically used to + * report an exception on a background thread as a failure for the currently running test. + */ + private static void maybeThrowPendingUncaughtException(boolean duringReset) { + final Throwable pending = sPendingUncaughtException.getAndSet(null); + if (pending != null) { + if (duringReset) { + throw new IllegalStateException( + "Found an uncaught exception during this test", pending); + } else { + throw new IllegalStateException( + "Found an uncaught exception before this test started", pending); + } + } + } } diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index 0285b386ed13..daed457fb695 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -16,6 +16,10 @@ package android.platform.test.ravenwood; +import static android.os.Process.FIRST_APPLICATION_UID; +import static android.os.Process.SYSTEM_UID; +import static android.os.UserHandle.USER_SYSTEM; + import static org.junit.Assert.fail; import android.platform.test.annotations.DisabledOnRavenwood; @@ -94,12 +98,12 @@ public class RavenwoodRule implements TestRule { } } - private static final int SYSTEM_UID = 1000; private static final int NOBODY_UID = 9999; - private static final int FIRST_APPLICATION_UID = 10000; private static final AtomicInteger sNextPid = new AtomicInteger(100); + int mCurrentUser = USER_SYSTEM; + /** * Unless the test author requests differently, run as "nobody", and give each collection of * tests its own unique PID. diff --git a/ravenwood/list-ravenwood-tests.sh b/ravenwood/list-ravenwood-tests.sh new file mode 100755 index 000000000000..fb9b823ee93b --- /dev/null +++ b/ravenwood/list-ravenwood-tests.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# 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. + +# List all the ravenwood test modules. + +jq -r 'to_entries[] | select( .value.compatibility_suites | index("ravenwood-tests") ) | .key' "$OUT/module-info.json" diff --git a/ravenwood/mockito/Android.bp b/ravenwood/mockito/Android.bp index 4135022d2bc9..a74bca47f692 100644 --- a/ravenwood/mockito/Android.bp +++ b/ravenwood/mockito/Android.bp @@ -7,16 +7,6 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -// Ravenwood tests run on the hostside, so we need mockit of the host variant. -// But we need to use it in modules of the android variant, so we "wash" the variant with it. -java_host_for_device { - name: "mockito_ravenwood", - libs: [ - "mockito", - "objenesis", - ], -} - android_ravenwood_test { name: "RavenwoodMockitoTest", @@ -26,8 +16,6 @@ android_ravenwood_test { static_libs: [ "junit", "truth", - - "mockito_ravenwood", ], libs: [ "android.test.mock", diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java index 364a86a0ec5f..1284d64b9a90 100644 --- a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java +++ b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.Intent; +import android.os.Parcel; import android.platform.test.ravenwood.RavenwoodRule; import org.junit.Rule; @@ -78,4 +79,13 @@ public class RavenwoodMockitoTest { assertThat(object.getPackageName()).isEqualTo("android"); } + + @Test + public void testMockFinalClass() { + var object = mock(Parcel.class); + + when(object.readInt()).thenReturn(123); + + assertThat(object.readInt()).isEqualTo(123); + } } diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt index e49b64ebdbef..a5ecd20b8b4f 100644 --- a/ravenwood/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/ravenwood-annotation-allowed-classes.txt @@ -82,6 +82,8 @@ android.os.UidBatteryConsumer android.os.UidBatteryConsumer$Builder android.os.UserHandle android.os.UserManager +android.os.VibrationAttributes +android.os.VibrationAttributes$Builder android.os.WorkSource android.content.ClipData @@ -144,6 +146,7 @@ android.graphics.RectF android.content.ContentProvider +android.app.ActivityManager android.app.Instrumentation android.metrics.LogMaker diff --git a/ravenwood/run-ravenwood-tests.sh b/ravenwood/run-ravenwood-tests.sh new file mode 100755 index 000000000000..3f4b8a79e864 --- /dev/null +++ b/ravenwood/run-ravenwood-tests.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# 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. + +# Run all the ravenwood tests. + +# "echo" is to remove the newlines +all_tests=$(echo $(${0%/*}/list-ravenwood-tests.sh) ) + +echo "Running tests: $all_tests" +atest $all_tests diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 63784ba61150..44144f84f4e9 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -5725,10 +5725,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } @Override - public void attachAccessibilityOverlayToDisplay_enforcePermission( + public void attachAccessibilityOverlayToDisplay( int displayId, SurfaceControl sc) { mContext.enforceCallingPermission( - INTERNAL_SYSTEM_WINDOW, "attachAccessibilityOverlayToDisplay_enforcePermission"); + INTERNAL_SYSTEM_WINDOW, "attachAccessibilityOverlayToDisplay"); mMainHandler.sendMessage( obtainMessage( AccessibilityManagerService::attachAccessibilityOverlayToDisplayInternal, diff --git a/services/autofill/features.aconfig b/services/autofill/features.aconfig index 727721d5319f..532db126bff2 100644 --- a/services/autofill/features.aconfig +++ b/services/autofill/features.aconfig @@ -12,4 +12,11 @@ flag { namespace: "autofill" description: "Guards against Autofill-Credman integration phase 2" bug: "320730001" -}
\ No newline at end of file +} + +flag { + name: "autofill_credman_dev_integration" + namespace: "autofill" + description: "Guards against Autofill-Credman Phase1 developer integration via new APIs" + bug: "320730001" +} diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index cd8be338a031..82d9377031bc 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -17,6 +17,7 @@ package com.android.server; import static android.app.Flags.modesApi; +import static android.app.Flags.enableNightModeCache; import static android.app.UiModeManager.ContrastUtils.CONTRAST_DEFAULT_VALUE; import static android.app.UiModeManager.DEFAULT_PRIORITY; import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; @@ -135,7 +136,23 @@ final class UiModeManagerService extends SystemService { private int mDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED; private int mLastBroadcastState = Intent.EXTRA_DOCK_STATE_UNDOCKED; - private int mNightMode = UiModeManager.MODE_NIGHT_NO; + + private final NightMode mNightMode = new NightMode(){ + private int mNightModeValue = UiModeManager.MODE_NIGHT_NO; + + @Override + public int get() { + return mNightModeValue; + } + + @Override + public void set(int mode) { + mNightModeValue = mode; + if (enableNightModeCache()) { + UiModeManager.invalidateNightModeCache(); + } + } + }; private int mNightModeCustomType = UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN; private int mAttentionModeThemeOverlay = UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; private final LocalTime DEFAULT_CUSTOM_NIGHT_START_TIME = LocalTime.of(22, 0); @@ -297,7 +314,7 @@ final class UiModeManagerService extends SystemService { @Override public void onTwilightStateChanged(@Nullable TwilightState state) { synchronized (mLock) { - if (mNightMode == UiModeManager.MODE_NIGHT_AUTO && mSystemReady) { + if (mNightMode.get() == UiModeManager.MODE_NIGHT_AUTO && mSystemReady) { if (shouldApplyAutomaticChangesImmediately()) { updateLocked(0, 0); } else { @@ -392,7 +409,7 @@ final class UiModeManagerService extends SystemService { private void updateSystemProperties() { int mode = Secure.getIntForUser(getContext().getContentResolver(), Secure.UI_NIGHT_MODE, - mNightMode, 0); + mNightMode.get(), 0); if (mode == MODE_NIGHT_AUTO || mode == MODE_NIGHT_CUSTOM) { mode = MODE_NIGHT_YES; } @@ -412,7 +429,7 @@ final class UiModeManagerService extends SystemService { @Override public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) { mCurrentUser = to.getUserIdentifier(); - if (mNightMode == MODE_NIGHT_AUTO) persistComputedNightMode(from.getUserIdentifier()); + if (mNightMode.get() == MODE_NIGHT_AUTO) persistComputedNightMode(from.getUserIdentifier()); getContext().getContentResolver().unregisterContentObserver(mSetupWizardObserver); verifySetupWizardCompleted(); synchronized (mLock) { @@ -471,12 +488,12 @@ final class UiModeManagerService extends SystemService { verifySetupWizardCompleted(); final Resources res = context.getResources(); + mNightMode.set(res.getInteger( + com.android.internal.R.integer.config_defaultNightMode)); mStartDreamImmediatelyOnDock = res.getBoolean( com.android.internal.R.bool.config_startDreamImmediatelyOnDock); mDreamsDisabledByAmbientModeSuppression = res.getBoolean( com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig); - mNightMode = res.getInteger( - com.android.internal.R.integer.config_defaultNightMode); mDefaultUiModeType = res.getInteger( com.android.internal.R.integer.config_defaultUiModeType); mCarModeKeepsScreenOn = (res.getInteger( @@ -510,7 +527,7 @@ final class UiModeManagerService extends SystemService { private final BroadcastReceiver mOnShutdown = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (mNightMode == MODE_NIGHT_AUTO) { + if (mNightMode.get() == MODE_NIGHT_AUTO) { persistComputedNightMode(mCurrentUser); } } @@ -585,7 +602,7 @@ final class UiModeManagerService extends SystemService { } private void updateCustomTimeLocked() { - if (mNightMode != MODE_NIGHT_CUSTOM) return; + if (mNightMode.get() != MODE_NIGHT_CUSTOM) return; if (shouldApplyAutomaticChangesImmediately()) { updateLocked(0, 0); } else { @@ -606,9 +623,9 @@ final class UiModeManagerService extends SystemService { return; } if (mSetupWizardComplete) { - mNightMode = Secure.getIntForUser(context.getContentResolver(), + mNightMode.set(Secure.getIntForUser(context.getContentResolver(), Secure.UI_NIGHT_MODE, res.getInteger( - com.android.internal.R.integer.config_defaultNightMode), userId); + com.android.internal.R.integer.config_defaultNightMode), userId)); mNightModeCustomType = Secure.getIntForUser(context.getContentResolver(), Secure.UI_NIGHT_MODE_CUSTOM_TYPE, MODE_NIGHT_CUSTOM_TYPE_UNKNOWN, userId); mOverrideNightModeOn = Secure.getIntForUser(context.getContentResolver(), @@ -623,7 +640,7 @@ final class UiModeManagerService extends SystemService { Secure.getLongForUser(context.getContentResolver(), Secure.DARK_THEME_CUSTOM_END_TIME, DEFAULT_CUSTOM_NIGHT_END_TIME.toNanoOfDay() / 1000L, userId) * 1000); - if (mNightMode == MODE_NIGHT_AUTO) { + if (mNightMode.get() == MODE_NIGHT_AUTO) { mComputedNightMode = Secure.getIntForUser(context.getContentResolver(), Secure.UI_NIGHT_MODE_LAST_COMPUTED, 0, userId) != 0; } @@ -834,21 +851,23 @@ final class UiModeManagerService extends SystemService { final long ident = Binder.clearCallingIdentity(); try { synchronized (mLock) { - if (mNightMode != mode || mNightModeCustomType != customModeType) { - if (mNightMode == MODE_NIGHT_AUTO || mNightMode == MODE_NIGHT_CUSTOM) { + if (mNightMode.get() != mode || mNightModeCustomType != customModeType) { + if (mNightMode.get() == MODE_NIGHT_AUTO + || mNightMode.get() == MODE_NIGHT_CUSTOM) { unregisterDeviceInactiveListenerLocked(); cancelCustomAlarm(); } mNightModeCustomType = mode == MODE_NIGHT_CUSTOM ? customModeType : MODE_NIGHT_CUSTOM_TYPE_UNKNOWN; - mNightMode = mode; + mNightMode.set(mode); //deactivates AttentionMode if user toggles DarkTheme mAttentionModeThemeOverlay = MODE_ATTENTION_THEME_OVERLAY_OFF; resetNightModeOverrideLocked(); persistNightMode(user); // on screen off will update configuration instead - if ((mNightMode != MODE_NIGHT_AUTO && mNightMode != MODE_NIGHT_CUSTOM) + if ((mNightMode.get() != MODE_NIGHT_AUTO + && mNightMode.get() != MODE_NIGHT_CUSTOM) || shouldApplyAutomaticChangesImmediately()) { unregisterDeviceInactiveListenerLocked(); updateLocked(0, 0); @@ -865,7 +884,7 @@ final class UiModeManagerService extends SystemService { @Override public int getNightMode() { synchronized (mLock) { - return mNightMode; + return mNightMode.get(); } } @@ -999,18 +1018,19 @@ final class UiModeManagerService extends SystemService { synchronized (mLock) { final long ident = Binder.clearCallingIdentity(); try { - if (mNightMode == MODE_NIGHT_AUTO || mNightMode == MODE_NIGHT_CUSTOM) { + if (mNightMode.get() == MODE_NIGHT_AUTO + || mNightMode.get() == MODE_NIGHT_CUSTOM) { unregisterDeviceInactiveListenerLocked(); mOverrideNightModeOff = !active; mOverrideNightModeOn = active; mOverrideNightModeUser = mCurrentUser; persistNightModeOverrides(mCurrentUser); - } else if (mNightMode == UiModeManager.MODE_NIGHT_NO + } else if (mNightMode.get() == UiModeManager.MODE_NIGHT_NO && active) { - mNightMode = UiModeManager.MODE_NIGHT_YES; - } else if (mNightMode == UiModeManager.MODE_NIGHT_YES + mNightMode.set(UiModeManager.MODE_NIGHT_YES); + } else if (mNightMode.get() == UiModeManager.MODE_NIGHT_YES && !active) { - mNightMode = UiModeManager.MODE_NIGHT_NO; + mNightMode.set(UiModeManager.MODE_NIGHT_NO); } updateConfigurationLocked(); applyConfigurationExternallyLocked(); @@ -1414,7 +1434,7 @@ final class UiModeManagerService extends SystemService { private void onCustomTimeUpdated(int user) { persistNightMode(user); - if (mNightMode != MODE_NIGHT_CUSTOM) return; + if (mNightMode.get() != MODE_NIGHT_CUSTOM) return; if (shouldApplyAutomaticChangesImmediately()) { unregisterDeviceInactiveListenerLocked(); updateLocked(0, 0); @@ -1431,8 +1451,8 @@ final class UiModeManagerService extends SystemService { pw.print(" mStartDreamImmediatelyOnDock="); pw.print(mStartDreamImmediatelyOnDock); - pw.print(" mNightMode="); pw.print(mNightMode); pw.print(" ("); - pw.print(Shell.nightModeToStr(mNightMode, mNightModeCustomType)); pw.print(") "); + pw.print(" mNightMode="); pw.print(mNightMode.get()); pw.print(" ("); + pw.print(Shell.nightModeToStr(mNightMode.get(), mNightModeCustomType)); pw.print(") "); pw.print(" mOverrideOn/Off="); pw.print(mOverrideNightModeOn); pw.print("/"); pw.print(mOverrideNightModeOff); pw.print(" mAttentionModeThemeOverlay="); pw.print(mAttentionModeThemeOverlay); @@ -1623,7 +1643,7 @@ final class UiModeManagerService extends SystemService { // Only persist setting if not in car mode if (mCarModeEnabled || mCar) return; Secure.putIntForUser(getContext().getContentResolver(), - Secure.UI_NIGHT_MODE, mNightMode, user); + Secure.UI_NIGHT_MODE, mNightMode.get(), user); Secure.putLongForUser(getContext().getContentResolver(), Secure.UI_NIGHT_MODE_CUSTOM_TYPE, mNightModeCustomType, user); Secure.putLongForUser(getContext().getContentResolver(), @@ -1659,11 +1679,11 @@ final class UiModeManagerService extends SystemService { uiMode = Configuration.UI_MODE_TYPE_VR_HEADSET; } - if (mNightMode == MODE_NIGHT_YES || mNightMode == UiModeManager.MODE_NIGHT_NO) { - updateComputedNightModeLocked(mNightMode == MODE_NIGHT_YES); + if (mNightMode.get() == MODE_NIGHT_YES || mNightMode.get() == UiModeManager.MODE_NIGHT_NO) { + updateComputedNightModeLocked(mNightMode.get() == MODE_NIGHT_YES); } - if (mNightMode == MODE_NIGHT_AUTO) { + if (mNightMode.get() == MODE_NIGHT_AUTO) { boolean activateNightMode = mComputedNightMode; if (mTwilightManager != null) { mTwilightManager.registerListener(mTwilightListener, mHandler); @@ -1677,7 +1697,7 @@ final class UiModeManagerService extends SystemService { } } - if (mNightMode == MODE_NIGHT_CUSTOM) { + if (mNightMode.get() == MODE_NIGHT_CUSTOM) { if (mNightModeCustomType == MODE_NIGHT_CUSTOM_TYPE_BEDTIME) { updateComputedNightModeLocked(mLastBedtimeRequestedNightMode); } else { @@ -2010,7 +2030,7 @@ final class UiModeManagerService extends SystemService { private void updateComputedNightModeLocked(boolean activate) { boolean newComputedValue = activate; - if (mNightMode != MODE_NIGHT_YES && mNightMode != UiModeManager.MODE_NIGHT_NO) { + if (mNightMode.get() != MODE_NIGHT_YES && mNightMode.get() != UiModeManager.MODE_NIGHT_NO) { if (mOverrideNightModeOn && !newComputedValue) { newComputedValue = true; } else if (mOverrideNightModeOff && newComputedValue) { @@ -2029,7 +2049,7 @@ final class UiModeManagerService extends SystemService { mComputedNightMode = newComputedValue; } - if (mNightMode != MODE_NIGHT_AUTO || (mTwilightManager != null + if (mNightMode.get() != MODE_NIGHT_AUTO || (mTwilightManager != null && mTwilightManager.getLastTwilightState() != null)) { resetNightModeOverrideLocked(); } @@ -2279,4 +2299,13 @@ final class UiModeManagerService extends SystemService { Sandman.startDreamWhenDockedIfAppropriate(context); } } + + /** + * Interface to contain the value for system night mode. We make the night mode accessible + * through this class to ensure that the reassignment of this value invalidates the cache. + */ + private interface NightMode { + int get(); + void set(int mode); + } } diff --git a/services/core/java/com/android/server/am/AssistDataRequester.java b/services/core/java/com/android/server/am/AssistDataRequester.java index 98129ed12afe..856a15f0ecda 100644 --- a/services/core/java/com/android/server/am/AssistDataRequester.java +++ b/services/core/java/com/android/server/am/AssistDataRequester.java @@ -222,7 +222,7 @@ public class AssistDataRequester extends IAssistDataReceiver.Stub { // Ensure that the current activity supports assist data boolean isAssistDataAllowed = false; try { - isAssistDataAllowed = mActivityTaskManager.isAssistDataAllowedOnCurrentActivity(); + isAssistDataAllowed = mActivityTaskManager.isAssistDataAllowed(); } catch (RemoteException e) { // Should never happen } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index cd295b521e89..f1eea728bedd 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -2820,6 +2820,10 @@ public class AudioDeviceBroker { return mDeviceInventory.isBluetoothAudioDeviceCategoryFixed(address); } + /*package*/ boolean isSADevice(AdiDeviceState deviceState) { + return mAudioService.isSADevice(deviceState); + } + //------------------------------------------------ // for testing purposes only void clearDeviceInventory() { diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 690c37a9349a..102a960d35f6 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -307,9 +307,12 @@ public class AudioDeviceInventory { && di.mPeerDeviceAddress.equals(ads2.getDeviceAddress()))) { continue; } - ads2.setHasHeadTracker(updatedDevice.hasHeadTracker()); - ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled()); - ads2.setSAEnabled(updatedDevice.isSAEnabled()); + if (mDeviceBroker.isSADevice(updatedDevice) + == mDeviceBroker.isSADevice(ads2)) { + ads2.setHasHeadTracker(updatedDevice.hasHeadTracker()); + ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled()); + ads2.setSAEnabled(updatedDevice.isSAEnabled()); + } ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory()); mDeviceBroker.postUpdatedAdiDeviceState(ads2); @@ -325,9 +328,12 @@ public class AudioDeviceInventory { && di.mDeviceAddress.equals(ads2.getDeviceAddress()))) { continue; } - ads2.setHasHeadTracker(updatedDevice.hasHeadTracker()); - ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled()); - ads2.setSAEnabled(updatedDevice.isSAEnabled()); + if (mDeviceBroker.isSADevice(updatedDevice) + == mDeviceBroker.isSADevice(ads2)) { + ads2.setHasHeadTracker(updatedDevice.hasHeadTracker()); + ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled()); + ads2.setSAEnabled(updatedDevice.isSAEnabled()); + } ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory()); mDeviceBroker.postUpdatedAdiDeviceState(ads2); @@ -348,10 +354,11 @@ public class AudioDeviceInventory { || !updatedDevice.getDeviceAddress().equals(ads.getDeviceAddress())) { continue; } - - ads.setHasHeadTracker(updatedDevice.hasHeadTracker()); - ads.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled()); - ads.setSAEnabled(updatedDevice.isSAEnabled()); + if (mDeviceBroker.isSADevice(updatedDevice) == mDeviceBroker.isSADevice(ads)) { + ads.setHasHeadTracker(updatedDevice.hasHeadTracker()); + ads.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled()); + ads.setSAEnabled(updatedDevice.isSAEnabled()); + } ads.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory()); mDeviceBroker.postUpdatedAdiDeviceState(ads); diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 9f7c07e0af35..bbbba26303a7 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -7855,7 +7855,7 @@ public class AudioService extends IAudioService.Stub sDeviceLogger.enqueue(new EventLogger.StringEvent("BlutoothActiveDeviceChanged for " + BluetoothProfile.getProfileName(profile) + ", device update " + previousDevice - + " -> " + newDevice)); + + " -> " + newDevice).printLog(TAG)); AudioDeviceBroker.BtDeviceChangedData data = new AudioDeviceBroker.BtDeviceChangedData(newDevice, previousDevice, info, "AudioService"); @@ -10638,6 +10638,10 @@ public class AudioService extends IAudioService.Stub mSpatializerHelper.setFeatureEnabled(mHasSpatializerEffect); } + /*package*/ boolean isSADevice(AdiDeviceState deviceState) { + return mSpatializerHelper.isSADevice(deviceState); + } + private boolean isBluetoothPrividged() { if (!bluetoothMacAddressAnonymization()) { return true; diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 8428f127e77d..8d767313d5ab 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -813,7 +813,7 @@ public class SpatializerHelper { return false; } - private boolean isSADevice(AdiDeviceState deviceState) { + /*package*/ boolean isSADevice(AdiDeviceState deviceState) { return deviceState.getDeviceType() == getCanonicalDeviceType(deviceState.getDeviceType(), deviceState.getInternalDeviceType()) && isDeviceCompatibleWithSpatializationModes( deviceState.getAudioDeviceAttributes()); diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java index 1ae4d6465c57..1dc882e5ed2b 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java @@ -18,6 +18,8 @@ package com.android.server.biometrics.sensors; import android.annotation.NonNull; import android.hardware.biometrics.AuthenticationStateListener; +import android.hardware.biometrics.BiometricFingerprintConstants; +import android.hardware.biometrics.BiometricSourceType; import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; @@ -115,7 +117,7 @@ public class AuthenticationStateListeners implements IBinder.DeathRecipient { * @param userId The user Id for the requested authentication */ public void onAuthenticationFailed(int requestReason, int userId) { - for (AuthenticationStateListener listener: mAuthenticationStateListeners) { + for (AuthenticationStateListener listener : mAuthenticationStateListeners) { try { listener.onAuthenticationFailed(requestReason, userId); } catch (RemoteException e) { @@ -125,6 +127,27 @@ public class AuthenticationStateListeners implements IBinder.DeathRecipient { } } + /** + * Defines behavior in response to biometric being acquired. + * @param biometricSourceType identifies [BiometricSourceType] biometric was acquired for + * @param requestReason reason from [BiometricRequestConstants.RequestReason] for authentication + * @param acquiredInfo [BiometricFingerprintConstants.FingerprintAcquired] int corresponding to + * a known acquired message. + */ + public void onAuthenticationAcquired( + BiometricSourceType biometricSourceType, int requestReason, + @BiometricFingerprintConstants.FingerprintAcquired int acquiredInfo + ) { + for (AuthenticationStateListener listener: mAuthenticationStateListeners) { + try { + listener.onAuthenticationAcquired(biometricSourceType, requestReason, acquiredInfo); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception in notifying listener that authentication " + + "stopped", e); + } + } + } + @Override public void binderDied() { // Do nothing, handled below diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index e0fd44b9f6bb..8121a639ab0a 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -29,6 +29,7 @@ import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired; import android.hardware.biometrics.BiometricManager.Authenticators; +import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.common.ICancellationSignal; import android.hardware.biometrics.common.OperationState; import android.hardware.biometrics.fingerprint.PointerContext; @@ -102,6 +103,7 @@ public class FingerprintAuthenticationClient private Runnable mAuthSuccessRunnable; private final Clock mClock; + public FingerprintAuthenticationClient( @NonNull Context context, @NonNull Supplier<AidlSession> lazyDaemon, @@ -280,6 +282,8 @@ public class FingerprintAuthenticationClient public void onAcquired(@FingerprintAcquired int acquiredInfo, int vendorCode) { // For UDFPS, notify SysUI with acquiredInfo, so that the illumination can be turned off // for most ACQUIRED messages. See BiometricFingerprintConstants#FingerprintAcquired + mAuthenticationStateListeners.onAuthenticationAcquired( + BiometricSourceType.FINGERPRINT, getRequestReason(), acquiredInfo); mSensorOverlays.ifUdfps(controller -> controller.onAcquired(getSensorId(), acquiredInfo)); super.onAcquired(acquiredInfo, vendorCode); PerformanceTracker pt = PerformanceTracker.getInstanceForSensorId(getSensorId()); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java index 60c532c26f5d..b6311afb5ea1 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java @@ -28,6 +28,7 @@ import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricManager.Authenticators; +import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.fingerprint.PointerContext; import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint; import android.hardware.fingerprint.FingerprintAuthenticateOptions; @@ -201,6 +202,8 @@ class FingerprintAuthenticationClient @Override public void onAcquired(int acquiredInfo, int vendorCode) { + mAuthenticationStateListeners.onAuthenticationAcquired( + BiometricSourceType.FINGERPRINT, getRequestReason(), acquiredInfo); super.onAcquired(acquiredInfo, vendorCode); @LockoutTracker.LockoutMode final int lockoutMode = diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 93addcd84d12..e930627b55af 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -872,14 +872,15 @@ public final class DisplayManagerService extends SystemService { } @VisibleForTesting - void performTraversalInternal(SurfaceControl.Transaction t) { + void performTraversalInternal(SurfaceControl.Transaction t, + SparseArray<SurfaceControl.Transaction> displayTransactions) { synchronized (mSyncRoot) { if (!mPendingTraversal) { return; } mPendingTraversal = false; - performTraversalLocked(t); + performTraversalLocked(t, displayTransactions); } // List is self-synchronized copy-on-write. @@ -2593,7 +2594,8 @@ public final class DisplayManagerService extends SystemService { } } - private void performTraversalLocked(SurfaceControl.Transaction t) { + private void performTraversalLocked(SurfaceControl.Transaction t, + SparseArray<SurfaceControl.Transaction> displayTransactions) { // Clear all viewports before configuring displays so that we can keep // track of which ones we have configured. clearViewportsLocked(); @@ -2601,9 +2603,11 @@ public final class DisplayManagerService extends SystemService { // Configure each display device. mLogicalDisplayMapper.forEachLocked((LogicalDisplay display) -> { final DisplayDevice device = display.getPrimaryDisplayDeviceLocked(); + final SurfaceControl.Transaction displayTransaction = + displayTransactions.get(display.getDisplayIdLocked(), t); if (device != null) { - configureDisplayLocked(t, device); - device.performTraversalLocked(t); + configureDisplayLocked(displayTransaction, device); + device.performTraversalLocked(displayTransaction); } }); @@ -4680,8 +4684,9 @@ public final class DisplayManagerService extends SystemService { } @Override - public void performTraversal(SurfaceControl.Transaction t) { - performTraversalInternal(t); + public void performTraversal(SurfaceControl.Transaction t, + SparseArray<SurfaceControl.Transaction> displayTransactions) { + performTraversalInternal(t, displayTransactions); } @Override diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index f031b7b677ac..cb00e44f09a9 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -3185,6 +3185,24 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } } + + if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) { + String ime = SecureSettingsWrapper.getString( + Settings.Secure.DEFAULT_INPUT_METHOD, null, mSettings.getUserId()); + String defaultDeviceIme = SecureSettingsWrapper.getString( + Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null, mSettings.getUserId()); + if (defaultDeviceIme != null && !Objects.equals(ime, defaultDeviceIme)) { + if (DEBUG) { + Slog.v(TAG, "Current input method " + ime + " differs from the stored default" + + " device input method for user " + mSettings.getUserId() + + " - restoring " + defaultDeviceIme); + } + SecureSettingsWrapper.putString( + Settings.Secure.DEFAULT_INPUT_METHOD, defaultDeviceIme, + mSettings.getUserId()); + } + } + // We are assuming that whoever is changing DEFAULT_INPUT_METHOD and // ENABLED_INPUT_METHODS is taking care of keeping them correctly in // sync, so we will never have a DEFAULT_INPUT_METHOD that is not @@ -3659,6 +3677,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Slog.e(TAG, "windowToken cannot be null."); return InputBindResult.NULL; } + // The user represented by userId, must be running. + if (!mUserManagerInternal.isUserRunning(userId)) { + // There is a chance that we hit here because of race condition. Let's just + // return an error code instead of crashing the caller process, which at + // least has INTERACT_ACROSS_USERS_FULL permission thus is likely to be an + // important process. + Slog.w(TAG, "User #" + userId + " is not running."); + return InputBindResult.INVALID_USER; + } try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.startInputOrWindowGainedFocus"); @@ -3666,20 +3693,67 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub "InputMethodManagerService#startInputOrWindowGainedFocus"); final InputBindResult result; synchronized (ImfLock.class) { + // If the system is not yet ready, we shouldn't be running third party code. if (!mSystemReady) { - // If the system is not yet ready, we shouldn't be running third arty code. return new InputBindResult( InputBindResult.ResultCode.ERROR_SYSTEM_NOT_READY, null /* method */, null /* accessibilitySessions */, null /* channel */, getSelectedMethodIdLocked(), getSequenceNumberLocked(), false /* isInputMethodSuppressingSpellChecker */); } + final ClientState cs = mClientController.getClient(client.asBinder()); + if (cs == null) { + throw new IllegalArgumentException("Unknown client " + client.asBinder()); + } final long ident = Binder.clearCallingIdentity(); try { + // Verify if IMMS is in the process of switching user. + if (mUserSwitchHandlerTask != null) { + // There is already an on-going pending user switch task. + final int nextUserId = mUserSwitchHandlerTask.mToUserId; + if (userId == nextUserId) { + scheduleSwitchUserTaskLocked(userId, cs.mClient); + return InputBindResult.USER_SWITCHING; + } + final int[] profileIdsWithDisabled = mUserManagerInternal.getProfileIds( + mSettings.getUserId(), false /* enabledOnly */); + for (int profileId : profileIdsWithDisabled) { + if (profileId == userId) { + scheduleSwitchUserTaskLocked(userId, cs.mClient); + return InputBindResult.USER_SWITCHING; + } + } + return InputBindResult.INVALID_USER; + } + + // Ensure that caller's focused window and display parameters are allowd to + // display input method. + final int imeClientFocus = mWindowManagerInternal.hasInputMethodClientFocus( + windowToken, cs.mUid, cs.mPid, cs.mSelfReportedDisplayId); + switch (imeClientFocus) { + case WindowManagerInternal.ImeClientFocusResult.DISPLAY_ID_MISMATCH: + Slog.e(TAG, + "startInputOrWindowGainedFocusInternal: display ID mismatch."); + return InputBindResult.DISPLAY_ID_MISMATCH; + case WindowManagerInternal.ImeClientFocusResult.NOT_IME_TARGET_WINDOW: + // Check with the window manager to make sure this client actually + // has a window with focus. If not, reject. This is thread safe + // because if the focus changes some time before or after, the + // next client receiving focus that has any interest in input will + // be calling through here after that change happens. + if (DEBUG) { + Slog.w(TAG, "Focus gain on non-focused client " + cs.mClient + + " (uid=" + cs.mUid + " pid=" + cs.mPid + ")"); + } + return InputBindResult.NOT_IME_TARGET_WINDOW; + case WindowManagerInternal.ImeClientFocusResult.INVALID_DISPLAY_ID: + return InputBindResult.INVALID_DISPLAY_ID; + } + result = startInputOrWindowGainedFocusInternalLocked(startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection, - unverifiedTargetSdkVersion, userId, imeDispatcher); + unverifiedTargetSdkVersion, userId, imeDispatcher, cs); } finally { Binder.restoreCallingIdentity(ident); } @@ -3708,7 +3782,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub IRemoteInputConnection inputContext, @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, @NonNull ClientState cs) { if (DEBUG) { Slog.v(TAG, "startInputOrWindowGainedFocusInternalLocked: reason=" + InputMethodDebug.startInputReasonToString(startInputReason) @@ -3721,60 +3795,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub + " windowFlags=#" + Integer.toHexString(windowFlags) + " unverifiedTargetSdkVersion=" + unverifiedTargetSdkVersion + " userId=" + userId - + " imeDispatcher=" + imeDispatcher); - } - - if (!mUserManagerInternal.isUserRunning(userId)) { - // There is a chance that we hit here because of race condition. Let's just - // return an error code instead of crashing the caller process, which at - // least has INTERACT_ACROSS_USERS_FULL permission thus is likely to be an - // important process. - Slog.w(TAG, "User #" + userId + " is not running."); - return InputBindResult.INVALID_USER; - } - - final ClientState cs = mClientController.getClient(client.asBinder()); - if (cs == null) { - throw new IllegalArgumentException("Unknown client " + client.asBinder()); - } - - final int imeClientFocus = mWindowManagerInternal.hasInputMethodClientFocus( - windowToken, cs.mUid, cs.mPid, cs.mSelfReportedDisplayId); - switch (imeClientFocus) { - case WindowManagerInternal.ImeClientFocusResult.DISPLAY_ID_MISMATCH: - Slog.e(TAG, "startInputOrWindowGainedFocusInternal: display ID mismatch."); - return InputBindResult.DISPLAY_ID_MISMATCH; - case WindowManagerInternal.ImeClientFocusResult.NOT_IME_TARGET_WINDOW: - // Check with the window manager to make sure this client actually - // has a window with focus. If not, reject. This is thread safe - // because if the focus changes some time before or after, the - // next client receiving focus that has any interest in input will - // be calling through here after that change happens. - if (DEBUG) { - Slog.w(TAG, "Focus gain on non-focused client " + cs.mClient - + " (uid=" + cs.mUid + " pid=" + cs.mPid + ")"); - } - return InputBindResult.NOT_IME_TARGET_WINDOW; - case WindowManagerInternal.ImeClientFocusResult.INVALID_DISPLAY_ID: - return InputBindResult.INVALID_DISPLAY_ID; - } - - if (mUserSwitchHandlerTask != null) { - // There is already an on-going pending user switch task. - final int nextUserId = mUserSwitchHandlerTask.mToUserId; - if (userId == nextUserId) { - scheduleSwitchUserTaskLocked(userId, cs.mClient); - return InputBindResult.USER_SWITCHING; - } - final int[] profileIdsWithDisabled = mUserManagerInternal.getProfileIds( - mSettings.getUserId(), false /* enabledOnly */); - for (int profileId : profileIdsWithDisabled) { - if (profileId == userId) { - scheduleSwitchUserTaskLocked(userId, cs.mClient); - return InputBindResult.USER_SWITCHING; - } - } - return InputBindResult.INVALID_USER; + + " imeDispatcher=" + imeDispatcher + + " cs=" + cs); } final boolean shouldClearFlag = mImePlatformCompatUtils.shouldClearShowForcedFlag(cs.mUid); @@ -5366,7 +5388,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!setSubtypeOnly) { // Set InputMethod here - mSettings.putSelectedInputMethod(imi != null ? imi.getId() : ""); + final String imeId = imi != null ? imi.getId() : ""; + mSettings.putSelectedInputMethod(imeId); + if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) { + mSettings.putSelectedDefaultDeviceInputMethod(imeId); + } } } @@ -5509,6 +5535,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; // IME is not found or not enabled. } settings.putSelectedInputMethod(imeId); + if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) { + settings.putSelectedDefaultDeviceInputMethod(imeId); + } settings.putSelectedSubtype(NOT_A_SUBTYPE_ID); return true; } @@ -6555,6 +6584,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // Reset selected IME. settings.putSelectedInputMethod(nextIme); + if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) { + settings.putSelectedDefaultDeviceInputMethod(nextIme); + } settings.putSelectedSubtype(NOT_A_SUBTYPE_ID); } out.println("Reset current and enabled IMEs for user #" + userId); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java index a51002be344f..e444db1b79e8 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java @@ -36,7 +36,6 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.function.Predicate; /** @@ -88,12 +87,6 @@ final class InputMethodSettings { mMethodMap = methodMap; mMethodList = methodMap.values(); mUserId = userId; - String ime = getSelectedInputMethod(); - String defaultDeviceIme = getSelectedDefaultDeviceInputMethod(); - if (defaultDeviceIme != null && !Objects.equals(ime, defaultDeviceIme)) { - putSelectedInputMethod(defaultDeviceIme); - putSelectedDefaultDeviceInputMethod(null); - } } @AnyThread diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 4da2cc9bbe20..1c9bbab3969e 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -2049,7 +2049,9 @@ public class NotificationManagerService extends SystemService { if (!mUserProfiles.isProfileUser(userId)) { mConditionProviders.onUserUnlocked(userId); mListeners.onUserUnlocked(userId); - mZenModeHelper.onUserUnlocked(userId); + if (!android.app.Flags.modesApi()) { + mZenModeHelper.onUserUnlocked(userId); + } } } } diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 41ff4150eb23..2f20bbe7f29c 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -328,6 +328,7 @@ public class ZenModeHelper { } } + // TODO: b/310620812 - Remove when MODES_API is inlined (no more callers). public void onUserUnlocked(int user) { loadConfigForUser(user, "onUserUnlocked"); } diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index dbff4423d10c..722654a9102c 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -43,6 +43,13 @@ flag { } flag { + name: "screenshare_notification_hiding" + namespace: "systemui" + description: "Enable hiding of notifications during screenshare" + bug: "312784809" +} + +flag { name: "sensitive_notification_app_protection" namespace: "systemui" description: "This flag controls the sensitive notification app protections while screen sharing" diff --git a/services/core/java/com/android/server/om/IdmapManager.java b/services/core/java/com/android/server/om/IdmapManager.java index 86d05d92c95b..25a39cc8456f 100644 --- a/services/core/java/com/android/server/om/IdmapManager.java +++ b/services/core/java/com/android/server/om/IdmapManager.java @@ -257,7 +257,7 @@ final class IdmapManager { private boolean matchesActorSignature(@NonNull AndroidPackage targetPackage, @NonNull AndroidPackage overlayPackage, int userId) { String targetOverlayableName = overlayPackage.getOverlayTargetOverlayableName(); - if (targetOverlayableName != null && !mPackageManager.getNamedActors().isEmpty()) { + if (targetOverlayableName != null) { try { OverlayableInfo overlayableInfo = mPackageManager.getOverlayableForTarget( targetPackage.getPackageName(), targetOverlayableName, userId); diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java index a61b03fdbb39..b9464d96a019 100644 --- a/services/core/java/com/android/server/om/OverlayManagerService.java +++ b/services/core/java/com/android/server/om/OverlayManagerService.java @@ -32,7 +32,6 @@ import static android.os.Process.INVALID_UID; import static android.os.Trace.TRACE_TAG_RRO; import static android.os.Trace.traceBegin; import static android.os.Trace.traceEnd; - import static com.android.server.om.OverlayManagerServiceImpl.OperationFailedException; import android.annotation.NonNull; @@ -363,7 +362,7 @@ public final class OverlayManagerService extends SystemService { defaultPackages.add(packageName); } } - return defaultPackages.toArray(new String[0]); + return defaultPackages.toArray(new String[defaultPackages.size()]); } private final class OverlayManagerPackageMonitor extends PackageMonitor { @@ -1144,10 +1143,9 @@ public final class OverlayManagerService extends SystemService { }; private static final class PackageManagerHelperImpl implements PackageManagerHelper { - private static final class PackageStateUsers { + private static class PackageStateUsers { private PackageState mPackageState; - private Boolean mDefinesOverlayable = null; - private final ArraySet<Integer> mInstalledUsers = new ArraySet<>(); + private final Set<Integer> mInstalledUsers = new ArraySet<>(); private PackageStateUsers(@NonNull PackageState packageState) { this.mPackageState = packageState; } @@ -1162,7 +1160,7 @@ public final class OverlayManagerService extends SystemService { // state may lead to contradictions within OMS. Better then to lag // behind until all pending intents have been processed. private final ArrayMap<String, PackageStateUsers> mCache = new ArrayMap<>(); - private final ArraySet<Integer> mInitializedUsers = new ArraySet<>(); + private final Set<Integer> mInitializedUsers = new ArraySet<>(); PackageManagerHelperImpl(Context context) { mContext = context; @@ -1178,7 +1176,8 @@ public final class OverlayManagerService extends SystemService { */ @NonNull public ArrayMap<String, PackageState> initializeForUser(final int userId) { - if (mInitializedUsers.add(userId)) { + if (!mInitializedUsers.contains(userId)) { + mInitializedUsers.add(userId); mPackageManagerInternal.forEachPackageState((packageState -> { if (packageState.getPkg() != null && packageState.getUserStateOrDefault(userId).isInstalled()) { @@ -1197,11 +1196,13 @@ public final class OverlayManagerService extends SystemService { return userPackages; } - private PackageStateUsers getRawPackageStateForUser(@NonNull final String packageName, + @Override + @Nullable + public PackageState getPackageStateForUser(@NonNull final String packageName, final int userId) { final PackageStateUsers pkg = mCache.get(packageName); if (pkg != null && pkg.mInstalledUsers.contains(userId)) { - return pkg; + return pkg.mPackageState; } try { if (!mPackageManager.isPackageAvailable(packageName, userId)) { @@ -1215,14 +1216,8 @@ public final class OverlayManagerService extends SystemService { return addPackageUser(packageName, userId); } - @Override - public PackageState getPackageStateForUser(@NonNull final String packageName, - final int userId) { - final PackageStateUsers pkg = getRawPackageStateForUser(packageName, userId); - return pkg != null ? pkg.mPackageState : null; - } - - private PackageStateUsers addPackageUser(@NonNull final String packageName, + @NonNull + private PackageState addPackageUser(@NonNull final String packageName, final int user) { final PackageState pkg = mPackageManagerInternal.getPackageStateInternal(packageName); if (pkg == null) { @@ -1234,20 +1229,20 @@ public final class OverlayManagerService extends SystemService { } @NonNull - private PackageStateUsers addPackageUser(@NonNull final PackageState pkg, + private PackageState addPackageUser(@NonNull final PackageState pkg, final int user) { PackageStateUsers pkgUsers = mCache.get(pkg.getPackageName()); if (pkgUsers == null) { pkgUsers = new PackageStateUsers(pkg); mCache.put(pkg.getPackageName(), pkgUsers); - } else if (pkgUsers.mPackageState != pkg) { + } else { pkgUsers.mPackageState = pkg; - pkgUsers.mDefinesOverlayable = null; } pkgUsers.mInstalledUsers.add(user); - return pkgUsers; + return pkgUsers.mPackageState; } + @NonNull private void removePackageUser(@NonNull final String packageName, final int user) { final PackageStateUsers pkgUsers = mCache.get(packageName); @@ -1265,15 +1260,15 @@ public final class OverlayManagerService extends SystemService { } } + @Nullable public PackageState onPackageAdded(@NonNull final String packageName, final int userId) { - final var pu = addPackageUser(packageName, userId); - return pu != null ? pu.mPackageState : null; + return addPackageUser(packageName, userId); } + @Nullable public PackageState onPackageUpdated(@NonNull final String packageName, final int userId) { - final var pu = addPackageUser(packageName, userId); - return pu != null ? pu.mPackageState : null; + return addPackageUser(packageName, userId); } public void onPackageRemoved(@NonNull final String packageName, final int userId) { @@ -1313,30 +1308,22 @@ public final class OverlayManagerService extends SystemService { return (pkgs.length == 0) ? null : pkgs[0]; } + @Nullable @Override public OverlayableInfo getOverlayableForTarget(@NonNull String packageName, @NonNull String targetOverlayableName, int userId) throws IOException { - final var psu = getRawPackageStateForUser(packageName, userId); - final var pkg = (psu == null || psu.mPackageState == null) - ? null : psu.mPackageState.getAndroidPackage(); + var packageState = getPackageStateForUser(packageName, userId); + var pkg = packageState == null ? null : packageState.getAndroidPackage(); if (pkg == null) { throw new IOException("Unable to get target package"); } - if (Boolean.FALSE.equals(psu.mDefinesOverlayable)) { - return null; - } - ApkAssets apkAssets = null; try { apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(), ApkAssets.PROPERTY_ONLY_OVERLAYABLES); - if (psu.mDefinesOverlayable == null) { - psu.mDefinesOverlayable = apkAssets.definesOverlayable(); - } - return Boolean.FALSE.equals(psu.mDefinesOverlayable) - ? null : apkAssets.getOverlayableInfo(targetOverlayableName); + return apkAssets.getOverlayableInfo(targetOverlayableName); } finally { if (apkAssets != null) { try { @@ -1350,29 +1337,24 @@ public final class OverlayManagerService extends SystemService { @Override public boolean doesTargetDefineOverlayable(String targetPackageName, int userId) throws IOException { - final var psu = getRawPackageStateForUser(targetPackageName, userId); - var pkg = (psu == null || psu.mPackageState == null) - ? null : psu.mPackageState.getAndroidPackage(); + var packageState = getPackageStateForUser(targetPackageName, userId); + var pkg = packageState == null ? null : packageState.getAndroidPackage(); if (pkg == null) { throw new IOException("Unable to get target package"); } - if (psu.mDefinesOverlayable == null) { - ApkAssets apkAssets = null; - try { - apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(), - ApkAssets.PROPERTY_ONLY_OVERLAYABLES); - psu.mDefinesOverlayable = apkAssets.definesOverlayable(); - } finally { - if (apkAssets != null) { - try { - apkAssets.close(); - } catch (Throwable ignored) { - } + ApkAssets apkAssets = null; + try { + apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath()); + return apkAssets.definesOverlayable(); + } finally { + if (apkAssets != null) { + try { + apkAssets.close(); + } catch (Throwable ignored) { } } } - return psu.mDefinesOverlayable; } @Override @@ -1563,7 +1545,8 @@ public final class OverlayManagerService extends SystemService { final OverlayPaths frameworkOverlays = mImpl.getEnabledOverlayPaths("android", userId, false); for (final String targetPackageName : targetPackageNames) { - final var list = new OverlayPaths.Builder(frameworkOverlays); + final OverlayPaths.Builder list = new OverlayPaths.Builder(); + list.addAll(frameworkOverlays); if (!"android".equals(targetPackageName)) { list.addAll(mImpl.getEnabledOverlayPaths(targetPackageName, userId, true)); } @@ -1575,21 +1558,17 @@ public final class OverlayManagerService extends SystemService { final HashSet<String> invalidPackages = new HashSet<>(); pm.setEnabledOverlayPackages(userId, pendingChanges, updatedPackages, invalidPackages); - if (DEBUG || !invalidPackages.isEmpty()) { - for (final String targetPackageName : targetPackageNames) { - if (DEBUG) { - Slog.d(TAG, - "-> Updating overlay: target=" + targetPackageName + " overlays=[" - + pendingChanges.get(targetPackageName) - + "] userId=" + userId); - } + for (final String targetPackageName : targetPackageNames) { + if (DEBUG) { + Slog.d(TAG, "-> Updating overlay: target=" + targetPackageName + " overlays=[" + + pendingChanges.get(targetPackageName) + + "] userId=" + userId); + } - if (invalidPackages.contains(targetPackageName)) { - Slog.e(TAG, TextUtils.formatSimple( - "Failed to change enabled overlays for %s user %d", - targetPackageName, - userId)); - } + if (invalidPackages.contains(targetPackageName)) { + Slog.e(TAG, TextUtils.formatSimple( + "Failed to change enabled overlays for %s user %d", targetPackageName, + userId)); } } return new ArrayList<>(updatedPackages); diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java index c1b6ccc7e25c..972c78db9460 100644 --- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java +++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java @@ -772,20 +772,24 @@ final class OverlayManagerServiceImpl { OverlayPaths getEnabledOverlayPaths(@NonNull final String targetPackageName, final int userId, boolean includeImmutableOverlays) { - final var paths = new OverlayPaths.Builder(); - mSettings.forEachMatching(userId, null, targetPackageName, oi -> { + final List<OverlayInfo> overlays = mSettings.getOverlaysForTarget(targetPackageName, + userId); + final OverlayPaths.Builder paths = new OverlayPaths.Builder(); + final int n = overlays.size(); + for (int i = 0; i < n; i++) { + final OverlayInfo oi = overlays.get(i); if (!oi.isEnabled()) { - return; + continue; } if (!includeImmutableOverlays && !oi.isMutable) { - return; + continue; } if (oi.isFabricated()) { paths.addNonApkPath(oi.baseCodePath); } else { paths.addApkPath(oi.baseCodePath); } - }); + } return paths.build(); } diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java index b8b49f3eed2f..eae614ac9e77 100644 --- a/services/core/java/com/android/server/om/OverlayManagerSettings.java +++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java @@ -47,7 +47,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; -import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Stream; @@ -183,23 +182,6 @@ final class OverlayManagerSettings { return CollectionUtils.map(items, SettingsItem::getOverlayInfo); } - void forEachMatching(int userId, String overlayName, String targetPackageName, - @NonNull Consumer<OverlayInfo> consumer) { - for (int i = 0, n = mItems.size(); i < n; i++) { - final SettingsItem item = mItems.get(i); - if (item.getUserId() != userId) { - continue; - } - if (overlayName != null && !item.mOverlay.getPackageName().equals(overlayName)) { - continue; - } - if (targetPackageName != null && !item.mTargetPackageName.equals(targetPackageName)) { - continue; - } - consumer.accept(item.getOverlayInfo()); - } - } - ArrayMap<String, List<OverlayInfo>> getOverlaysForUser(final int userId) { final List<SettingsItem> items = selectWhereUser(userId); diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index 474b5907524c..339b1e71c091 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -19,7 +19,6 @@ package com.android.server.pm; import static android.app.ActivityManager.START_ABORTED; import static android.app.ActivityManager.START_CLASS_NOT_FOUND; import static android.app.ActivityManager.START_PERMISSION_DENIED; -import static android.app.ActivityManager.START_SUCCESS; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_IGNORED; import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; @@ -286,10 +285,16 @@ public class PackageArchiver { Slog.e(TAG, TextUtils.formatSimple( "Unexpected error occurred while unarchiving package %s: %s.", packageName, t.getLocalizedMessage())); - return START_ABORTED; } - return START_SUCCESS; + // We return STATUS_ABORTED because: + // 1. Archived App is not actually present during activity start. Hence the unarchival + // start should be treated as an error code. + // 2. STATUS_ABORTED is not visible to the end consumers. Hence, it will not change user + // experience. + // 3. Returning STATUS_ABORTED helps us avoid manually handling of different cases like + // aborting activity options, animations etc in the Windows Manager. + return START_ABORTED; } /** diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 51790b875465..b947aa38671f 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -5016,6 +5016,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY: case KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY: case KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL: { + Slog.i(TAG, "Stylus buttons event: " + keyCode + " received. Should handle event? " + + mStylusButtonsEnabled); if (mStylusButtonsEnabled) { sendSystemKeyToStatusBarAsync(event); } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java index 8549957f46b8..37f38252698e 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java @@ -347,7 +347,7 @@ public class WallpaperCropper { for (Rect crop : relativeCropHints) { Rect originalRect = new Rect(crop); originalRect.scale(wallpaper.mSampleSize); - originalRect.offset(wallpaper.cropHint.left, wallpaper.cropHint.right); + originalRect.offset(wallpaper.cropHint.left, wallpaper.cropHint.top); result.add(originalRect); } return result; diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index d6f52b89819e..85580ac6810a 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1034,20 +1034,19 @@ class ActivityStarter { } if (err == ActivityManager.START_SUCCESS && aInfo == null) { + // We couldn't find the specific class specified in the Intent. + err = ActivityManager.START_CLASS_NOT_FOUND; + if (isArchivingEnabled()) { PackageArchiver packageArchiver = mService .getPackageManagerInternalLocked() .getPackageArchiver(); if (packageArchiver.isIntentResolvedToArchivedApp(intent, mRequest.userId)) { - return packageArchiver + err = packageArchiver .requestUnarchiveOnActivityStart( intent, callingPackage, mRequest.userId, realCallingUid); } } - - // We couldn't find the specific class specified in the Intent. - // Also the end of the line. - err = ActivityManager.START_CLASS_NOT_FOUND; } if (err == ActivityManager.START_SUCCESS && sourceRecord != null diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 3397a3dd2290..0def5a1861ea 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -3505,8 +3505,9 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override - public boolean isAssistDataAllowedOnCurrentActivity() { + public boolean isAssistDataAllowed() { int userId; + boolean hasRestrictedWindow; synchronized (mGlobalLock) { final Task focusedRootTask = getTopDisplayFocusedRootTask(); if (focusedRootTask == null || focusedRootTask.isActivityTypeAssistant()) { @@ -3518,8 +3519,17 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return false; } userId = activity.mUserId; + DisplayContent displayContent = activity.getDisplayContent(); + if (displayContent == null) { + return false; + } + hasRestrictedWindow = displayContent.forAllWindows(windowState -> { + return windowState.isOnScreen() && UserManager.isUserTypePrivateProfile( + getUserManager().getProfileType(windowState.mShowUserId)); + }, true /* traverseTopToBottom */); } - return DevicePolicyCache.getInstance().isScreenCaptureAllowed(userId); + return DevicePolicyCache.getInstance().isScreenCaptureAllowed(userId) + && !hasRestrictedWindow; } private void onLocalVoiceInteractionStartedLocked(IBinder activity, diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 82dbf8d4242b..716aee3f8692 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1429,14 +1429,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return mTokenMap.get(binder); } - ActivityRecord getActivityRecord(IBinder binder) { - final WindowToken token = getWindowToken(binder); - if (token == null) { - return null; - } - return token.asActivityRecord(); - } - void addWindowToken(IBinder binder, WindowToken token) { final DisplayContent dc = mWmService.mRoot.getWindowTokenDisplay(token); if (dc != null) { @@ -2250,7 +2242,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } } - mWmService.mDisplayManagerInternal.performTraversal(transaction); if (shellTransitions) { // Before setDisplayProjection is applied by the start transaction of transition, // set the transform hint to avoid using surface in old rotation. @@ -6985,7 +6976,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp @Override public void onAppTransitionFinishedLocked(IBinder token) { - final ActivityRecord r = getActivityRecord(token); + final ActivityRecord r = ActivityRecord.forTokenLocked(token); // Ignore the animating recents so the fixed rotation transform won't be switched twice // by finishing the recents animation and moving it to top. That also avoids flickering // due to wait for previous activity to be paused if it supports PiP that ignores the diff --git a/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java b/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java index eae9951d0679..d08f0438f853 100644 --- a/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java +++ b/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java @@ -19,6 +19,7 @@ package com.android.server.wm; import android.annotation.NonNull; import android.internal.perfetto.protos.PerfettoTrace; import android.os.SystemClock; +import android.os.Trace; import android.tracing.perfetto.DataSourceParams; import android.tracing.perfetto.InitArguments; import android.tracing.perfetto.Producer; @@ -57,6 +58,16 @@ class PerfettoTransitionTracer implements TransitionTracer { return; } + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logSentTransition"); + try { + doLogSentTransition(transition, targets); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + } + + private void doLogSentTransition( + Transition transition, ArrayList<Transition.ChangeInfo> targets) { mDataSource.trace((ctx) -> { final ProtoOutputStream os = ctx.newTracePacket(); @@ -91,6 +102,15 @@ class PerfettoTransitionTracer implements TransitionTracer { return; } + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logFinishedTransition"); + try { + doLogFinishTransition(transition); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + } + + private void doLogFinishTransition(Transition transition) { mDataSource.trace((ctx) -> { final ProtoOutputStream os = ctx.newTracePacket(); @@ -114,6 +134,15 @@ class PerfettoTransitionTracer implements TransitionTracer { return; } + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logAbortedTransition"); + try { + doLogAbortedTransition(transition); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + } + + private void doLogAbortedTransition(Transition transition) { mDataSource.trace((ctx) -> { final ProtoOutputStream os = ctx.newTracePacket(); @@ -131,6 +160,15 @@ class PerfettoTransitionTracer implements TransitionTracer { return; } + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logRemovingStartingWindow"); + try { + doLogRemovingStartingWindow(startingData); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + } + + public void doLogRemovingStartingWindow(@NonNull StartingData startingData) { mDataSource.trace((ctx) -> { final ProtoOutputStream os = ctx.newTracePacket(); diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 587cc7489763..25646f19c461 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -249,6 +249,8 @@ class RootWindowContainer extends WindowContainer<DisplayContent> /** Reference to default display so we can quickly look it up. */ private DisplayContent mDefaultDisplay; private final SparseArray<IntArray> mDisplayAccessUIDs = new SparseArray<>(); + private final SparseArray<SurfaceControl.Transaction> mDisplayTransactions = + new SparseArray<>(); /** The current user */ int mCurrentUser; @@ -569,22 +571,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> }, true /* traverseTopToBottom */); } - /** - * Returns the app window token for the input binder if it exist in the system. - * NOTE: Only one AppWindowToken is allowed to exist in the system for a binder token, since - * AppWindowToken represents an activity which can only exist on one display. - */ - ActivityRecord getActivityRecord(IBinder binder) { - for (int i = mChildren.size() - 1; i >= 0; --i) { - final DisplayContent dc = mChildren.get(i); - final ActivityRecord activity = dc.getActivityRecord(binder); - if (activity != null) { - return activity; - } - } - return null; - } - /** Returns the window token for the input binder if it exist in the system. */ WindowToken getWindowToken(IBinder binder) { for (int i = mChildren.size() - 1; i >= 0; --i) { @@ -991,11 +977,13 @@ class RootWindowContainer extends WindowContainer<DisplayContent> for (int j = 0; j < count; ++j) { final DisplayContent dc = mChildren.get(j); dc.applySurfaceChangesTransaction(); + mDisplayTransactions.append(dc.mDisplayId, dc.getSyncTransaction()); } // Give the display manager a chance to adjust properties like display rotation if it needs // to. - mWmService.mDisplayManagerInternal.performTraversal(t); + mWmService.mDisplayManagerInternal.performTraversal(t, mDisplayTransactions); + mDisplayTransactions.clear(); } /** diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java index bdb45884887c..967f415d2c11 100644 --- a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java +++ b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java @@ -36,8 +36,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.protolog.common.ProtoLog; import java.io.PrintWriter; -import java.util.Map; -import java.util.Set; +import java.util.ArrayList; public class ScreenRecordingCallbackController { @@ -57,10 +56,10 @@ public class ScreenRecordingCallbackController { } @GuardedBy("WindowManagerService.mGlobalLock") - private final Map<IBinder, Callback> mCallbacks = new ArrayMap<>(); + private final ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<>(); @GuardedBy("WindowManagerService.mGlobalLock") - private final Map<Integer /*UID*/, Boolean> mLastInvokedStateByUid = new ArrayMap<>(); + private final ArrayMap<Integer /*UID*/, Boolean> mLastInvokedStateByUid = new ArrayMap<>(); private final WindowManagerService mWms; @@ -108,8 +107,8 @@ public class ScreenRecordingCallbackController { } IBinder binder = ServiceManager.getService(MEDIA_PROJECTION_SERVICE); - IMediaProjectionManager mediaProjectionManager = - IMediaProjectionManager.Stub.asInterface(binder); + IMediaProjectionManager mediaProjectionManager = IMediaProjectionManager.Stub.asInterface( + binder); long identityToken = Binder.clearCallingIdentity(); MediaProjectionInfo mediaProjectionInfo = null; @@ -157,11 +156,14 @@ public class ScreenRecordingCallbackController { synchronized (mWms.mGlobalLock) { IBinder binder = callback.asBinder(); Callback callbackInfo = mCallbacks.remove(binder); + if (callbackInfo == null) { + return; + } binder.unlinkToDeath(callbackInfo, 0); boolean uidHasCallback = false; - for (Callback cb : mCallbacks.values()) { - if (cb.mUid == callbackInfo.mUid) { + for (int i = 0; i < mCallbacks.size(); i++) { + if (mCallbacks.valueAt(i).mUid == callbackInfo.mUid) { uidHasCallback = true; break; } @@ -213,7 +215,9 @@ public class ScreenRecordingCallbackController { return; } - dispatchCallbacks(Set.of(uid), processVisible); + ArraySet<Integer> uidSet = new ArraySet<>(); + uidSet.add(uid); + dispatchCallbacks(uidSet, processVisible); } @GuardedBy("WindowManagerService.mGlobalLock") @@ -233,8 +237,8 @@ public class ScreenRecordingCallbackController { } @GuardedBy("WindowManagerService.mGlobalLock") - private Set<Integer> getRecordedUids() { - Set<Integer> result = new ArraySet<>(); + private ArraySet<Integer> getRecordedUids() { + ArraySet<Integer> result = new ArraySet<>(); if (mRecordedWC == null) { return result; } @@ -248,37 +252,43 @@ public class ScreenRecordingCallbackController { } @GuardedBy("WindowManagerService.mGlobalLock") - private void dispatchCallbacks(Set<Integer> uids, boolean visibleInScreenRecording) { + private void dispatchCallbacks(ArraySet<Integer> uids, boolean visibleInScreenRecording) { if (uids.isEmpty()) { return; } - for (Integer uid : uids) { - mLastInvokedStateByUid.put(uid, visibleInScreenRecording); + for (int i = 0; i < uids.size(); i++) { + mLastInvokedStateByUid.put(uids.valueAt(i), visibleInScreenRecording); } - for (Callback callback : mCallbacks.values()) { - if (!uids.contains(callback.mUid)) { - continue; - } - try { - callback.mCallback.onScreenRecordingStateChanged(visibleInScreenRecording); - } catch (RemoteException e) { - // Client has died. Cleanup is handled via DeathRecipient. + ArrayList<IScreenRecordingCallback> callbacks = new ArrayList<>(); + for (int i = 0; i < mCallbacks.size(); i++) { + if (uids.contains(mCallbacks.valueAt(i).mUid)) { + callbacks.add(mCallbacks.valueAt(i).mCallback); } } + + mWms.mH.post(() -> { + for (int i = 0; i < callbacks.size(); i++) { + try { + callbacks.get(i).onScreenRecordingStateChanged(visibleInScreenRecording); + } catch (RemoteException e) { + // Client has died. Cleanup is handled via DeathRecipient. + } + } + }); } void dump(PrintWriter pw) { pw.format("ScreenRecordingCallbackController:\n"); pw.format(" Registered callbacks:\n"); - for (Map.Entry<IBinder, Callback> entry : mCallbacks.entrySet()) { - pw.format(" callback=%s uid=%s\n", entry.getKey(), entry.getValue().mUid); + for (int i = 0; i < mCallbacks.size(); i++) { + pw.format(" callback=%s uid=%s\n", mCallbacks.keyAt(i), mCallbacks.valueAt(i).mUid); } pw.format(" Last invoked states:\n"); - for (Map.Entry<Integer, Boolean> entry : mLastInvokedStateByUid.entrySet()) { - pw.format(" uid=%s isVisibleInScreenRecording=%s\n", entry.getKey(), - entry.getValue()); + for (int i = 0; i < mLastInvokedStateByUid.size(); i++) { + pw.format(" uid=%s isVisibleInScreenRecording=%s\n", mLastInvokedStateByUid.keyAt(i), + mLastInvokedStateByUid.valueAt(i)); } } } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 8f9ed8353456..5b5177683252 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5070,7 +5070,7 @@ class Task extends TaskFragment { mHandler.removeMessages(TRANSLUCENT_TIMEOUT_MSG); if (waitingActivity != null) { - mWmService.setWindowOpaqueLocked(waitingActivity.token, false); + waitingActivity.setMainWindowOpaque(false); if (waitingActivity.attachedToProcess()) { try { waitingActivity.app.getThread().scheduleTranslucentConversionComplete( diff --git a/services/core/java/com/android/server/wm/TrustedOverlayHost.java b/services/core/java/com/android/server/wm/TrustedOverlayHost.java index f8edc2b871be..debe7946dc95 100644 --- a/services/core/java/com/android/server/wm/TrustedOverlayHost.java +++ b/services/core/java/com/android/server/wm/TrustedOverlayHost.java @@ -88,7 +88,17 @@ class TrustedOverlayHost { void addOverlay(SurfaceControlViewHost.SurfacePackage p, SurfaceControl currentParent) { requireOverlaySurfaceControl(); - mOverlays.add(p); + + boolean hasExistingOverlay = false; + for (int i = mOverlays.size() - 1; i >= 0; i--) { + SurfaceControlViewHost.SurfacePackage l = mOverlays.get(i); + if (l.getSurfaceControl().isSameSurface(p.getSurfaceControl())) { + hasExistingOverlay = true; + } + } + if (!hasExistingOverlay) { + mOverlays.add(p); + } SurfaceControl.Transaction t = mWmService.mTransactionFactory.get(); t.reparent(p.getSurfaceControl(), mSurfaceControl) diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 426694d178af..554cbce510d8 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -158,6 +158,7 @@ import static com.android.window.flags.Flags.multiCrop; import android.Manifest; import android.Manifest.permission; import android.animation.ValueAnimator; +import android.annotation.EnforcePermission; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; @@ -1073,7 +1074,7 @@ public class WindowManagerService extends IWindowManager.Stub @Override public void onAppTransitionFinishedLocked(IBinder token) { - final ActivityRecord atoken = mRoot.getActivityRecord(token); + final ActivityRecord atoken = ActivityRecord.forTokenLocked(token); if (atoken == null) { return; } @@ -3105,13 +3106,6 @@ public class WindowManagerService extends IWindowManager.Stub return mRecentsAnimationController != null && mRecentsAnimationController.isTargetApp(r); } - void setWindowOpaqueLocked(IBinder token, boolean isOpaque) { - final ActivityRecord wtoken = mRoot.getActivityRecord(token); - if (wtoken != null) { - wtoken.setMainWindowOpaque(isOpaque); - } - } - boolean isValidPictureInPictureAspectRatio(DisplayContent displayContent, float aspectRatio) { return displayContent.getPinnedTaskController().isValidPictureInPictureAspectRatio( aspectRatio); @@ -3292,7 +3286,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @android.annotation.EnforcePermission(android.Manifest.permission.DISABLE_KEYGUARD) + @EnforcePermission(android.Manifest.permission.DISABLE_KEYGUARD) /** * @see android.app.KeyguardManager#exitKeyguardSecurely */ @@ -4520,7 +4514,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS) + @EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS) @Override public SurfaceControl addShellRoot(int displayId, IWindow client, @WindowManager.ShellRootLayer int shellRootLayer) { @@ -4539,7 +4533,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS) + @EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS) @Override public void setShellRootAccessibilityWindow(int displayId, @WindowManager.ShellRootLayer int shellRootLayer, IWindow target) { @@ -4562,7 +4556,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS) + @EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS) @Override public void setDisplayWindowInsetsController( int displayId, IDisplayWindowInsetsController insetsController) { @@ -4581,7 +4575,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS) + @EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS) @Override public void updateDisplayWindowRequestedVisibleTypes( int displayId, @InsetsType int requestedVisibleTypes) { @@ -5841,7 +5835,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @android.annotation.EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) @Override public void setForcedDisplaySize(int displayId, int width, int height) { setForcedDisplaySize_enforcePermission(); @@ -5859,7 +5853,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @android.annotation.EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) @Override public void setForcedDisplayScalingMode(int displayId, int mode) { setForcedDisplayScalingMode_enforcePermission(); @@ -5947,7 +5941,7 @@ public class WindowManagerService extends IWindowManager.Stub return changed; } - @android.annotation.EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) @Override public void clearForcedDisplaySize(int displayId) { clearForcedDisplaySize_enforcePermission(); @@ -6010,7 +6004,7 @@ public class WindowManagerService extends IWindowManager.Stub return -1; } - @android.annotation.EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) @Override public void setForcedDisplayDensityForUser(int displayId, int density, int userId) { setForcedDisplayDensityForUser_enforcePermission(); @@ -6036,7 +6030,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @android.annotation.EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) @Override public void clearForcedDisplayDensityForUser(int displayId, int userId) { clearForcedDisplayDensityForUser_enforcePermission(); @@ -6536,7 +6530,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @android.annotation.EnforcePermission(android.Manifest.permission.STATUS_BAR) + @EnforcePermission(android.Manifest.permission.STATUS_BAR) public void setNavBarVirtualKeyHapticFeedbackEnabled(boolean enabled) { setNavBarVirtualKeyHapticFeedbackEnabled_enforcePermission(); @@ -6578,7 +6572,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @android.annotation.EnforcePermission(android.Manifest.permission.RESTRICTED_VR_ACCESS) + @EnforcePermission(android.Manifest.permission.RESTRICTED_VR_ACCESS) @Override public Region getCurrentImeTouchRegion() { getCurrentImeTouchRegion_enforcePermission(); @@ -8468,12 +8462,13 @@ public class WindowManagerService extends IWindowManager.Stub SurfaceControlViewHost.SurfacePackage overlay) { if (overlay == null) { throw new IllegalArgumentException("Invalid overlay passed in for task=" + taskId); - } else if (overlay.getSurfaceControl() == null - || !overlay.getSurfaceControl().isValid()) { - throw new IllegalArgumentException( - "Invalid overlay surfacecontrol passed in for task=" + taskId); } synchronized (mGlobalLock) { + if (overlay.getSurfaceControl() == null + || !overlay.getSurfaceControl().isValid()) { + throw new IllegalArgumentException( + "Invalid overlay surfacecontrol passed in for task=" + taskId); + } final Task task = mRoot.getRootTask(taskId); if (task == null) { throw new IllegalArgumentException("no task with taskId" + taskId); @@ -9956,13 +9951,17 @@ public class WindowManagerService extends IWindowManager.Stub mTrustedPresentationListenerController.unregisterListener(listener, id); } + @EnforcePermission(android.Manifest.permission.DETECT_SCREEN_RECORDING) @Override public boolean registerScreenRecordingCallback(IScreenRecordingCallback callback) { + registerScreenRecordingCallback_enforcePermission(); return mScreenRecordingCallbackController.register(callback); } + @EnforcePermission(android.Manifest.permission.DETECT_SCREEN_RECORDING) @Override public void unregisterScreenRecordingCallback(IScreenRecordingCallback callback) { + unregisterScreenRecordingCallback_enforcePermission(); mScreenRecordingCallbackController.unregister(callback); } diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp index 56423b961813..897afbfd2046 100644 --- a/services/tests/InputMethodSystemServerTests/Android.bp +++ b/services/tests/InputMethodSystemServerTests/Android.bp @@ -73,7 +73,6 @@ android_ravenwood_test { "androidx.annotation_annotation", "androidx.test.rules", "framework", - "mockito_ravenwood", "ravenwood-runtime", "ravenwood-utils", "services", diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index 75febd902dcf..42bcb3367933 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -103,6 +103,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemProperties; import android.platform.test.flag.junit.SetFlagsRule; +import android.util.SparseArray; import android.view.ContentRecordingSession; import android.view.Display; import android.view.DisplayAdjustments; @@ -432,7 +433,7 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); @@ -507,7 +508,7 @@ public class DisplayManagerServiceTest { assertTrue(expectedDisplayTypeToViewPortTypeMapping.keySet().contains(info.type)); } - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); @@ -559,7 +560,7 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); @@ -594,7 +595,7 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, /* now= */ 0); @@ -632,7 +633,7 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, /* now= */ 0); @@ -667,7 +668,7 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, /* now= */ 0); @@ -947,7 +948,7 @@ public class DisplayManagerServiceTest { PACKAGE_NAME); verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); @@ -1439,7 +1440,7 @@ public class DisplayManagerServiceTest { PACKAGE_NAME); verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); @@ -1694,7 +1695,7 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); @@ -1728,7 +1729,7 @@ public class DisplayManagerServiceTest { null /* projection */, PACKAGE_NAME); verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId); assertNotNull(ddi); @@ -1797,7 +1798,7 @@ public class DisplayManagerServiceTest { mock(DisplayWindowPolicyController.class), PACKAGE_NAME); verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId); assertNotNull(ddi); @@ -2912,6 +2913,11 @@ public class DisplayManagerServiceTest { assertEquals(expectedMode, displayInfo.getMode()); } + private void performTraversalInternal(DisplayManagerService displayManager) { + displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class), + new SparseArray<>()); + } + private int getDisplayIdForDisplayDevice( DisplayManagerService displayManager, DisplayManagerService.BinderService displayManagerBinderService, diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp index f49f6383b3c8..64fef68e387a 100644 --- a/services/tests/powerstatstests/Android.bp +++ b/services/tests/powerstatstests/Android.bp @@ -81,7 +81,6 @@ android_ravenwood_test { "androidx.annotation_annotation", "androidx.test.rules", "truth", - "mockito_ravenwood", ], srcs: [ ":power_stats_ravenwood_tests", diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 5611415c8fb1..ad6e2c657c96 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -153,7 +153,6 @@ android_ravenwood_test { static_libs: [ "androidx.annotation_annotation", "androidx.test.rules", - "mockito_ravenwood", "services.core", ], srcs: [ diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index f96d9e841eba..9cdaec647a43 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java @@ -18,6 +18,7 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; import static android.adaptiveauth.Flags.FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS; import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED; +import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START; import static com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR; @@ -517,6 +518,16 @@ public class FingerprintAuthenticationClientTest { } @Test + public void testAuthenticationStateListeners_onAuthenticationAcquired() + throws RemoteException { + final FingerprintAuthenticationClient client = createClient(); + client.start(mCallback); + client.onAcquired(FINGERPRINT_ACQUIRED_START, 0); + + verify(mAuthenticationStateListeners).onAuthenticationAcquired(any(), anyInt(), anyInt()); + } + + @Test public void testAuthenticationStateListeners_onAuthenticationSucceeded() throws RemoteException { mSetFlagsRule.enableFlags(FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS); diff --git a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java index 1c6d36b0a0d2..ea84eb2fbf73 100644 --- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java +++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java @@ -36,6 +36,7 @@ import android.os.IBinder; import android.os.IDumpstateListener; import android.os.Process; import android.os.RemoteException; +import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -48,6 +49,7 @@ import androidx.test.runner.AndroidJUnit4; 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; @@ -118,6 +120,7 @@ public class BugreportManagerServiceImplTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_ONBOARDING_BUGREPORT_V2_ENABLED) public void testBugreportFileManagerFileExists() { Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage); mBugreportFileManager.addBugreportFileForCaller( @@ -137,6 +140,7 @@ public class BugreportManagerServiceImplTest { @Test @RequiresFlagsEnabled(Flags.FLAG_ONBOARDING_BUGREPORT_V2_ENABLED) + @Ignore public void testBugreportFileManagerKeepFilesOnRetrieval() { Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage); mBugreportFileManager.addBugreportFileForCaller( @@ -150,6 +154,7 @@ public class BugreportManagerServiceImplTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_ONBOARDING_BUGREPORT_V2_ENABLED) public void testBugreportFileManagerMultipleFiles() { Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage); mBugreportFileManager.addBugreportFileForCaller( diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java index 839cf7ce4926..c247c08c8010 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java @@ -82,6 +82,7 @@ import android.content.res.Resources; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; +import android.os.IpcDataCache; import android.os.PowerManager; import android.os.PowerManagerInternal; import android.os.PowerSaveState; @@ -174,6 +175,11 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { @Before + public void disableProcessCaches() { + IpcDataCache.disableForTestMode(); + } + + @Before public void setUp() { // The AIDL stub will use PermissionEnforcer to check permission from the caller. mPermissionEnforcer = new FakePermissionEnforcer(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 5d114f4bb702..74622014aa5c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -5202,7 +5202,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // rules for a missing package, created a long time ago and deleted a long time ago config.deletedRules.put("del6", newZenRule("bad_pkg", twoMonthsAgo, twoMonthsAgo)); - mZenModeHelper.onUserUnlocked(42); // copies config and cleans it up. + mZenModeHelper.onUserSwitched(42); // copies config and cleans it up. assertThat(mZenModeHelper.mConfig.automaticRules.keySet()) .containsExactly("ar1", "ar2", "ar3", "ar4"); diff --git a/services/tests/wmtests/src/com/android/server/wm/AssistDataRequesterTest.java b/services/tests/wmtests/src/com/android/server/wm/AssistDataRequesterTest.java index 56c3ec0ffe2d..c2a5824d4ffd 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AssistDataRequesterTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AssistDataRequesterTest.java @@ -146,7 +146,7 @@ public class AssistDataRequesterTest { private void setupMocks(boolean currentActivityAssistAllowed, boolean assistStructureAllowed, boolean assistScreenshotAllowed) throws Exception { - doReturn(currentActivityAssistAllowed).when(mAtm).isAssistDataAllowedOnCurrentActivity(); + doReturn(currentActivityAssistAllowed).when(mAtm).isAssistDataAllowed(); doReturn(assistStructureAllowed ? MODE_ALLOWED : MODE_ERRORED).when(mAppOpsManager) .noteOpNoThrow(eq(OP_ASSIST_STRUCTURE), anyInt(), anyString(), any(), any()); doReturn(assistScreenshotAllowed ? MODE_ALLOWED : MODE_ERRORED).when(mAppOpsManager) diff --git a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java index 09f677e701df..46e14d51b5fd 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java @@ -16,8 +16,6 @@ package com.android.server.wm; -import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; - import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -38,13 +36,12 @@ import android.app.servertransaction.ClientTransactionItem; import android.os.IBinder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; -import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.filters.SmallTest; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -56,13 +53,8 @@ import org.mockito.MockitoAnnotations; */ @SmallTest @Presubmit -public class ClientLifecycleManagerTests { - - @Rule(order = 0) - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); - - @Rule(order = 1) - public final SystemServicesTestRule mSystemServices = new SystemServicesTestRule(); +@RunWith(WindowTestRunner.class) +public class ClientLifecycleManagerTests extends SystemServiceTestsBase { @Mock private IBinder mClientBinder; @@ -86,7 +78,7 @@ public class ClientLifecycleManagerTests { public void setup() { MockitoAnnotations.initMocks(this); - mWms = mSystemServices.getWindowManagerService(); + mWms = mSystemServicesTestRule.getWindowManagerService(); mLifecycleManager = spy(new ClientLifecycleManager()); mLifecycleManager.setWindowManager(mWms); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index be837441ef94..0ee78e38301c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -1802,8 +1802,7 @@ class WindowTestsBase extends SystemServiceTestsBase { @Override public void addStartingWindow(StartingWindowInfo info) { synchronized (mWMService.mGlobalLock) { - final ActivityRecord activity = mWMService.mRoot.getActivityRecord( - info.appToken); + final ActivityRecord activity = ActivityRecord.forTokenLocked(info.appToken); IWindow iWindow = mock(IWindow.class); doReturn(mock(IBinder.class)).when(iWindow).asBinder(); final WindowState window = WindowTestsBase.createWindow(null, @@ -1825,8 +1824,7 @@ class WindowTestsBase extends SystemServiceTestsBase { final IBinder appToken = mTaskAppMap.get(removalInfo.taskId); if (appToken != null) { mTaskAppMap.remove(removalInfo.taskId); - final ActivityRecord activity = mWMService.mRoot.getActivityRecord( - appToken); + final ActivityRecord activity = ActivityRecord.forTokenLocked(appToken); WindowState win = mAppWindowMap.remove(appToken); activity.removeChild(win); activity.mStartingWindow = null; diff --git a/telephony/java/android/telephony/NetworkRegistrationInfo.java b/telephony/java/android/telephony/NetworkRegistrationInfo.java index 7ccc27e2f94e..0c324e6061cb 100644 --- a/telephony/java/android/telephony/NetworkRegistrationInfo.java +++ b/telephony/java/android/telephony/NetworkRegistrationInfo.java @@ -649,12 +649,19 @@ public final class NetworkRegistrationInfo implements Parcelable { } /** - * @return Reason for denial if the registration state is {@link #REGISTRATION_STATE_DENIED}. - * Depending on {@code accessNetworkTechnology}, the values are defined in 3GPP TS 24.008 - * 10.5.3.6 for UMTS, 3GPP TS 24.301 9.9.3.9 for LTE, and 3GPP2 A.S0001 6.2.2.44 for CDMA - * @hide + * Get the 3GPP/3GPP2 reason code indicating why registration failed. + * + * Returns the reason code for non-transient registration failures. Typically this method will + * only return the last reason code received during a network selection procedure. The reason + * code is system-specific; however, the reason codes for both 3GPP and 3GPP2 systems are + * largely equivalent across generations. + * + * @return registration reject cause if available, otherwise 0. Depending on + * {@link #getAccessNetworkTechnology}, the values are defined in 3GPP TS 24.008 10.5.3.6 for + * WCDMA/UMTS, 3GPP TS 24.301 9.9.3.9 for LTE/EPS, 3GPP 24.501 Annex A for NR/5GS, or 3GPP2 + * A.S0001 6.2.2.44 for CDMA. */ - @SystemApi + @FlaggedApi(Flags.FLAG_NETWORK_REGISTRATION_INFO_REJECT_CAUSE) public int getRejectCause() { return mRejectCause; } |