diff options
279 files changed, 5438 insertions, 2031 deletions
diff --git a/Android.bp b/Android.bp index 444725eb2c79..127556f8e075 100644 --- a/Android.bp +++ b/Android.bp @@ -415,6 +415,7 @@ java_defaults { "mimemap", "av-types-aidl-java", "tv_tuner_resource_manager_aidl_interface-java", + "media_quality_aidl_interface-java", "soundtrigger_middleware-aidl-java", "modules-utils-binary-xml", "modules-utils-build", diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 526a213a6003..132c65cc26ee 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -410,6 +410,7 @@ package android.os { method public void invalidateCache(); method public static void invalidateCache(@NonNull String, @NonNull String); method @Nullable public Result query(@NonNull Query); + method @FlaggedApi("android.os.ipc_data_cache_test_apis") public static void setTestMode(boolean); field public static final String MODULE_BLUETOOTH = "bluetooth"; } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index d651010b641a..9c74471c65f1 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -2459,7 +2459,7 @@ package android.os { method public static void invalidateCache(@NonNull String, @NonNull String); method public final boolean isDisabled(); method @Nullable public Result query(@NonNull Query); - method public static void setTestMode(boolean); + method @FlaggedApi("android.os.ipc_data_cache_test_apis") public static void setTestMode(boolean); field public static final String MODULE_BLUETOOTH = "bluetooth"; field public static final String MODULE_SYSTEM = "system_server"; field public static final String MODULE_TEST = "test"; diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java index d84a4c12a2cd..d5b2f980e1a6 100644 --- a/core/java/android/animation/AnimationHandler.java +++ b/core/java/android/animation/AnimationHandler.java @@ -384,6 +384,12 @@ public class AnimationHandler { }); } + void removePendingEndAnimationCallback(Runnable notifyEndAnimation) { + if (mPendingEndAnimationListeners != null) { + mPendingEndAnimationListeners.remove(notifyEndAnimation); + } + } + private void doAnimationFrame(long frameTime) { long currentTime = SystemClock.uptimeMillis(); final int size = mAnimationCallbacks.size(); diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java index 4bf87f91cb2f..e62cd556af9a 100644 --- a/core/java/android/animation/Animator.java +++ b/core/java/android/animation/Animator.java @@ -82,6 +82,12 @@ public abstract class Animator implements Cloneable { static boolean sPostNotifyEndListenerEnabled; /** + * If {@link #sPostNotifyEndListenerEnabled} is enabled, it will be set when the end callback + * is scheduled. It is cleared when it runs or finishes immediately, e.g. cancel. + */ + private Runnable mPendingEndCallback; + + /** * A cache of the values in a list. Used so that when calling the list, we have a copy * of it in case the list is modified while iterating. The array can be reused to avoid * allocation on every notification. @@ -660,10 +666,33 @@ public abstract class Animator implements Cloneable { } } + /** + * This is called when the animator needs to finish immediately. This is usually no-op unless + * {@link #sPostNotifyEndListenerEnabled} is enabled and a finish request calls around the last + * animation frame. + * + * @param notifyListeners Whether to invoke {@link AnimatorListener#onAnimationEnd}. + * @return {@code true} if the pending listeners are removed. + */ + boolean consumePendingEndListeners(boolean notifyListeners) { + if (mPendingEndCallback == null) { + return false; + } + AnimationHandler.getInstance().removePendingEndAnimationCallback(mPendingEndCallback); + mPendingEndCallback = null; + if (notifyListeners) { + notifyEndListeners(false /* isReversing */); + } + return true; + } + void notifyEndListenersFromEndAnimation(boolean isReversing, boolean postNotifyEndListener) { if (postNotifyEndListener) { - AnimationHandler.getInstance().postEndAnimationCallback( - () -> completeEndAnimation(isReversing, "postNotifyAnimEnd")); + mPendingEndCallback = () -> { + completeEndAnimation(isReversing, "postNotifyAnimEnd"); + mPendingEndCallback = null; + }; + AnimationHandler.getInstance().postEndAnimationCallback(mPendingEndCallback); } else { completeEndAnimation(isReversing, "notifyAnimEnd"); } diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java index 78566d2fe98d..4a07de0410ae 100644 --- a/core/java/android/animation/AnimatorSet.java +++ b/core/java/android/animation/AnimatorSet.java @@ -423,6 +423,13 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim notifyListeners(AnimatorCaller.ON_CANCEL, false); callOnPlayingSet(Animator::cancel); mPlayingSet.clear(); + // If the end callback is pending, invoke the end callbacks of the animator nodes before + // ending this set. Pass notifyListeners=false because this endAnimation will do that. + if (consumePendingEndListeners(false /* notifyListeners */)) { + for (int i = mNodeMap.size() - 1; i >= 0; i--) { + mNodeMap.keyAt(i).consumePendingEndListeners(true /* notifyListeners */); + } + } endAnimation(); } } diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index 492c2ffc561f..fbcc73ea59e7 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -1182,6 +1182,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio // If end has already been requested, through a previous end() or cancel() call, no-op // until animation starts again. if (mAnimationEndRequested) { + consumePendingEndListeners(true /* notifyListeners */); return; } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 8d4925d8182d..127a08b04e87 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -6143,6 +6143,20 @@ public class Notification implements Parcelable result.mTitleMarginSet.applyToView(contentView, p.mTextViewId); contentView.setInt(p.mTextViewId, "setNumIndentLines", p.hasTitle() ? 0 : 1); } + // The expand button uses paddings rather than margins, so we'll adjust it + // separately. + adjustExpandButtonPadding(contentView, result.mRightIconVisible); + } + + private void adjustExpandButtonPadding(RemoteViews contentView, boolean rightIconVisible) { + if (notificationsRedesignTemplates()) { + final Resources res = mContext.getResources(); + int normalPadding = res.getDimensionPixelSize(R.dimen.notification_2025_margin); + int iconSpacing = res.getDimensionPixelSize( + R.dimen.notification_2025_expand_button_right_icon_spacing); + contentView.setInt(R.id.expand_button, "setStartPadding", + rightIconVisible ? iconSpacing : normalPadding); + } } // This code is executed on behalf of other apps' notifications, sometimes even by 3p apps, @@ -6154,12 +6168,21 @@ public class Notification implements Parcelable @NonNull TemplateBindResult result) { final Resources resources = mContext.getResources(); final float density = resources.getDisplayMetrics().density; - final float iconMarginDp = resources.getDimension( - R.dimen.notification_right_icon_content_margin) / density; + int iconMarginId = notificationsRedesignTemplates() + ? R.dimen.notification_2025_right_icon_content_margin + : R.dimen.notification_right_icon_content_margin; + final float iconMarginDp = resources.getDimension(iconMarginId) / density; final float contentMarginDp = resources.getDimension( R.dimen.notification_content_margin_end) / density; - final float expanderSizeDp = resources.getDimension( - R.dimen.notification_header_expand_icon_size) / density - contentMarginDp; + float spaceForExpanderDp; + if (notificationsRedesignTemplates()) { + spaceForExpanderDp = resources.getDimension( + R.dimen.notification_2025_right_icon_expanded_margin_end) / density + - contentMarginDp; + } else { + spaceForExpanderDp = resources.getDimension( + R.dimen.notification_header_expand_icon_size) / density - contentMarginDp; + } final float viewHeightDp = resources.getDimension( R.dimen.notification_right_icon_size) / density; float viewWidthDp = viewHeightDp; // icons are 1:1 by default @@ -6176,9 +6199,10 @@ public class Notification implements Parcelable } } } + // Margin needed for the header to accommodate the icon when shown final float extraMarginEndDpIfVisible = viewWidthDp + iconMarginDp; result.setRightIconState(rightIcon != null /* visible */, viewWidthDp, - viewHeightDp, extraMarginEndDpIfVisible, expanderSizeDp); + viewHeightDp, extraMarginEndDpIfVisible, spaceForExpanderDp); } /** @@ -14658,13 +14682,19 @@ public class Notification implements Parcelable public final MarginSet mTitleMarginSet = new MarginSet(); public void setRightIconState(boolean visible, float widthDp, float heightDp, - float marginEndDpIfVisible, float expanderSizeDp) { + float marginEndDpIfVisible, float spaceForExpanderDp) { mRightIconVisible = visible; mRightIconWidthDp = widthDp; mRightIconHeightDp = heightDp; - mHeadingExtraMarginSet.setValues(0, marginEndDpIfVisible); - mHeadingFullMarginSet.setValues(expanderSizeDp, marginEndDpIfVisible + expanderSizeDp); - mTitleMarginSet.setValues(0, marginEndDpIfVisible + expanderSizeDp); + mHeadingExtraMarginSet.setValues( + /* valueIfGone = */ 0, + /* valueIfVisible = */ marginEndDpIfVisible); + mHeadingFullMarginSet.setValues( + /* valueIfGone = */ spaceForExpanderDp, + /* valueIfVisible = */ marginEndDpIfVisible + spaceForExpanderDp); + mTitleMarginSet.setValues( + /* valueIfGone = */ 0, + /* valueIfVisible = */ marginEndDpIfVisible + spaceForExpanderDp); } /** diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index 38141cf20ce3..6e495768bfd4 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -1417,7 +1417,36 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * Enable or disable testing. The protocol requires that the mode toggle: for instance, it is + * Throw if the current process is not allowed to use test APIs. + */ + @android.ravenwood.annotation.RavenwoodReplace + private static void throwIfNotTest() { + final ActivityThread activityThread = ActivityThread.currentActivityThread(); + if (activityThread == null) { + // Only tests can reach here. + return; + } + final Instrumentation instrumentation = activityThread.getInstrumentation(); + if (instrumentation == null) { + // Only tests can reach here. + return; + } + if (instrumentation.isInstrumenting()) { + return; + } + if (Flags.enforcePicTestmodeProtocol()) { + throw new IllegalStateException("Test-only API called not from a test."); + } + } + + /** + * Do not throw if running under ravenwood. + */ + private static void throwIfNotTest$ravenwood() { + } + + /** + * Enable or disable test mode. The protocol requires that the mode toggle: for instance, it is * illegal to clear the test mode if the test mode is already off. Enabling test mode puts * all caches in the process into test mode; all nonces are initialized to UNSET and * subsequent reads and writes are to process memory. This has the effect of disabling all @@ -1425,10 +1454,12 @@ public class PropertyInvalidatedCache<Query, Result> { * operation. * @param mode The desired test mode. * @throws IllegalStateException if the supplied mode is already set. + * @throws IllegalStateException if the process is not running an instrumentation test. * @hide */ @VisibleForTesting public static void setTestMode(boolean mode) { + throwIfNotTest(); synchronized (sGlobalLock) { if (sTestMode == mode) { final String msg = "cannot set test mode redundantly: mode=" + mode; @@ -1464,9 +1495,11 @@ public class PropertyInvalidatedCache<Query, Result> { * for which it would not otherwise have permission. Caches in test mode do NOT write their * values to the system properties. The effect is local to the current process. Test mode * must be true when this method is called. + * @throws IllegalStateException if the process is not running an instrumentation test. * @hide */ public void testPropertyName() { + throwIfNotTest(); synchronized (sGlobalLock) { if (sTestMode == false) { throw new IllegalStateException("cannot test property name with test mode off"); @@ -1777,10 +1810,12 @@ public class PropertyInvalidatedCache<Query, Result> { * When multiple caches share a single property value, using an instance method on one of * the cache objects to invalidate all of the cache objects becomes confusing and you should * just use the static version of this function. + * @throws IllegalStateException if the process is not running an instrumentation test. * @hide */ @VisibleForTesting public void disableSystemWide() { + throwIfNotTest(); disableSystemWide(mPropertyName); } diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 5359ba44a3d2..73de1b67dc66 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -8269,6 +8269,7 @@ public class DevicePolicyManager { * * @throws SecurityException if the caller is not a device owner, a profile owner or * delegated certificate chooser. + * @throws IllegalArgumentException if {@code alias} does not correspond to an existing key * @see #grantKeyPairToWifiAuth */ public boolean isKeyPairGrantedToWifiAuth(@NonNull String alias) { diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 0ecd2754b1f0..572bffe6c6a4 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -402,3 +402,13 @@ flag { description: "Add new API for secondary lockscreen" bug: "336297680" } + +flag { + name: "remove_managed_esim_on_work_profile_deletion" + namespace: "enterprise" + description: "Remove managed eSIM when work profile is deleted" + bug: "347925470" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 5c904c15e706..7f4fd3eff57e 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -617,8 +617,8 @@ flag { } flag { - namespace: "multi_user" name: "logout_user_api" + namespace: "multiuser" description: "Add API to logout user" bug: "350045389" } diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java index e43a5fce6cb7..040dcfdeb998 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -183,11 +183,6 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private static native long nativeChanges(long connectionPtr); private static native long nativeTotalChanges(long connectionPtr); - // This method is deprecated and should be removed when it is no longer needed by the - // robolectric tests. It should not be called from any frameworks java code. - @Deprecated - private static native void nativeClose(long connectionPtr); - private SQLiteConnection(SQLiteConnectionPool pool, SQLiteDatabaseConfiguration configuration, int connectionId, boolean primaryConnection) { diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java index 0964cde5a1f4..c3ec96d17437 100644 --- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java +++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java @@ -145,8 +145,17 @@ public final class MessageQueue { } if (Flags.forceConcurrentMessageQueue()) { - sIsProcessAllowedToUseConcurrent = true; - return; + // b/379472827: Robolectric tests use reflection to access MessageQueue.mMessages. + // This is a hack to allow Robolectric tests to use the legacy implementation. + try { + Class.forName("org.robolectric.Robolectric"); + } catch (ClassNotFoundException e) { + // This is not a Robolectric test. + sIsProcessAllowedToUseConcurrent = true; + return; + } + // This is a Robolectric test. + // Continue to the following checks. } final String processName = Process.myProcessName(); diff --git a/core/java/android/os/IpcDataCache.java b/core/java/android/os/IpcDataCache.java index 2e7c3be53d90..e888f520b842 100644 --- a/core/java/android/os/IpcDataCache.java +++ b/core/java/android/os/IpcDataCache.java @@ -718,7 +718,7 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, } /** - * Enable or disable testing. The protocol requires that the mode toggle: for instance, it is + * Enable or disable test mode. The protocol requires that the mode toggle: for instance, it is * illegal to clear the test mode if the test mode is already off. Enabling test mode puts * all caches in the process into test mode; all nonces are initialized to UNSET and * subsequent reads and writes are to process memory. This has the effect of disabling all @@ -726,8 +726,11 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, * operation. * @param mode The desired test mode. * @throws IllegalStateException if the supplied mode is already set. + * @throws IllegalStateException if the process is not running an instrumentation test. * @hide */ + @FlaggedApi(android.os.Flags.FLAG_IPC_DATA_CACHE_TEST_APIS) + @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) @TestApi public static void setTestMode(boolean mode) { PropertyInvalidatedCache.setTestMode(mode); diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index 86acb2b21cfa..5d80119410e1 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -227,6 +227,14 @@ flag { } flag { + name: "ipc_data_cache_test_apis" + namespace: "system_performance" + description: "Expose IpcDataCache test apis to mainline modules." + bug: "396173886" + is_exported: true +} + +flag { name: "mainline_vcn_platform_api" namespace: "vcn" description: "Expose platform APIs to mainline VCN" diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java index a8a22f675e08..b82f278ef7d5 100644 --- a/core/java/android/os/health/SystemHealthManager.java +++ b/core/java/android/os/health/SystemHealthManager.java @@ -216,7 +216,7 @@ public class SystemHealthManager { /** * Gets the maximum number of TIDs this device supports for getting CPU headroom. * <p> - * See {@link CpuHeadroomParams#setTids(int...)}. + * See {@link CpuHeadroomParams.Builder#setTids(int...)}. * * @return the maximum size of TIDs supported * @throws UnsupportedOperationException if the CPU headroom API is unsupported. @@ -288,9 +288,7 @@ public class SystemHealthManager { /** * Gets the range of the calculation window size for CPU headroom. * <p> - * In API version 36, the range will be a superset of [50, 10000]. - * <p> - * See {@link CpuHeadroomParams#setCalculationWindowMillis(int)}. + * See {@link CpuHeadroomParams.Builder#setCalculationWindowMillis(int)}. * * @return the range of the calculation window size supported in milliseconds. * @throws UnsupportedOperationException if the CPU headroom API is unsupported. @@ -310,9 +308,7 @@ public class SystemHealthManager { /** * Gets the range of the calculation window size for GPU headroom. * <p> - * In API version 36, the range will be a superset of [50, 10000]. - * <p> - * See {@link GpuHeadroomParams#setCalculationWindowMillis(int)}. + * See {@link GpuHeadroomParams.Builder#setCalculationWindowMillis(int)}. * * @return the range of the calculation window size supported in milliseconds. * @throws UnsupportedOperationException if the GPU headroom API is unsupported. diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 0476f62ec263..4bd54a173601 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -71,6 +71,19 @@ flag { } flag { + name: "unknown_call_setting_blocked_logging_enabled" + is_exported: true + is_fixed_read_only: true + namespace: "permissions" + description: "enable the metrics when blocking certain app installs during an unknown call" + bug: "364535720" + + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "op_enable_mobile_data_by_user" is_exported: true namespace: "permissions" diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index b97c9b5e83f2..f91056dbce30 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -8651,6 +8651,34 @@ public final class Settings { public static final String DOCKED_CLOCK_FACE = "docked_clock_face"; /** + * Setting to indicate that content filters should be enabled on web browsers. + * + * <ul> + * <li>0 = Allow all sites + * <li>1 = Try to block explicit sites + * </ul> + * + * @hide + */ + @Readable + public static final String BROWSER_CONTENT_FILTERS_ENABLED = + "browser_content_filters_enabled"; + + /** + * Setting to indicate that content filters should be enabled in web search engines. + * + * <ul> + * <li>0 = Off + * <li>1 = Filter + * </ul> + * + * @hide + */ + @Readable + public static final String SEARCH_CONTENT_FILTERS_ENABLED = + "search_content_filters_enabled"; + + /** * Set by the system to track if the user needs to see the call to action for * the lockscreen notification policy. * @hide @@ -11132,6 +11160,12 @@ public final class Settings { public static final String DOUBLE_TAP_TO_WAKE = "double_tap_to_wake"; /** + * Controls whether double tap to sleep is enabled. + * @hide + */ + public static final String DOUBLE_TAP_TO_SLEEP = "double_tap_to_sleep"; + + /** * The current assistant component. It could be a voice interaction service, * or an activity that handles ACTION_ASSIST, or empty which means using the default * handling. diff --git a/core/java/android/security/advancedprotection/AdvancedProtectionManager.java b/core/java/android/security/advancedprotection/AdvancedProtectionManager.java index 770e234381c4..0b2239aa42b2 100644 --- a/core/java/android/security/advancedprotection/AdvancedProtectionManager.java +++ b/core/java/android/security/advancedprotection/AdvancedProtectionManager.java @@ -57,6 +57,7 @@ import java.util.concurrent.Executor; @SystemService(Context.ADVANCED_PROTECTION_SERVICE) public final class AdvancedProtectionManager { private static final String TAG = "AdvancedProtectionMgr"; + private static final String PKG_SETTINGS = "com.android.settings"; //TODO(b/378931989): Switch to android.app.admin.DevicePolicyIdentifiers.MEMORY_TAGGING_POLICY //when the appropriate flag is launched. @@ -343,6 +344,7 @@ public final class AdvancedProtectionManager { } Intent intent = new Intent(ACTION_SHOW_ADVANCED_PROTECTION_SUPPORT_DIALOG); + intent.setPackage(PKG_SETTINGS); intent.setFlags(FLAG_ACTIVITY_NEW_TASK); intent.putExtra(EXTRA_SUPPORT_DIALOG_FEATURE, featureId); intent.putExtra(EXTRA_SUPPORT_DIALOG_TYPE, type); diff --git a/core/java/android/util/MapCollections.java b/core/java/android/util/MapCollections.java index cce3a0e3eaa9..e7ceaae964ef 100644 --- a/core/java/android/util/MapCollections.java +++ b/core/java/android/util/MapCollections.java @@ -355,7 +355,12 @@ abstract class MapCollections<K, V> { } return result; } - }; + + @Override + public String toString() { + return toStringHelper(0, this, "KeySet"); + } + } final class ValuesCollection implements Collection<V> { @@ -456,7 +461,12 @@ abstract class MapCollections<K, V> { public <T> T[] toArray(T[] array) { return toArrayHelper(array, 1); } - }; + + @Override + public String toString() { + return toStringHelper(1, this, "ValuesCollection"); + } + } public static <K, V> boolean containsAllHelper(Map<K, V> map, Collection<?> collection) { Iterator<?> it = collection.iterator(); @@ -513,6 +523,29 @@ abstract class MapCollections<K, V> { return array; } + private String toStringHelper(int offset, Object thing, String thingName) { + int size = colGetSize(); + if (size == 0) { + return "[]"; + } + + StringBuilder buffer = new StringBuilder(size * 14); + buffer.append('['); + for (int i = 0; i < size; i++) { + if (i > 0) { + buffer.append(", "); + } + Object entry = colGetEntry(i, offset); + if (entry != thing) { + buffer.append(entry); + } else { + buffer.append("(this ").append(thingName).append(")"); + } + } + buffer.append(']'); + return buffer.toString(); + } + public static <T> boolean equalsSetHelper(Set<T> set, Object object) { if (set == object) { return true; diff --git a/core/java/android/window/DesktopExperienceFlags.java b/core/java/android/window/DesktopExperienceFlags.java index b4ff8e70ec36..cf582176a9f7 100644 --- a/core/java/android/window/DesktopExperienceFlags.java +++ b/core/java/android/window/DesktopExperienceFlags.java @@ -66,7 +66,6 @@ public enum DesktopExperienceFlags { false), ENABLE_PER_DISPLAY_PACKAGE_CONTEXT_CACHE_IN_STATUSBAR_NOTIF( Flags::enablePerDisplayPackageContextCacheInStatusbarNotif, false), - ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE(Flags::enableProjectedDisplayDesktopMode, false), ENABLE_TASKBAR_CONNECTED_DISPLAYS(Flags::enableTaskbarConnectedDisplays, false), ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS(Flags::enterDesktopByDefaultOnFreeformDisplays, false), diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 78769248c013..17165cdcf7b1 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -138,10 +138,14 @@ public enum DesktopModeFlags { ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS( Flags::enableWindowingTransitionHandlersObservers, false), EXCLUDE_CAPTION_FROM_APP_BOUNDS(Flags::excludeCaptionFromAppBounds, false), + FORCE_CLOSE_TOP_TRANSPARENT_FULLSCREEN_TASK( + Flags::forceCloseTopTransparentFullscreenTask, false), IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES( Flags::ignoreAspectRatioRestrictionsForResizeableFreeformActivities, true), INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC( - Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true) + Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true), + INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES( + Flags::inheritTaskBoundsForTrampolineTaskLaunches, false), // go/keep-sorted end ; diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 1f710c1cc8c0..ce3a101a7519 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -27,6 +27,17 @@ flag { } flag { + name: "inherit_task_bounds_for_trampoline_task_launches" + namespace: "lse_desktop_experience" + description: "Forces trampoline task launches to inherit the bounds of the previous instance /n" + "before is closes to prevent each task from cascading." + bug: "392815318" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "include_top_transparent_fullscreen_task_in_desktop_heuristic" namespace: "lse_desktop_experience" description: "Whether to include any top transparent fullscreen task launched in desktop /n" @@ -50,6 +61,17 @@ flag { } flag { + name: "force_close_top_transparent_fullscreen_task" + namespace: "lse_desktop_experience" + description: "If a top transparent fullscreen task is on top of desktop mode, force it to /n" + "close if another task is opened or brought to front." + bug: "395041610" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_windowing_dynamic_initial_bounds" namespace: "lse_desktop_experience" description: "Enables new initial bounds for desktop windowing which adjust depending on app constraints" diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index b4fec416bd5f..3927c713e500 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -86,6 +86,14 @@ flag { } flag { + name: "action_mode_edge_to_edge" + namespace: "windowing_frontend" + description: "Make contextual action bar edge-to-edge" + bug: "379783298" + is_fixed_read_only: true +} + +flag { name: "keyguard_going_away_timeout" namespace: "windowing_frontend" description: "Allow a maximum of 10 seconds with keyguardGoingAway=true before force-resetting" @@ -501,6 +509,17 @@ flag { description: "Sets Launch powermode for activity launches earlier" bug: "399380676" is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "scramble_snapshot_file_name" + namespace: "windowing_frontend" + description: "Scramble the file name of task snapshot." + bug: "293139053" + is_fixed_read_only: true metadata { purpose: PURPOSE_BUGFIX } diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index 81ca23173457..c6207f9451c2 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -45,11 +45,13 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.concurrent.locks.ReentrantLock; /** @@ -203,15 +205,6 @@ public class BatteryStatsHistory { BatteryHistoryFragment getLatestFragment(); /** - * Given a fragment, returns the earliest fragment that follows it whose monotonic - * start time falls within the specified range. `startTimeMs` is inclusive, `endTimeMs` - * is exclusive. - */ - @Nullable - BatteryHistoryFragment getNextFragment(BatteryHistoryFragment current, long startTimeMs, - long endTimeMs); - - /** * Acquires a lock on the entire store. */ void lock(); @@ -268,6 +261,60 @@ public class BatteryStatsHistory { void reset(); } + class BatteryHistoryParcelContainer { + private boolean mParcelReadyForReading; + private Parcel mParcel; + private BatteryStatsHistory.BatteryHistoryFragment mFragment; + private long mMonotonicStartTime; + + BatteryHistoryParcelContainer(@NonNull Parcel parcel, long monotonicStartTime) { + mParcel = parcel; + mMonotonicStartTime = monotonicStartTime; + mParcelReadyForReading = true; + } + + BatteryHistoryParcelContainer(@NonNull BatteryHistoryFragment fragment) { + mFragment = fragment; + mMonotonicStartTime = fragment.monotonicTimeMs; + mParcelReadyForReading = false; + } + + @Nullable + Parcel getParcel() { + if (mParcelReadyForReading) { + return mParcel; + } + + Parcel parcel = Parcel.obtain(); + if (readFragmentToParcel(parcel, mFragment)) { + parcel.readInt(); // skip buffer size + mParcel = parcel; + } else { + parcel.recycle(); + } + mParcelReadyForReading = true; + return mParcel; + } + + long getMonotonicStartTime() { + return mMonotonicStartTime; + } + + /** + * Recycles the parcel (if appropriate). Should be called after the parcel has been + * processed by the iterator. + */ + void close() { + if (mParcel != null && mFragment != null) { + mParcel.recycle(); + } + // ParcelContainers are not meant to be reusable. To prevent any unintentional + // access to the parcel after it has been recycled, clear the references to it. + mParcel = null; + mFragment = null; + } + } + private final Parcel mHistoryBuffer; private final HistoryStepDetailsCalculator mStepDetailsCalculator; private final Clock mClock; @@ -447,6 +494,7 @@ public class BatteryStatsHistory { mWritableHistory = writableHistory; if (mWritableHistory != null) { mMutable = false; + mHistoryBufferStartTime = mWritableHistory.mHistoryBufferStartTime; mHistoryMonotonicEndTime = mWritableHistory.mHistoryMonotonicEndTime; } @@ -689,95 +737,66 @@ public class BatteryStatsHistory { } /** - * When iterating history files and history buffer, always start from the lowest numbered - * history file, when reached the mActiveFile (highest numbered history file), do not read from - * mActiveFile, read from history buffer instead because the buffer has more updated data. - * - * @return The parcel that has next record. null if finished all history files and history - * buffer + * Returns all chunks of accumulated history that contain items within the range between + * `startTimeMs` (inclusive) and `endTimeMs` (exclusive) */ - @Nullable - public Parcel getNextParcel(long startTimeMs, long endTimeMs) { - checkImmutable(); + Queue<BatteryHistoryParcelContainer> getParcelContainers(long startTimeMs, long endTimeMs) { + if (mMutable) { + throw new IllegalStateException("Iterating over a mutable battery history"); + } - // First iterate through all records in current parcel. - if (mCurrentParcel != null) { - if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) { - // There are more records in current parcel. - return mCurrentParcel; - } else if (mHistoryBuffer == mCurrentParcel) { - // finished iterate through all history files and history buffer. - return null; - } else if (mHistoryParcels == null - || !mHistoryParcels.contains(mCurrentParcel)) { - // current parcel is from history file. - mCurrentParcel.recycle(); - } + if (endTimeMs == MonotonicClock.UNDEFINED || endTimeMs == 0) { + endTimeMs = Long.MAX_VALUE; } + Queue<BatteryHistoryParcelContainer> containers = new ArrayDeque<>(); + if (mStore != null) { - BatteryHistoryFragment next = mStore.getNextFragment(mCurrentFragment, startTimeMs, - endTimeMs); - while (next != null) { - mCurrentParcel = null; - mCurrentParcelEnd = 0; - final Parcel p = Parcel.obtain(); - if (readFragmentToParcel(p, next)) { - int bufSize = p.readInt(); - int curPos = p.dataPosition(); - mCurrentParcelEnd = curPos + bufSize; - mCurrentParcel = p; - if (curPos < mCurrentParcelEnd) { - mCurrentFragment = next; - return mCurrentParcel; - } - } else { - p.recycle(); + List<BatteryHistoryFragment> fragments = mStore.getFragments(); + for (int i = 0; i < fragments.size(); i++) { + BatteryHistoryFragment fragment = fragments.get(i); + if (fragment.monotonicTimeMs >= endTimeMs) { + break; + } + + if (fragment.monotonicTimeMs >= startTimeMs && fragment != mActiveFragment) { + containers.add(new BatteryHistoryParcelContainer(fragment)); } - next = mStore.getNextFragment(next, startTimeMs, endTimeMs); } } - // mHistoryParcels is created when BatteryStatsImpl object is created from deserialization - // of a parcel, such as Settings app or checkin file. if (mHistoryParcels != null) { - while (mParcelIndex < mHistoryParcels.size()) { - final Parcel p = mHistoryParcels.get(mParcelIndex++); + for (int i = 0; i < mHistoryParcels.size(); i++) { + final Parcel p = mHistoryParcels.get(i); if (!verifyVersion(p)) { continue; } - // skip monotonic time field. - p.readLong(); - // skip monotonic end time field - p.readLong(); + + long monotonicStartTime = p.readLong(); + if (monotonicStartTime >= endTimeMs) { + continue; + } + + long monotonicEndTime = p.readLong(); + if (monotonicEndTime < startTimeMs) { + continue; + } + // skip monotonic size field p.readLong(); + // skip buffer size field + p.readInt(); - final int bufSize = p.readInt(); - final int curPos = p.dataPosition(); - mCurrentParcelEnd = curPos + bufSize; - mCurrentParcel = p; - if (curPos < mCurrentParcelEnd) { - return mCurrentParcel; - } + containers.add(new BatteryHistoryParcelContainer(p, monotonicStartTime)); } } - // finished iterator through history files (except the last one), now history buffer. - if (mHistoryBuffer.dataSize() <= 0) { - // buffer is empty. - return null; - } - mHistoryBuffer.setDataPosition(0); - mCurrentParcel = mHistoryBuffer; - mCurrentParcelEnd = mCurrentParcel.dataSize(); - return mCurrentParcel; - } - - private void checkImmutable() { - if (mMutable) { - throw new IllegalStateException("Iterating over a mutable battery history"); + if (mHistoryBufferStartTime < endTimeMs) { + mHistoryBuffer.setDataPosition(0); + containers.add( + new BatteryHistoryParcelContainer(mHistoryBuffer, mHistoryBufferStartTime)); } + return containers; } /** @@ -818,21 +837,6 @@ public class BatteryStatsHistory { } /** - * Extracts the monotonic time, as per {@link MonotonicClock}, from the supplied battery history - * buffer. - */ - public long getHistoryBufferStartTime(Parcel p) { - int pos = p.dataPosition(); - p.setDataPosition(0); - p.readInt(); // Skip the version field - long monotonicTime = p.readLong(); - p.readLong(); // Skip monotonic end time field - p.readLong(); // Skip monotonic size field - p.setDataPosition(pos); - return monotonicTime; - } - - /** * Writes the battery history contents for persistence. */ public void writeSummaryToParcel(Parcel out, boolean inclHistory) { diff --git a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java index 38398b4bf6f0..09f9b0bf8c7e 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java +++ b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java @@ -24,6 +24,7 @@ import android.util.Slog; import android.util.SparseArray; import java.util.Iterator; +import java.util.Queue; /** * An iterator for {@link BatteryStats.HistoryItem}'s. @@ -48,7 +49,10 @@ public class BatteryStatsHistoryIterator implements Iterator<BatteryStats.Histor private long mBaseMonotonicTime; private long mBaseTimeUtc; private int mItemIndex = 0; - private int mMaxHistoryItems; + private final int mMaxHistoryItems; + private int mParcelDataPosition; + + private Queue<BatteryStatsHistory.BatteryHistoryParcelContainer> mParcelContainers; public BatteryStatsHistoryIterator(@NonNull BatteryStatsHistory history, long startTimeMs, long endTimeMs) { @@ -62,7 +66,11 @@ public class BatteryStatsHistoryIterator implements Iterator<BatteryStats.Histor @Override public boolean hasNext() { if (!mNextItemReady) { - advance(); + if (!advance()) { + mHistoryItem = null; + close(); + } + mNextItemReady = true; } return mHistoryItem != null; @@ -75,35 +83,48 @@ public class BatteryStatsHistoryIterator implements Iterator<BatteryStats.Histor @Override public BatteryStats.HistoryItem next() { if (!mNextItemReady) { - advance(); + if (!advance()) { + mHistoryItem = null; + close(); + } } mNextItemReady = false; return mHistoryItem; } - private void advance() { - while (true) { - if (mItemIndex > mMaxHistoryItems) { - Slog.wtfStack(TAG, "Number of battery history items is too large: " + mItemIndex); - break; - } + private boolean advance() { + if (mParcelContainers == null) { + mParcelContainers = mBatteryStatsHistory.getParcelContainers(mStartTimeMs, mEndTimeMs); + } - Parcel p = mBatteryStatsHistory.getNextParcel(mStartTimeMs, mEndTimeMs); - if (p == null) { - break; + BatteryStatsHistory.BatteryHistoryParcelContainer container; + while ((container = mParcelContainers.peek()) != null) { + Parcel p = container.getParcel(); + if (p == null || p.dataPosition() >= p.dataSize()) { + container.close(); + mParcelContainers.remove(); + mParcelDataPosition = 0; + continue; } if (!mTimeInitialized) { - mBaseMonotonicTime = mBatteryStatsHistory.getHistoryBufferStartTime(p); + mBaseMonotonicTime = container.getMonotonicStartTime(); mHistoryItem.time = mBaseMonotonicTime; mTimeInitialized = true; } try { readHistoryDelta(p, mHistoryItem); + int dataPosition = p.dataPosition(); + if (dataPosition <= mParcelDataPosition) { + Slog.wtf(TAG, "Corrupted battery history, parcel is not progressing: " + + dataPosition + " of " + p.dataSize()); + return false; + } + mParcelDataPosition = dataPosition; } catch (Throwable t) { Slog.wtf(TAG, "Corrupted battery history", t); - break; + return false; } if (mHistoryItem.cmd == BatteryStats.HistoryItem.CMD_CURRENT_TIME @@ -111,21 +132,24 @@ public class BatteryStatsHistoryIterator implements Iterator<BatteryStats.Histor mBaseTimeUtc = mHistoryItem.currentTime - (mHistoryItem.time - mBaseMonotonicTime); } - mHistoryItem.currentTime = mBaseTimeUtc + (mHistoryItem.time - mBaseMonotonicTime); + if (mHistoryItem.time < mStartTimeMs) { + continue; + } - if (mEndTimeMs != 0 && mHistoryItem.time >= mEndTimeMs) { - break; + if (mEndTimeMs != 0 && mEndTimeMs != MonotonicClock.UNDEFINED + && mHistoryItem.time >= mEndTimeMs) { + return false; } - if (mHistoryItem.time >= mStartTimeMs) { - mItemIndex++; - mNextItemReady = true; - return; + + if (mItemIndex++ > mMaxHistoryItems) { + Slog.wtfStack(TAG, "Number of battery history items is too large: " + mItemIndex); + return false; } - } - mHistoryItem = null; - mNextItemReady = true; - close(); + mHistoryItem.currentTime = mBaseTimeUtc + (mHistoryItem.time - mBaseMonotonicTime); + return true; + } + return false; } private void readHistoryDelta(Parcel src, BatteryStats.HistoryItem cur) { diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index e20a52b24485..3d81e4fc7acd 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -120,6 +120,7 @@ import com.android.internal.view.menu.MenuHelper; import com.android.internal.widget.ActionBarContextView; import com.android.internal.widget.BackgroundFallback; import com.android.internal.widget.floatingtoolbar.FloatingToolbar; +import com.android.window.flags.Flags; import java.util.List; import java.util.concurrent.Executor; @@ -1003,7 +1004,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind public void onWindowSystemUiVisibilityChanged(int visible) { updateColorViews(null /* insets */, true /* animate */); - if (mStatusGuard != null && mStatusGuard.getVisibility() == VISIBLE) { + if (!Flags.actionModeEdgeToEdge() + && mStatusGuard != null && mStatusGuard.getVisibility() == VISIBLE) { updateStatusGuardColor(); } } @@ -1040,7 +1042,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind } mFrameOffsets.set(insets.getSystemWindowInsetsAsRect()); insets = updateColorViews(insets, true /* animate */); - insets = updateStatusGuard(insets); + insets = updateActionModeInsets(insets); if (getForeground() != null) { drawableChanged(); } @@ -1592,7 +1594,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind } } - private WindowInsets updateStatusGuard(WindowInsets insets) { + private WindowInsets updateActionModeInsets(WindowInsets insets) { boolean showStatusGuard = false; // Show the status guard when the non-overlay contextual action bar is showing if (mPrimaryActionModeView != null) { @@ -1608,54 +1610,78 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind final Rect rect = mTempRect; // Apply the insets that have not been applied by the contentParent yet. - WindowInsets innerInsets = + final WindowInsets innerInsets = mWindow.mContentParent.computeSystemWindowInsets(insets, rect); - int newTopMargin = innerInsets.getSystemWindowInsetTop(); - int newLeftMargin = innerInsets.getSystemWindowInsetLeft(); - int newRightMargin = innerInsets.getSystemWindowInsetRight(); - - // Must use root window insets for the guard, because the color views consume - // the navigation bar inset if the window does not request LAYOUT_HIDE_NAV - but - // the status guard is attached at the root. - WindowInsets rootInsets = getRootWindowInsets(); - int newGuardLeftMargin = rootInsets.getSystemWindowInsetLeft(); - int newGuardRightMargin = rootInsets.getSystemWindowInsetRight(); - - if (mlp.topMargin != newTopMargin || mlp.leftMargin != newLeftMargin - || mlp.rightMargin != newRightMargin) { - mlpChanged = true; - mlp.topMargin = newTopMargin; - mlp.leftMargin = newLeftMargin; - mlp.rightMargin = newRightMargin; - } + final boolean consumesSystemWindowInsetsTop; + if (Flags.actionModeEdgeToEdge()) { + final Insets newPadding = innerInsets.getSystemWindowInsets(); + final Insets newMargin = innerInsets.getInsets( + WindowInsets.Type.navigationBars()); + + // Don't extend into navigation bar area so the width can align with status + // bar color view. + if (mlp.leftMargin != newMargin.left + || mlp.rightMargin != newMargin.right) { + mlpChanged = true; + mlp.leftMargin = newMargin.left; + mlp.rightMargin = newMargin.right; + } + + mPrimaryActionModeView.setPadding( + newPadding.left - newMargin.left, + newPadding.top, + newPadding.right - newMargin.right, + 0); + consumesSystemWindowInsetsTop = newPadding.top > 0; + } else { + int newTopMargin = innerInsets.getSystemWindowInsetTop(); + int newLeftMargin = innerInsets.getSystemWindowInsetLeft(); + int newRightMargin = innerInsets.getSystemWindowInsetRight(); + + // Must use root window insets for the guard, because the color views + // consume the navigation bar inset if the window does not request + // LAYOUT_HIDE_NAV - but the status guard is attached at the root. + WindowInsets rootInsets = getRootWindowInsets(); + int newGuardLeftMargin = rootInsets.getSystemWindowInsetLeft(); + int newGuardRightMargin = rootInsets.getSystemWindowInsetRight(); + + if (mlp.topMargin != newTopMargin || mlp.leftMargin != newLeftMargin + || mlp.rightMargin != newRightMargin) { + mlpChanged = true; + mlp.topMargin = newTopMargin; + mlp.leftMargin = newLeftMargin; + mlp.rightMargin = newRightMargin; + } - if (newTopMargin > 0 && mStatusGuard == null) { - mStatusGuard = new View(mContext); - mStatusGuard.setVisibility(GONE); - final LayoutParams lp = new LayoutParams(MATCH_PARENT, - mlp.topMargin, Gravity.LEFT | Gravity.TOP); - lp.leftMargin = newGuardLeftMargin; - lp.rightMargin = newGuardRightMargin; - addView(mStatusGuard, indexOfChild(mStatusColorViewState.view), lp); - } else if (mStatusGuard != null) { - final LayoutParams lp = (LayoutParams) - mStatusGuard.getLayoutParams(); - if (lp.height != mlp.topMargin || lp.leftMargin != newGuardLeftMargin - || lp.rightMargin != newGuardRightMargin) { - lp.height = mlp.topMargin; + if (newTopMargin > 0 && mStatusGuard == null) { + mStatusGuard = new View(mContext); + mStatusGuard.setVisibility(GONE); + final LayoutParams lp = new LayoutParams(MATCH_PARENT, + mlp.topMargin, Gravity.LEFT | Gravity.TOP); lp.leftMargin = newGuardLeftMargin; lp.rightMargin = newGuardRightMargin; - mStatusGuard.setLayoutParams(lp); + addView(mStatusGuard, indexOfChild(mStatusColorViewState.view), lp); + } else if (mStatusGuard != null) { + final LayoutParams lp = (LayoutParams) + mStatusGuard.getLayoutParams(); + if (lp.height != mlp.topMargin || lp.leftMargin != newGuardLeftMargin + || lp.rightMargin != newGuardRightMargin) { + lp.height = mlp.topMargin; + lp.leftMargin = newGuardLeftMargin; + lp.rightMargin = newGuardRightMargin; + mStatusGuard.setLayoutParams(lp); + } } - } - // The action mode's theme may differ from the app, so - // always show the status guard above it if we have one. - showStatusGuard = mStatusGuard != null; + // The action mode's theme may differ from the app, so + // always show the status guard above it if we have one. + showStatusGuard = mStatusGuard != null; - if (showStatusGuard && mStatusGuard.getVisibility() != VISIBLE) { - // If it wasn't previously shown, the color may be stale - updateStatusGuardColor(); + if (showStatusGuard && mStatusGuard.getVisibility() != VISIBLE) { + // If it wasn't previously shown, the color may be stale + updateStatusGuardColor(); + } + consumesSystemWindowInsetsTop = showStatusGuard; } // We only need to consume the insets if the action @@ -1664,14 +1690,16 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind // screen_simple_overlay_action_mode.xml). final boolean nonOverlay = (mWindow.getLocalFeaturesPrivate() & (1 << Window.FEATURE_ACTION_MODE_OVERLAY)) == 0; - if (nonOverlay && showStatusGuard) { + if (nonOverlay && consumesSystemWindowInsetsTop) { insets = insets.inset(0, insets.getSystemWindowInsetTop(), 0, 0); } } else { - // reset top margin - if (mlp.topMargin != 0 || mlp.leftMargin != 0 || mlp.rightMargin != 0) { - mlpChanged = true; - mlp.topMargin = 0; + if (!Flags.actionModeEdgeToEdge()) { + // reset top margin + if (mlp.topMargin != 0 || mlp.leftMargin != 0 || mlp.rightMargin != 0) { + mlpChanged = true; + mlp.topMargin = 0; + } } } if (mlpChanged) { @@ -1679,7 +1707,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind } } } - if (mStatusGuard != null) { + if (!Flags.actionModeEdgeToEdge() && mStatusGuard != null) { mStatusGuard.setVisibility(showStatusGuard ? VISIBLE : GONE); } return insets; @@ -2183,7 +2211,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind for (int i = getChildCount() - 1; i >= 0; i--) { View v = getChildAt(i); if (v != mStatusColorViewState.view && v != mNavigationColorViewState.view - && v != mStatusGuard) { + && (Flags.actionModeEdgeToEdge() || v != mStatusGuard)) { removeViewAt(i); } } diff --git a/core/java/com/android/internal/widget/ActionBarContextView.java b/core/java/com/android/internal/widget/ActionBarContextView.java index 80fc218839d5..d5bb51187ba4 100644 --- a/core/java/com/android/internal/widget/ActionBarContextView.java +++ b/core/java/com/android/internal/widget/ActionBarContextView.java @@ -34,6 +34,7 @@ import android.widget.TextView; import com.android.internal.R; import com.android.internal.view.menu.MenuBuilder; +import com.android.window.flags.Flags; /** * @hide @@ -315,12 +316,14 @@ public class ActionBarContextView extends AbsActionBarView { final int contentWidth = MeasureSpec.getSize(widthMeasureSpec); - int maxHeight = mContentHeight > 0 ? - mContentHeight : MeasureSpec.getSize(heightMeasureSpec); + final int maxHeight = !Flags.actionModeEdgeToEdge() && mContentHeight > 0 + ? mContentHeight : MeasureSpec.getSize(heightMeasureSpec); final int verticalPadding = getPaddingTop() + getPaddingBottom(); int availableWidth = contentWidth - getPaddingLeft() - getPaddingRight(); - final int height = maxHeight - verticalPadding; + final int height = Flags.actionModeEdgeToEdge() + ? mContentHeight > 0 ? mContentHeight : maxHeight + : maxHeight - verticalPadding; final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); if (mClose != null) { @@ -376,7 +379,8 @@ public class ActionBarContextView extends AbsActionBarView { } setMeasuredDimension(contentWidth, measuredHeight); } else { - setMeasuredDimension(contentWidth, maxHeight); + setMeasuredDimension(contentWidth, Flags.actionModeEdgeToEdge() + ? mContentHeight + verticalPadding : maxHeight); } } diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java index ff57fd4fe2ce..362b79db4003 100644 --- a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java +++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java @@ -294,54 +294,24 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar } } - private boolean applyInsets(View view, Rect insets, boolean toPadding, - boolean left, boolean top, boolean right, boolean bottom) { - boolean changed; - if (toPadding) { - changed = setMargin(view, EMPTY_RECT, left, top, right, bottom); - changed |= setPadding(view, insets, left, top, right, bottom); - } else { - changed = setPadding(view, EMPTY_RECT, left, top, right, bottom); - changed |= setMargin(view, insets, left, top, right, bottom); - } - return changed; - } - - private boolean setPadding(View view, Rect insets, - boolean left, boolean top, boolean right, boolean bottom) { - if ((left && view.getPaddingLeft() != insets.left) - || (top && view.getPaddingTop() != insets.top) - || (right && view.getPaddingRight() != insets.right) - || (bottom && view.getPaddingBottom() != insets.bottom)) { - view.setPadding( - left ? insets.left : view.getPaddingLeft(), - top ? insets.top : view.getPaddingTop(), - right ? insets.right : view.getPaddingRight(), - bottom ? insets.bottom : view.getPaddingBottom()); - return true; - } - return false; - } - - private boolean setMargin(View view, Rect insets, - boolean left, boolean top, boolean right, boolean bottom) { + private boolean setMargin(View view, int left, int top, int right, int bottom) { final LayoutParams lp = (LayoutParams) view.getLayoutParams(); boolean changed = false; - if (left && lp.leftMargin != insets.left) { + if (lp.leftMargin != left) { changed = true; - lp.leftMargin = insets.left; + lp.leftMargin = left; } - if (top && lp.topMargin != insets.top) { + if (lp.topMargin != top) { changed = true; - lp.topMargin = insets.top; + lp.topMargin = top; } - if (right && lp.rightMargin != insets.right) { + if (lp.rightMargin != right) { changed = true; - lp.rightMargin = insets.right; + lp.rightMargin = right; } - if (bottom && lp.bottomMargin != insets.bottom) { + if (lp.bottomMargin != bottom) { changed = true; - lp.bottomMargin = insets.bottom; + lp.bottomMargin = bottom; } return changed; } @@ -367,12 +337,30 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar final Insets sysInsets = insets.getSystemWindowInsets(); mSystemInsets.set(sysInsets.left, sysInsets.top, sysInsets.right, sysInsets.bottom); - // The top and bottom action bars are always within the content area. - boolean changed = applyInsets(mActionBarTop, mSystemInsets, - mActionBarExtendsIntoSystemInsets, true, true, true, false); - if (mActionBarBottom != null) { - changed |= applyInsets(mActionBarBottom, mSystemInsets, - mActionBarExtendsIntoSystemInsets, true, false, true, true); + boolean changed = false; + if (mActionBarExtendsIntoSystemInsets) { + // Don't extend into navigation bar area so the width can align with status bar + // color view. + final Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()); + final int paddingLeft = sysInsets.left - navBarInsets.left; + final int paddingRight = sysInsets.right - navBarInsets.right; + mActionBarTop.setPadding(paddingLeft, sysInsets.top, paddingRight, 0); + changed |= setMargin( + mActionBarTop, navBarInsets.left, 0, navBarInsets.right, 0); + if (mActionBarBottom != null) { + mActionBarBottom.setPadding(paddingLeft, 0, paddingRight, sysInsets.bottom); + changed |= setMargin( + mActionBarBottom, navBarInsets.left, 0, navBarInsets.right, 0); + } + } else { + mActionBarTop.setPadding(0, 0, 0, 0); + changed |= setMargin( + mActionBarTop, sysInsets.left, sysInsets.top, sysInsets.right, 0); + if (mActionBarBottom != null) { + mActionBarBottom.setPadding(0, 0, 0, 0); + changed |= setMargin( + mActionBarTop, sysInsets.left, 0, sysInsets.right, sysInsets.bottom); + } } // Cannot use the result of computeSystemWindowInsets, because that consumes the @@ -521,7 +509,12 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar ); } } - setMargin(mContent, mContentInsets, true, true, true, true); + setMargin( + mContent, + mContentInsets.left, + mContentInsets.top, + mContentInsets.right, + mContentInsets.bottom); if (!mLastInnerInsets.equals(mInnerInsets)) { // If the inner insets have changed, we need to dispatch this down to diff --git a/core/java/com/android/internal/widget/NotificationExpandButton.java b/core/java/com/android/internal/widget/NotificationExpandButton.java index dd12f69a56fb..42b1bdbc51b2 100644 --- a/core/java/com/android/internal/widget/NotificationExpandButton.java +++ b/core/java/com/android/internal/widget/NotificationExpandButton.java @@ -129,6 +129,16 @@ public class NotificationExpandButton extends FrameLayout { updateExpandedState(); } + /** + * Adjust the padding at the start of the view based on the layout direction (RTL/LTR). + * This is needed because RemoteViews don't have an equivalent for + * {@link this#setPaddingRelative}. + */ + @RemotableViewMethod + public void setStartPadding(int startPadding) { + setPaddingRelative(startPadding, getPaddingTop(), getPaddingEnd(), getPaddingBottom()); + } + private void updateExpandedState() { int drawableId; int contentDescriptionId; diff --git a/core/jni/android_database_SQLiteConnection.cpp b/core/jni/android_database_SQLiteConnection.cpp index 36c08a51c66a..95bab542f9d6 100644 --- a/core/jni/android_database_SQLiteConnection.cpp +++ b/core/jni/android_database_SQLiteConnection.cpp @@ -231,12 +231,6 @@ static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr, jboolean } } -// This method is deprecated and should be removed when it is no longer needed by the -// robolectric tests. -static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr) { - nativeClose(env, clazz, connectionPtr, false); -} - static void sqliteCustomScalarFunctionCallback(sqlite3_context *context, int argc, sqlite3_value **argv) { JNIEnv* env = AndroidRuntime::getJNIEnv(); @@ -974,8 +968,6 @@ static const JNINativeMethod sMethods[] = (void*)nativeOpen }, { "nativeClose", "(JZ)V", (void*) static_cast<void(*)(JNIEnv*,jclass,jlong,jboolean)>(nativeClose) }, - { "nativeClose", "(J)V", - (void*) static_cast<void(*)(JNIEnv*,jclass,jlong)>(nativeClose) }, { "nativeRegisterCustomScalarFunction", "(JLjava/lang/String;Ljava/util/function/UnaryOperator;)V", (void*)nativeRegisterCustomScalarFunction }, { "nativeRegisterCustomAggregateFunction", "(JLjava/lang/String;Ljava/util/function/BinaryOperator;)V", diff --git a/core/res/res/drawable/accessibility_autoclick_scroll_down.xml b/core/res/res/drawable/accessibility_autoclick_scroll_down.xml new file mode 100644 index 000000000000..13f1ba0dafe8 --- /dev/null +++ b/core/res/res/drawable/accessibility_autoclick_scroll_down.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2025 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="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/materialColorPrimary" + android:pathData="M12,20l8,-8h-5V4h-6v8H4z"/> +</vector> diff --git a/core/res/res/drawable/accessibility_autoclick_scroll_exit.xml b/core/res/res/drawable/accessibility_autoclick_scroll_exit.xml new file mode 100644 index 000000000000..e53301f25d65 --- /dev/null +++ b/core/res/res/drawable/accessibility_autoclick_scroll_exit.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2025 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="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/materialColorPrimary" + android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/> +</vector> diff --git a/core/res/res/drawable/accessibility_autoclick_scroll_left.xml b/core/res/res/drawable/accessibility_autoclick_scroll_left.xml new file mode 100644 index 000000000000..39475bc689d2 --- /dev/null +++ b/core/res/res/drawable/accessibility_autoclick_scroll_left.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2025 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="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/materialColorPrimary" + android:pathData="M4,12l8,8v-5h8v-6h-8V4z"/> +</vector> diff --git a/core/res/res/drawable/accessibility_autoclick_scroll_right.xml b/core/res/res/drawable/accessibility_autoclick_scroll_right.xml new file mode 100644 index 000000000000..bbd7b2a79459 --- /dev/null +++ b/core/res/res/drawable/accessibility_autoclick_scroll_right.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2025 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="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/materialColorPrimary" + android:pathData="M20,12l-8,-8v5H4v6h8v5z"/> +</vector> diff --git a/core/res/res/drawable/accessibility_autoclick_scroll_up.xml b/core/res/res/drawable/accessibility_autoclick_scroll_up.xml new file mode 100644 index 000000000000..2e2c245effa1 --- /dev/null +++ b/core/res/res/drawable/accessibility_autoclick_scroll_up.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2025 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="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/materialColorPrimary" + android:pathData="M12,4L4,12h5v8h6v-8h5z"/> +</vector> diff --git a/core/res/res/layout/accessibility_autoclick_scroll_panel.xml b/core/res/res/layout/accessibility_autoclick_scroll_panel.xml new file mode 100644 index 000000000000..1e093bbc7c35 --- /dev/null +++ b/core/res/res/layout/accessibility_autoclick_scroll_panel.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2025 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/accessibility_autoclick_scroll_panel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:background="@drawable/accessibility_autoclick_type_panel_rounded_background" + android:orientation="vertical" + android:padding="16dp"> + + <!-- Up arrow --> + <LinearLayout + android:id="@+id/scroll_up_layout" + style="@style/AccessibilityAutoclickScrollPanelButtonLayoutStyle" + android:layout_gravity="center_horizontal" + android:layout_marginBottom="@dimen/accessibility_autoclick_type_panel_button_spacing"> + <ImageButton + android:id="@+id/scroll_up" + style="@style/AccessibilityAutoclickScrollPanelImageButtonStyle" + android:contentDescription="@string/accessibility_autoclick_scroll_up" + android:src="@drawable/accessibility_autoclick_scroll_up" /> + </LinearLayout> + + <!-- Middle row: Left, Exit, Right --> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="horizontal"> + + <LinearLayout + android:id="@+id/scroll_left_layout" + style="@style/AccessibilityAutoclickScrollPanelButtonLayoutStyle" + android:layout_marginEnd="@dimen/accessibility_autoclick_type_panel_button_spacing"> + <ImageButton + android:id="@+id/scroll_left" + style="@style/AccessibilityAutoclickScrollPanelImageButtonStyle" + android:contentDescription="@string/accessibility_autoclick_scroll_left" + android:src="@drawable/accessibility_autoclick_scroll_left" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/scroll_exit_layout" + style="@style/AccessibilityAutoclickScrollPanelButtonLayoutStyle" + android:layout_marginEnd="@dimen/accessibility_autoclick_type_panel_button_spacing"> + <ImageButton + android:id="@+id/scroll_exit" + style="@style/AccessibilityAutoclickScrollPanelImageButtonStyle" + android:contentDescription="@string/accessibility_autoclick_scroll_exit" + android:src="@drawable/ic_close" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/scroll_right_layout" + style="@style/AccessibilityAutoclickScrollPanelButtonLayoutStyle"> + <ImageButton + android:id="@+id/scroll_right" + style="@style/AccessibilityAutoclickScrollPanelImageButtonStyle" + android:contentDescription="@string/accessibility_autoclick_scroll_right" + android:src="@drawable/accessibility_autoclick_scroll_right" /> + </LinearLayout> + </LinearLayout> + + <!-- Down arrow --> + <LinearLayout + android:id="@+id/scroll_down_layout" + style="@style/AccessibilityAutoclickScrollPanelButtonLayoutStyle" + android:layout_gravity="center_horizontal" + android:layout_marginTop="@dimen/accessibility_autoclick_type_panel_button_spacing"> + <ImageButton + android:id="@+id/scroll_down" + style="@style/AccessibilityAutoclickScrollPanelImageButtonStyle" + android:contentDescription="@string/accessibility_autoclick_scroll_down" + android:src="@drawable/accessibility_autoclick_scroll_down" /> + </LinearLayout> + +</LinearLayout> diff --git a/core/res/res/layout/notification_2025_expand_button.xml b/core/res/res/layout/notification_2025_expand_button.xml index 1c367544c90a..8ba844a4868b 100644 --- a/core/res/res/layout/notification_2025_expand_button.xml +++ b/core/res/res/layout/notification_2025_expand_button.xml @@ -15,6 +15,7 @@ --> <!-- extends FrameLayout --> +<!-- Note: The button's padding may be dynamically adjusted in code --> <com.android.internal.widget.NotificationExpandButton xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/expand_button" diff --git a/core/res/res/layout/notification_2025_right_icon.xml b/core/res/res/layout/notification_2025_right_icon.xml new file mode 100644 index 000000000000..24d381d10501 --- /dev/null +++ b/core/res/res/layout/notification_2025_right_icon.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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 + --> +<!-- Large icon to be used in expanded notification layouts. --> +<com.android.internal.widget.CachingIconView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/right_icon" + android:layout_width="@dimen/notification_right_icon_size" + android:layout_height="@dimen/notification_right_icon_size" + android:layout_gravity="top|end" + android:layout_marginEnd="@dimen/notification_2025_right_icon_expanded_margin_end" + android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin" + android:background="@drawable/notification_large_icon_outline" + android:clipToOutline="true" + android:importantForAccessibility="no" + android:scaleType="centerCrop" + android:maxDrawableWidth="@dimen/notification_right_icon_size" + android:maxDrawableHeight="@dimen/notification_right_icon_size" + /> diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml index d29b7af9e24e..fe8875e413c4 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_base.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml @@ -66,14 +66,17 @@ android:orientation="horizontal" > + <!-- + We use a smaller vertical margin than usual, to allow the content of custom views to + grow up to 48dp height when needed in collapsed notifications. + --> <LinearLayout android:id="@+id/notification_headerless_view_column" android:layout_width="0px" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_weight="1" - android:layout_marginBottom="@dimen/notification_2025_margin" - android:layout_marginTop="@dimen/notification_2025_margin" + android:layout_marginVertical="@dimen/notification_2025_reduced_margin" android:orientation="vertical" > @@ -81,6 +84,7 @@ android:id="@+id/notification_top_line" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="@dimen/notification_2025_additional_margin" android:minHeight="@dimen/notification_2025_content_min_height" android:clipChildren="false" android:theme="@style/Theme.DeviceDefault.Notification" @@ -118,6 +122,7 @@ <com.android.internal.widget.NotificationVanishingFrameLayout android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/notification_2025_additional_margin" android:minHeight="@dimen/notification_headerless_line_height" > <!-- This is the simplest way to keep this text vertically centered without @@ -146,9 +151,8 @@ android:layout_width="@dimen/notification_right_icon_size" android:layout_height="@dimen/notification_right_icon_size" android:layout_gravity="center_vertical|end" - android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" - android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" - android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin" + android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin" android:background="@drawable/notification_large_icon_outline" android:clipToOutline="true" android:importantForAccessibility="no" diff --git a/core/res/res/layout/notification_2025_template_collapsed_call.xml b/core/res/res/layout/notification_2025_template_collapsed_call.xml index ee691e4d6894..9fb63f6eff13 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_call.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_call.xml @@ -147,9 +147,8 @@ android:layout_width="@dimen/notification_right_icon_size" android:layout_height="@dimen/notification_right_icon_size" android:layout_gravity="center_vertical|end" - android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" - android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" - android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin" + android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin" android:background="@drawable/notification_large_icon_outline" android:clipToOutline="true" android:importantForAccessibility="no" diff --git a/core/res/res/layout/notification_2025_template_collapsed_conversation.xml b/core/res/res/layout/notification_2025_template_collapsed_conversation.xml index f80411103501..a6fdcd95399e 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_conversation.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_conversation.xml @@ -149,9 +149,9 @@ android:layout_width="@dimen/notification_right_icon_size" android:layout_height="@dimen/notification_right_icon_size" android:layout_gravity="center_vertical|end" - android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" - android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" - android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:layout_marginTop="@dimen/notification_2025_margin" + android:layout_marginBottom="@dimen/notification_2025_margin" + android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin" android:forceHasOverlappingRendering="false" android:spacing="0dp" android:clipChildren="false" @@ -163,9 +163,8 @@ android:layout_width="@dimen/notification_right_icon_size" android:layout_height="@dimen/notification_right_icon_size" android:layout_gravity="center_vertical|end" - android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" - android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" - android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin" + android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin" android:background="@drawable/notification_large_icon_outline" android:clipToOutline="true" android:importantForAccessibility="no" diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml index 5beab508aecf..7e84292b0924 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_media.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml @@ -68,14 +68,17 @@ android:orientation="horizontal" > + <!-- + We use a smaller vertical margin than usual, to allow the content of custom views to + grow up to 48dp height when needed in collapsed notifications. + --> <LinearLayout android:id="@+id/notification_headerless_view_column" android:layout_width="0px" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_weight="1" - android:layout_marginBottom="@dimen/notification_2025_margin" - android:layout_marginTop="@dimen/notification_2025_margin" + android:layout_marginVertical="@dimen/notification_2025_reduced_margin" android:orientation="vertical" > @@ -83,6 +86,7 @@ android:id="@+id/notification_top_line" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="@dimen/notification_2025_additional_margin" android:minHeight="@dimen/notification_headerless_line_height" android:clipChildren="false" android:theme="@style/Theme.DeviceDefault.Notification" @@ -119,6 +123,7 @@ <com.android.internal.widget.NotificationVanishingFrameLayout android:layout_width="match_parent" + android:layout_marginBottom="@dimen/notification_2025_additional_margin" android:layout_height="@dimen/notification_headerless_line_height" > <!-- This is the simplest way to keep this text vertically centered without @@ -147,9 +152,8 @@ android:layout_width="@dimen/notification_right_icon_size" android:layout_height="@dimen/notification_right_icon_size" android:layout_gravity="center_vertical|end" - android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" - android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" - android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin" + android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin" android:background="@drawable/notification_large_icon_outline" android:clipToOutline="true" android:importantForAccessibility="no" diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml index d7c3263904d4..3716fa6825b3 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml @@ -159,9 +159,9 @@ android:layout_width="@dimen/notification_right_icon_size" android:layout_height="@dimen/notification_right_icon_size" android:layout_gravity="center_vertical|end" - android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" - android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" - android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:layout_marginTop="@dimen/notification_2025_margin" + android:layout_marginBottom="@dimen/notification_2025_margin" + android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin" android:forceHasOverlappingRendering="false" android:spacing="0dp" android:clipChildren="false" @@ -173,9 +173,8 @@ android:layout_width="@dimen/notification_right_icon_size" android:layout_height="@dimen/notification_right_icon_size" android:layout_gravity="center_vertical|end" - android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" - android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" - android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin" + android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin" android:background="@drawable/notification_large_icon_outline" android:clipToOutline="true" android:importantForAccessibility="no" diff --git a/core/res/res/layout/notification_2025_template_expanded_base.xml b/core/res/res/layout/notification_2025_template_expanded_base.xml index e12db2783191..8d99e47c5386 100644 --- a/core/res/res/layout/notification_2025_template_expanded_base.xml +++ b/core/res/res/layout/notification_2025_template_expanded_base.xml @@ -63,7 +63,7 @@ /> </LinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </FrameLayout> <ViewStub diff --git a/core/res/res/layout/notification_2025_template_expanded_big_picture.xml b/core/res/res/layout/notification_2025_template_expanded_big_picture.xml index fac9d1c47f41..e8e460d1b4ae 100644 --- a/core/res/res/layout/notification_2025_template_expanded_big_picture.xml +++ b/core/res/res/layout/notification_2025_template_expanded_big_picture.xml @@ -25,7 +25,7 @@ <include layout="@layout/notification_2025_template_header" /> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> <LinearLayout android:id="@+id/notification_action_list_margin_target" diff --git a/core/res/res/layout/notification_2025_template_expanded_big_text.xml b/core/res/res/layout/notification_2025_template_expanded_big_text.xml index 4a807cb674c6..b68db7f0a638 100644 --- a/core/res/res/layout/notification_2025_template_expanded_big_text.xml +++ b/core/res/res/layout/notification_2025_template_expanded_big_text.xml @@ -91,5 +91,5 @@ <include layout="@layout/notification_material_action_list" /> </com.android.internal.widget.RemeasuringLinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </FrameLayout> diff --git a/core/res/res/layout/notification_2025_template_expanded_call.xml b/core/res/res/layout/notification_2025_template_expanded_call.xml index bbc29664d594..7b45b55ba15b 100644 --- a/core/res/res/layout/notification_2025_template_expanded_call.xml +++ b/core/res/res/layout/notification_2025_template_expanded_call.xml @@ -68,6 +68,6 @@ </com.android.internal.widget.RemeasuringLinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </com.android.internal.widget.CallLayout> diff --git a/core/res/res/layout/notification_2025_template_expanded_conversation.xml b/core/res/res/layout/notification_2025_template_expanded_conversation.xml index d7e8bb3b6da2..592785d53018 100644 --- a/core/res/res/layout/notification_2025_template_expanded_conversation.xml +++ b/core/res/res/layout/notification_2025_template_expanded_conversation.xml @@ -70,6 +70,6 @@ </com.android.internal.widget.RemeasuringLinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </com.android.internal.widget.ConversationLayout> diff --git a/core/res/res/layout/notification_2025_template_expanded_inbox.xml b/core/res/res/layout/notification_2025_template_expanded_inbox.xml index ccab02e312cc..6459e1eab862 100644 --- a/core/res/res/layout/notification_2025_template_expanded_inbox.xml +++ b/core/res/res/layout/notification_2025_template_expanded_inbox.xml @@ -130,5 +130,5 @@ android:layout_marginTop="@dimen/notification_content_margin" /> <include layout="@layout/notification_material_action_list" /> </LinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </FrameLayout> diff --git a/core/res/res/layout/notification_2025_template_expanded_media.xml b/core/res/res/layout/notification_2025_template_expanded_media.xml index e90ab792581f..801e339b3a92 100644 --- a/core/res/res/layout/notification_2025_template_expanded_media.xml +++ b/core/res/res/layout/notification_2025_template_expanded_media.xml @@ -99,6 +99,6 @@ </LinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </com.android.internal.widget.MediaNotificationView> diff --git a/core/res/res/layout/notification_2025_template_expanded_messaging.xml b/core/res/res/layout/notification_2025_template_expanded_messaging.xml index 20abfee6a4b6..82c71527a291 100644 --- a/core/res/res/layout/notification_2025_template_expanded_messaging.xml +++ b/core/res/res/layout/notification_2025_template_expanded_messaging.xml @@ -70,6 +70,6 @@ </com.android.internal.widget.RemeasuringLinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </com.android.internal.widget.MessagingLayout> diff --git a/core/res/res/layout/notification_2025_template_expanded_progress.xml b/core/res/res/layout/notification_2025_template_expanded_progress.xml index 87ded8975cb0..2ff252747fd2 100644 --- a/core/res/res/layout/notification_2025_template_expanded_progress.xml +++ b/core/res/res/layout/notification_2025_template_expanded_progress.xml @@ -99,7 +99,7 @@ </LinearLayout> </LinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </FrameLayout> <ViewStub diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 9773f557dfaa..8c5b20da73ef 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2812,6 +2812,20 @@ <integer name="config_dreamsBatteryLevelDrainCutoff">5</integer> <!-- Limit of how long the device can remain unlocked due to attention checking. --> <integer name="config_attentionMaximumExtension">900000</integer> <!-- 15 minutes. --> + + <!-- Enables or disables the 'prevent screen timeout' feature, where when a user manually + undims the screen, the feature acquires a wakelock to prevent screen timeout. + false = disabled, true = enabled. Disabled by default. --> + <bool name="config_defaultPreventScreenTimeoutEnabled">false</bool> + <!-- Default value (in milliseconds) to prevent the screen timeout after user undims it + manually between screen dims, a sign the user is interacting with the device. --> + <integer name="config_defaultPreventScreenTimeoutForMillis">300000</integer> <!-- 5 minutes. --> + <!-- Default max duration (in milliseconds) of the time between undims to still consider them + consecutive. --> + <integer name="config_defaultMaxDurationBetweenUndimsMillis">600000</integer> <!-- 10 min. --> + <!-- Default number of user undims required to trigger preventing screen sleep. --> + <integer name="config_defaultUndimsRequired">2</integer> + <!-- Whether there is to be a chosen Dock User who is the only user allowed to dream. --> <bool name="config_dreamsOnlyEnabledForDockUser">false</bool> <!-- Whether dreams are disabled when ambient mode is suppressed. --> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index a1961aedf6b7..465e318511c6 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -390,6 +390,16 @@ <!-- The absolute size of the notification expand icon. --> <dimen name="notification_header_expand_icon_size">56dp</dimen> + <!-- Margin to allow space for the expand button when showing the right icon in expanded --> + <!-- notifications. This is equal to notification_2025_expand_button_pill_width --> + <!-- + notification_2025_margin (end padding for expand button) --> + <!-- + notification_2025_expand_button_right_icon_spacing (space between pill and icon) --> + <dimen name="notification_2025_right_icon_expanded_margin_end">52dp</dimen> + + <!-- The large icon has a smaller vertical margin than most other notification content, to --> + <!-- allow it to grow up to 48dp. --> + <dimen name="notification_2025_right_icon_vertical_margin">12dp</dimen> + <!-- the height of the expand button pill --> <dimen name="notification_expand_button_pill_height">24dp</dimen> @@ -411,15 +421,24 @@ <!-- the padding of the expand icon in the notification header --> <dimen name="notification_2025_expand_button_horizontal_icon_padding">6dp</dimen> - <!-- a smaller padding for the end of the expand button, for use when showing the number --> + <!-- smaller padding for the end of the expand icon, for use when showing the number --> <dimen name="notification_2025_expand_button_reduced_end_padding">4dp</dimen> + <!-- the space needed between the expander pill and the large icon when visible --> + <dimen name="notification_2025_expand_button_right_icon_spacing">8dp</dimen> + <!-- the size of the notification close button --> <dimen name="notification_close_button_size">16dp</dimen> <!-- Margin for all notification content --> <dimen name="notification_2025_margin">16dp</dimen> + <!-- A smaller version of the margin to be used when we need more space for the content --> + <dimen name="notification_2025_reduced_margin">12dp</dimen> + + <!-- The difference between the usual margin and the reduced margin --> + <dimen name="notification_2025_additional_margin">4dp</dimen> + <!-- Vertical margin for the headerless notification content, when content has 1 line --> <!-- 16 * 2 (margins) + 24 (1 line) = 56 (notification) --> <dimen name="notification_headerless_margin_oneline">16dp</dimen> @@ -438,7 +457,7 @@ <dimen name="notification_collapsed_height_with_summarization">156dp</dimen> <!-- Max height of a collapsed (headerless) notification with one or two lines --> - <!-- 16 * 2 (margins) + 48 (min content height) = 72 (notification) --> + <!-- 14 * 2 (reduced margins) + 48 (max collapsed content height) = 72 (notification) --> <dimen name="notification_2025_min_height">72dp</dimen> <!-- Height of a headerless notification with one line --> @@ -729,6 +748,9 @@ <!-- The accessibility autoclick panel divider height --> <dimen name="accessibility_autoclick_type_panel_divider_height">24dp</dimen> + <!-- The accessibility autoclick scroll panel button width and height --> + <dimen name="accessibility_autoclick_scroll_panel_button_size">36dp</dimen> + <!-- Margin around the various security views --> <dimen name="keyguard_muliuser_selector_margin">8dp</dimen> @@ -893,6 +915,8 @@ <dimen name="notification_right_icon_size">48dp</dimen> <!-- The margin between the right icon and the content. --> <dimen name="notification_right_icon_content_margin">12dp</dimen> + <!-- The margin between the right icon and the content. (2025 redesign version) --> + <dimen name="notification_2025_right_icon_content_margin">16dp</dimen> <!-- The top and bottom margin of the right icon in the normal notification states --> <dimen name="notification_right_icon_headerless_margin">20dp</dimen> <!-- The top margin of the right icon in the "big" notification states --> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index b3cc8837e0bf..faa660a65d34 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -6226,6 +6226,18 @@ <string name="accessibility_autoclick_pause">Pause</string> <!-- Label for autoclick position button [CHAR LIMIT=NONE] --> <string name="accessibility_autoclick_position">Position</string> + <!-- Label for autoclick scroll up button [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_scroll_up">Scroll Up</string> + <!-- Label for autoclick scroll down button [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_scroll_down">Scroll Down</string> + <!-- Label for autoclick scroll left button [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_scroll_left">Scroll Left</string> + <!-- Label for autoclick scroll right button [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_scroll_right">Scroll Right</string> + <!-- Label for autoclick scroll exit button [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_scroll_exit">Exit Scroll Mode</string> + <!-- Label for autoclick scroll panel title [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_scroll_panel_title">Scroll Panel</string> <!-- Text to tell the user that a package has been forced by themselves in the RESTRICTED bucket. [CHAR LIMIT=NONE] --> <string name="as_app_forced_to_restricted_bucket"> diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index ee1edda838fd..1c3529fa7258 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -1761,6 +1761,19 @@ please see styles_device_defaults.xml. <item name="android:tint">@color/materialColorPrimary</item> </style> + <style name="AccessibilityAutoclickScrollPanelButtonLayoutStyle"> + <item name="android:gravity">center</item> + <item name="android:layout_width">@dimen/accessibility_autoclick_scroll_panel_button_size </item> + <item name="android:layout_height">@dimen/accessibility_autoclick_scroll_panel_button_size</item> + </style> + + <style name="AccessibilityAutoclickScrollPanelImageButtonStyle"> + <item name="android:gravity">center</item> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:scaleType">center</item> + </style> + <!-- TODO(b/309578419): Make activities go edge-to-edge properly and then remove this. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 2b7261b62c64..ee74208e65c6 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3267,6 +3267,8 @@ <java-symbol type="dimen" name="notification_2025_content_margin_start" /> <java-symbol type="dimen" name="notification_2025_expand_button_horizontal_icon_padding" /> <java-symbol type="dimen" name="notification_2025_expand_button_reduced_end_padding" /> + <java-symbol type="dimen" name="notification_2025_expand_button_right_icon_spacing" /> + <java-symbol type="dimen" name="notification_2025_right_icon_expanded_margin_end" /> <java-symbol type="dimen" name="notification_progress_margin_horizontal" /> <java-symbol type="dimen" name="notification_header_background_height" /> <java-symbol type="dimen" name="notification_header_touchable_height" /> @@ -3957,6 +3959,7 @@ <java-symbol type="dimen" name="notification_big_picture_max_width"/> <java-symbol type="dimen" name="notification_right_icon_size"/> <java-symbol type="dimen" name="notification_right_icon_content_margin"/> + <java-symbol type="dimen" name="notification_2025_right_icon_content_margin"/> <java-symbol type="dimen" name="notification_actions_icon_drawable_size"/> <java-symbol type="dimen" name="notification_custom_view_max_image_height"/> <java-symbol type="dimen" name="notification_custom_view_max_image_width"/> @@ -4442,6 +4445,11 @@ <!-- For Attention Service --> <java-symbol type="integer" name="config_attentionMaximumExtension" /> + <java-symbol type="bool" name="config_defaultPreventScreenTimeoutEnabled" /> + <java-symbol type="integer" name="config_defaultPreventScreenTimeoutForMillis" /> + <java-symbol type="integer" name="config_defaultMaxDurationBetweenUndimsMillis" /> + <java-symbol type="integer" name="config_defaultUndimsRequired" /> + <java-symbol type="string" name="config_incidentReportApproverPackage" /> <java-symbol type="array" name="config_restrictedImagesServices" /> @@ -5666,6 +5674,32 @@ <java-symbol type="drawable" name="accessibility_autoclick_resume" /> <java-symbol type="drawable" name="ic_accessibility_autoclick" /> + <!-- Accessibility autoclick scroll panel related --> + <java-symbol type="layout" name="accessibility_autoclick_scroll_panel" /> + <java-symbol type="dimen" name="accessibility_autoclick_scroll_panel_button_size" /> + <java-symbol type="drawable" name="accessibility_autoclick_scroll_up" /> + <java-symbol type="drawable" name="accessibility_autoclick_scroll_down" /> + <java-symbol type="drawable" name="accessibility_autoclick_scroll_left" /> + <java-symbol type="drawable" name="accessibility_autoclick_scroll_right" /> + <java-symbol type="drawable" name="accessibility_autoclick_scroll_exit" /> + <java-symbol type="string" name="accessibility_autoclick_scroll_up" /> + <java-symbol type="string" name="accessibility_autoclick_scroll_down" /> + <java-symbol type="string" name="accessibility_autoclick_scroll_left" /> + <java-symbol type="string" name="accessibility_autoclick_scroll_right" /> + <java-symbol type="string" name="accessibility_autoclick_scroll_exit" /> + <java-symbol type="string" name="accessibility_autoclick_scroll_panel_title" /> + <java-symbol type="id" name="accessibility_autoclick_scroll_panel" /> + <java-symbol type="id" name="scroll_up_layout" /> + <java-symbol type="id" name="scroll_down_layout" /> + <java-symbol type="id" name="scroll_left_layout" /> + <java-symbol type="id" name="scroll_right_layout" /> + <java-symbol type="id" name="scroll_exit_layout" /> + <java-symbol type="id" name="scroll_up" /> + <java-symbol type="id" name="scroll_down" /> + <java-symbol type="id" name="scroll_left" /> + <java-symbol type="id" name="scroll_right" /> + <java-symbol type="id" name="scroll_exit" /> + <!-- For HapticFeedbackConstants configurability defined at HapticFeedbackCustomization --> <java-symbol type="string" name="config_hapticFeedbackCustomizationFile" /> <java-symbol type="xml" name="haptic_feedback_customization" /> diff --git a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java index 33a46d0fde17..6d86bd209a3d 100644 --- a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java +++ b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java @@ -20,6 +20,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import android.os.Handler; +import android.os.Looper; import android.util.PollingCheck; import android.view.View; @@ -486,6 +488,42 @@ public class AnimatorSetCallsTest { }); } + @Test + public void testCancelOnPendingEndListener() throws Throwable { + final CountDownLatch endLatch = new CountDownLatch(1); + final Handler handler = new Handler(Looper.getMainLooper()); + final boolean[] endCalledRightAfterCancel = new boolean[2]; + final AnimatorSet set = new AnimatorSet(); + final ValueAnimatorTests.MyListener asListener = new ValueAnimatorTests.MyListener(); + final ValueAnimatorTests.MyListener vaListener = new ValueAnimatorTests.MyListener(); + final ValueAnimator va = new ValueAnimator(); + va.setFloatValues(0f, 1f); + va.setDuration(30); + va.addUpdateListener(animation -> { + if (animation.getAnimatedFraction() == 1f) { + handler.post(() -> { + set.cancel(); + endCalledRightAfterCancel[0] = vaListener.endCalled; + endCalledRightAfterCancel[1] = asListener.endCalled; + endLatch.countDown(); + }); + } + }); + set.addListener(asListener); + va.addListener(vaListener); + set.play(va); + + ValueAnimator.setPostNotifyEndListenerEnabled(true); + try { + handler.post(set::start); + assertTrue(endLatch.await(1, TimeUnit.SECONDS)); + assertTrue(endCalledRightAfterCancel[0]); + assertTrue(endCalledRightAfterCancel[1]); + } finally { + ValueAnimator.setPostNotifyEndListenerEnabled(false); + } + } + private void waitForOnUiThread(PollingCheck.PollingCheckCondition condition) { final boolean[] value = new boolean[1]; PollingCheck.waitFor(() -> { diff --git a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java index 04698465e971..a55909f0c193 100644 --- a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java +++ b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java @@ -922,6 +922,36 @@ public class ValueAnimatorTests { } @Test + public void testCancelOnPendingEndListener() throws Throwable { + final CountDownLatch endLatch = new CountDownLatch(1); + final Handler handler = new Handler(Looper.getMainLooper()); + final boolean[] endCalledRightAfterCancel = new boolean[1]; + final MyListener listener = new MyListener(); + final ValueAnimator va = new ValueAnimator(); + va.setFloatValues(0f, 1f); + va.setDuration(30); + va.addUpdateListener(animation -> { + if (animation.getAnimatedFraction() == 1f) { + handler.post(() -> { + va.cancel(); + endCalledRightAfterCancel[0] = listener.endCalled; + endLatch.countDown(); + }); + } + }); + va.addListener(listener); + + ValueAnimator.setPostNotifyEndListenerEnabled(true); + try { + handler.post(va::start); + assertThat(endLatch.await(1, TimeUnit.SECONDS)).isTrue(); + assertThat(endCalledRightAfterCancel[0]).isTrue(); + } finally { + ValueAnimator.setPostNotifyEndListenerEnabled(false); + } + } + + @Test public void testZeroDuration() throws Throwable { // Run two animators with zero duration, with one running forward and the other one // backward. Check that the animations start and finish with the correct end fractions. @@ -1182,6 +1212,7 @@ public class ValueAnimatorTests { assertEquals(A1_START_VALUE, a1.getAnimatedValue()); }); } + class MyUpdateListener implements ValueAnimator.AnimatorUpdateListener { boolean wasRunning = false; long firstRunningFrameTime = -1; @@ -1207,7 +1238,7 @@ public class ValueAnimatorTests { } } - class MyListener implements Animator.AnimatorListener { + static class MyListener implements Animator.AnimatorListener { boolean startCalled = false; boolean cancelCalled = false; boolean endCalled = false; diff --git a/core/tests/coretests/src/android/util/ArrayMapTest.java b/core/tests/coretests/src/android/util/ArrayMapTest.java index 711ff9458e11..c7efe6f93f0d 100644 --- a/core/tests/coretests/src/android/util/ArrayMapTest.java +++ b/core/tests/coretests/src/android/util/ArrayMapTest.java @@ -14,14 +14,18 @@ package android.util; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.fail; import android.platform.test.annotations.IgnoreUnderRavenwood; +import android.platform.test.annotations.Presubmit; import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -32,6 +36,7 @@ import java.util.ConcurrentModificationException; * Unit tests for ArrayMap that don't belong in CTS. */ @LargeTest +@Presubmit @RunWith(AndroidJUnit4.class) public class ArrayMapTest { private static final String TAG = "ArrayMapTest"; @@ -51,6 +56,7 @@ public class ArrayMapTest { * @throws Exception */ @Test + @Ignore("Failing; b/399137661") @IgnoreUnderRavenwood(reason = "Long test runtime") public void testConcurrentModificationException() throws Exception { final int TEST_LEN_MS = 5000; @@ -118,4 +124,49 @@ public class ArrayMapTest { } } } + + @Test + public void testToString() { + map.put("1", "One"); + map.put("2", "Two"); + map.put("3", "Five"); + map.put("3", "Three"); + + assertThat(map.toString()).isEqualTo("{1=One, 2=Two, 3=Three}"); + assertThat(map.keySet().toString()).isEqualTo("[1, 2, 3]"); + assertThat(map.values().toString()).isEqualTo("[One, Two, Three]"); + } + + @Test + public void testToStringRecursive() { + ArrayMap<Object, Object> weird = new ArrayMap<>(); + weird.put("1", weird); + + assertThat(weird.toString()).isEqualTo("{1=(this Map)}"); + assertThat(weird.keySet().toString()).isEqualTo("[1]"); + assertThat(weird.values().toString()).isEqualTo("[{1=(this Map)}]"); + } + + @Test + public void testToStringRecursiveKeySet() { + ArrayMap<Object, Object> weird = new ArrayMap<>(); + weird.put("1", weird.keySet()); + weird.put(weird.keySet(), "2"); + + assertThat(weird.toString()).isEqualTo("{1=[1, (this KeySet)], [1, (this KeySet)]=2}"); + assertThat(weird.keySet().toString()).isEqualTo("[1, (this KeySet)]"); + assertThat(weird.values().toString()).isEqualTo("[[1, (this KeySet)], 2]"); + } + + @Test + public void testToStringRecursiveValues() { + ArrayMap<Object, Object> weird = new ArrayMap<>(); + weird.put("1", weird.values()); + weird.put(weird.values(), "2"); + + assertThat(weird.toString()).isEqualTo( + "{1=[(this ValuesCollection), 2], [(this ValuesCollection), 2]=2}"); + assertThat(weird.keySet().toString()).isEqualTo("[1, [(this ValuesCollection), 2]]"); + assertThat(weird.values().toString()).isEqualTo("[(this ValuesCollection), 2]"); + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index e141f70d8abb..da25da1256b0 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -54,6 +54,8 @@ import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit; import static androidx.window.extensions.embedding.TaskFragmentContainer.OverlayContainerRestoreParams; +import static com.android.window.flags.Flags.activityEmbeddingDelayTaskFragmentFinishForActivityLaunch; + import android.annotation.CallbackExecutor; import android.app.Activity; import android.app.ActivityClient; @@ -815,11 +817,17 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); } else if (!container.isWaitingActivityAppear()) { - // Do not finish the container before the expected activity appear until - // timeout. - mTransactionManager.getCurrentTransactionRecord() - .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); - mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */); + if (activityEmbeddingDelayTaskFragmentFinishForActivityLaunch() + && container.hasActivityLaunchHint()) { + // If we have recently attempted to launch a new activity into this + // TaskFragment, we schedule delayed cleanup. If the new activity appears in + // this TaskFragment, we no longer need to finish the TaskFragment. + container.scheduleDelayedTaskFragmentCleanup(); + } else { + mTransactionManager.getCurrentTransactionRecord() + .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); + mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */); + } } } else if (wasInPip && isInPip) { // No update until exit PIP. @@ -3164,6 +3172,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // TODO(b/229680885): skip override launching TaskFragment token by split-rule options.putBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN, launchedInTaskFragment.getTaskFragmentToken()); + if (activityEmbeddingDelayTaskFragmentFinishForActivityLaunch()) { + launchedInTaskFragment.setActivityLaunchHint(); + } mCurrentIntent = intent; } else { transactionRecord.abort(); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index b3e003e7ad95..6fa855e8b6d6 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -18,6 +18,8 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static com.android.window.flags.Flags.activityEmbeddingDelayTaskFragmentFinishForActivityLaunch; + import android.app.Activity; import android.app.ActivityThread; import android.app.WindowConfiguration.WindowingMode; @@ -53,6 +55,8 @@ import java.util.Objects; class TaskFragmentContainer { private static final int APPEAR_EMPTY_TIMEOUT_MS = 3000; + private static final int DELAYED_TASK_FRAGMENT_CLEANUP_TIMEOUT_MS = 500; + /** Parcelable data of this TaskFragmentContainer. */ @NonNull private final ParcelableTaskFragmentContainerData mParcelableData; @@ -165,6 +169,18 @@ class TaskFragmentContainer { */ private boolean mLastDimOnTask; + /** The timestamp of the latest pending activity launch attempt. 0 means no pending launch. */ + private long mLastActivityLaunchTimestampMs = 0; + + /** + * The scheduled runnable for delayed TaskFragment cleanup. This is used when the TaskFragment + * becomes empty, but we expect a new activity to appear in it soon. + * + * It should be {@code null} when not scheduled. + */ + @Nullable + private Runnable mDelayedTaskFragmentCleanupRunnable; + /** * Creates a container with an existing activity that will be re-parented to it in a window * container transaction. @@ -540,6 +556,10 @@ class TaskFragmentContainer { mAppearEmptyTimeout = null; } + if (activityEmbeddingDelayTaskFragmentFinishForActivityLaunch()) { + clearActivityLaunchHintIfNecessary(mInfo, info); + } + mHasCrossProcessActivities = false; mInfo = info; if (mInfo == null || mInfo.isEmpty()) { @@ -1064,6 +1084,89 @@ class TaskFragmentContainer { return isOverlay() && mParcelableData.mAssociatedActivityToken != null; } + /** + * Indicates whether there is possibly a pending activity launching into this TaskFragment. + * + * This should only be used as a hint because we cannot reliably determine if the new activity + * is going to appear into this TaskFragment. + * + * TODO(b/293800510) improve activity launch tracking in TaskFragment. + */ + boolean hasActivityLaunchHint() { + if (mLastActivityLaunchTimestampMs == 0) { + return false; + } + if (System.currentTimeMillis() > mLastActivityLaunchTimestampMs + APPEAR_EMPTY_TIMEOUT_MS) { + // The hint has expired after APPEAR_EMPTY_TIMEOUT_MS. + mLastActivityLaunchTimestampMs = 0; + return false; + } + return true; + } + + /** Records the latest activity launch attempt. */ + void setActivityLaunchHint() { + mLastActivityLaunchTimestampMs = System.currentTimeMillis(); + } + + /** + * If we get a new info showing that the TaskFragment has more activities than the previous + * info, we clear the new activity launch hint. + * + * Note that this is not a reliable way and cannot cover situations when the attempted + * activity launch did not cause TaskFragment info activity count changes, such as trampoline + * launches or single top launches. + * + * TODO(b/293800510) improve activity launch tracking in TaskFragment. + */ + private void clearActivityLaunchHintIfNecessary( + @Nullable TaskFragmentInfo oldInfo, @NonNull TaskFragmentInfo newInfo) { + final int previousActivityCount = oldInfo == null ? 0 : oldInfo.getRunningActivityCount(); + if (newInfo.getRunningActivityCount() > previousActivityCount) { + mLastActivityLaunchTimestampMs = 0; + cancelDelayedTaskFragmentCleanup(); + } + } + + /** + * Schedules delayed TaskFragment cleanup due to pending activity launch. The scheduled cleanup + * will be canceled if a new activity appears in this TaskFragment. + */ + void scheduleDelayedTaskFragmentCleanup() { + if (mDelayedTaskFragmentCleanupRunnable != null) { + // Remove the previous callback if there is already one scheduled. + mController.getHandler().removeCallbacks(mDelayedTaskFragmentCleanupRunnable); + } + mDelayedTaskFragmentCleanupRunnable = new Runnable() { + @Override + public void run() { + synchronized (mController.mLock) { + if (mDelayedTaskFragmentCleanupRunnable != this) { + // The scheduled cleanup runnable has been canceled or rescheduled, so + // skipping. + return; + } + if (isEmpty()) { + mLastActivityLaunchTimestampMs = 0; + mController.onTaskFragmentAppearEmptyTimeout( + TaskFragmentContainer.this); + } + mDelayedTaskFragmentCleanupRunnable = null; + } + } + }; + mController.getHandler().postDelayed( + mDelayedTaskFragmentCleanupRunnable, DELAYED_TASK_FRAGMENT_CLEANUP_TIMEOUT_MS); + } + + private void cancelDelayedTaskFragmentCleanup() { + if (mDelayedTaskFragmentCleanupRunnable == null) { + return; + } + mController.getHandler().removeCallbacks(mDelayedTaskFragmentCleanupRunnable); + mDelayedTaskFragmentCleanupRunnable = null; + } + @Override public String toString() { return toString(true /* includeContainersToFinishOnExit */); diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 1e72d64397d7..ab2804626361 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -194,3 +194,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_gsf" + namespace: "multitasking" + description: "Applies GSF font styles to multitasking." + bug: "400534660" + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt index 24f43d347163..5777cb0cc8c5 100644 --- a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt @@ -94,6 +94,7 @@ class DragZoneFactoryScreenshotTest(private val param: Param) { private val splitScreenModeName = when (splitScreenMode) { + SplitScreenMode.UNSUPPORTED -> "_split_unsupported" SplitScreenMode.NONE -> "" SplitScreenMode.SPLIT_50_50 -> "_split_50_50" SplitScreenMode.SPLIT_10_90 -> "_split_10_90" diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt index 362a5c5c3750..afeaf70c9d62 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt @@ -303,6 +303,7 @@ class DragZoneFactory( val isVerticalSplit = deviceConfig.isSmallTablet == deviceConfig.isLandscape return if (isVerticalSplit) { when (splitScreenModeChecker.getSplitScreenMode()) { + SplitScreenMode.UNSUPPORTED -> emptyList() SplitScreenMode.SPLIT_50_50, SplitScreenMode.NONE -> listOf( @@ -360,6 +361,7 @@ class DragZoneFactory( } } else { when (splitScreenModeChecker.getSplitScreenMode()) { + SplitScreenMode.UNSUPPORTED -> emptyList() SplitScreenMode.SPLIT_50_50, SplitScreenMode.NONE -> listOf( @@ -453,6 +455,7 @@ class DragZoneFactory( // vertical split drag zones are aligned with the full screen drag zone width val splitZoneLeft = windowBounds.right / 2 - fullScreenDragZoneWidth / 2 when (splitScreenModeChecker.getSplitScreenMode()) { + SplitScreenMode.UNSUPPORTED -> emptyList() SplitScreenMode.SPLIT_50_50, SplitScreenMode.NONE -> listOf( @@ -560,7 +563,8 @@ class DragZoneFactory( NONE, SPLIT_50_50, SPLIT_10_90, - SPLIT_90_10 + SPLIT_90_10, + UNSUPPORTED } fun getSplitScreenMode(): SplitScreenMode diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt index 2bb6cf4ec3aa..73277310ffe4 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt @@ -20,6 +20,7 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Paint import android.graphics.RectF +import android.util.TypedValue import android.view.View import com.android.wm.shell.shared.R @@ -37,14 +38,21 @@ class DropTargetView(context: Context) : View(context) { private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = context.getColor(com.android.internal.R.color.materialColorPrimaryContainer) style = Paint.Style.STROKE - strokeWidth = context.resources.getDimensionPixelSize(R.dimen.drop_target_stroke).toFloat() + strokeWidth = 1.dpToPx() } - private val cornerRadius = context.resources.getDimensionPixelSize( - R.dimen.drop_target_radius).toFloat() + private val cornerRadius = 28.dpToPx() private val rect = RectF(0f, 0f, 0f, 0f) + // TODO b/396539130: Use shared xml resources once we can access them in launcher + private fun Int.dpToPx() = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + this.toFloat(), + context.resources.displayMetrics + ) + override fun onDraw(canvas: Canvas) { canvas.save() canvas.drawRoundRect(rect, cornerRadius, cornerRadius, rectPaint) diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index ed5e0c608675..2e33253b5e09 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -17,9 +17,7 @@ package com.android.wm.shell.shared.desktopmode; import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED; -import static android.window.DesktopExperienceFlags.ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE; -import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement; import static com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper.enableBubbleToFullscreen; import android.annotation.NonNull; @@ -226,7 +224,7 @@ public class DesktopModeStatus { /** * Return {@code true} if the current device can host desktop sessions on its internal display. */ - private static boolean canInternalDisplayHostDesktops(@NonNull Context context) { + public static boolean canInternalDisplayHostDesktops(@NonNull Context context) { return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); } @@ -271,29 +269,6 @@ public class DesktopModeStatus { } /** - * Check to see if a display should have desktop mode enabled or not. Internal - * and external displays have separate logic. - */ - public static boolean isDesktopModeSupportedOnDisplay(Context context, Display display) { - if (!canEnterDesktopMode(context)) { - return false; - } - if (display.getType() == Display.TYPE_INTERNAL) { - return canInternalDisplayHostDesktops(context); - } - - // TODO (b/395014779): Change this to use WM API - if ((display.getType() == Display.TYPE_EXTERNAL - || display.getType() == Display.TYPE_OVERLAY) - && enableDisplayContentModeManagement()) { - final WindowManager wm = context.getSystemService(WindowManager.class); - return wm != null && wm.shouldShowSystemDecors(display.getDisplayId()); - } - - return false; - } - - /** * Returns whether the multiple desktops feature is enabled for this device (both backend and * frontend implementations). */ @@ -366,11 +341,8 @@ public class DesktopModeStatus { if (!enforceDeviceRestrictions()) { return true; } - // If projected display is enabled, #canInternalDisplayHostDesktops is no longer a - // requirement. - final boolean desktopModeSupported = ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE.isTrue() - ? isDesktopModeSupported(context) : (isDesktopModeSupported(context) - && canInternalDisplayHostDesktops(context)); + final boolean desktopModeSupported = isDesktopModeSupported(context) + && canInternalDisplayHostDesktops(context); final boolean desktopModeSupportedByDevOptions = Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionSupported(context); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 80b65d025399..53dede6bd227 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -28,7 +28,7 @@ import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME; -import static com.android.systemui.Flags.predictiveBackDelayTransition; +import static com.android.systemui.Flags.predictiveBackDelayWmTransition; import static com.android.window.flags.Flags.unifyBackNavigationTransition; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; @@ -433,7 +433,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont public void onThresholdCrossed() { mThresholdCrossed = true; BackTouchTracker activeTracker = getActiveTracker(); - if (predictiveBackDelayTransition() && activeTracker != null && mActiveCallback == null + if (predictiveBackDelayWmTransition() && activeTracker != null && mActiveCallback == null && mBackGestureStarted) { startBackNavigation(activeTracker); } @@ -494,12 +494,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (swipeEdge == EDGE_NONE) { // start animation immediately for non-gestural sources (without ACTION_MOVE // events) - if (!predictiveBackDelayTransition()) { + if (!predictiveBackDelayWmTransition()) { mThresholdCrossed = true; } mPointersPilfered = true; onGestureStarted(touchX, touchY, swipeEdge); - if (predictiveBackDelayTransition()) { + if (predictiveBackDelayWmTransition()) { onThresholdCrossed(); } mShouldStartOnNextMoveEvent = false; @@ -555,7 +555,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mPostCommitAnimationInProgress = false; mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable); startSystemAnimation(); - } else if (!predictiveBackDelayTransition()) { + } else if (!predictiveBackDelayWmTransition()) { startBackNavigation(touchTracker); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java index e7e7be9cdf6b..d5f2dbdbf5f5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java @@ -159,19 +159,22 @@ public class BubbleTransitions { private final WindowContainerTransaction mPendingWct; private final boolean mReleasedOnLeft; private final float mTaskScale; + private final float mCornerRadius; private final PointF mDragPosition; /** * @param releasedOnLeft true if the bubble was released in the left drop target * @param taskScale the scale of the task when it was dragged to bubble + * @param cornerRadius the corner radius of the task when it was dragged to bubble * @param dragPosition the position of the task when it was dragged to bubble * @param wct pending operations to be applied when finishing the drag */ - public DragData(boolean releasedOnLeft, float taskScale, @Nullable PointF dragPosition, - @Nullable WindowContainerTransaction wct) { + public DragData(boolean releasedOnLeft, float taskScale, float cornerRadius, + @Nullable PointF dragPosition, @Nullable WindowContainerTransaction wct) { mPendingWct = wct; mReleasedOnLeft = releasedOnLeft; mTaskScale = taskScale; + mCornerRadius = cornerRadius; mDragPosition = dragPosition != null ? dragPosition : new PointF(0, 0); } @@ -198,6 +201,13 @@ public class BubbleTransitions { } /** + * @return the corner radius of the task when it was dragged to bubble + */ + public float getCornerRadius() { + return mCornerRadius; + } + + /** * @return position of the task when it was dragged to bubble */ public PointF getDragPosition() { @@ -362,6 +372,7 @@ public class BubbleTransitions { (int) mDragData.getDragPosition().y); startTransaction.setScale(mSnapshot, mDragData.getTaskScale(), mDragData.getTaskScale()); + startTransaction.setCornerRadius(mSnapshot, mDragData.getCornerRadius()); } // Now update state (and talk to launcher) in parallel with snapshot stuff @@ -377,12 +388,6 @@ public class BubbleTransitions { startTransaction.setPosition(mSnapshot, left, top); startTransaction.setLayer(mSnapshot, Integer.MAX_VALUE); - BubbleBarExpandedView bbev = mBubble.getBubbleBarExpandedView(); - if (bbev != null) { - // Corners get reset during the animation. Add them back - startTransaction.setCornerRadius(mSnapshot, bbev.getRestingCornerRadius()); - } - startTransaction.apply(); mTaskViewTransitions.onExternalDone(transition); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HomeIntentProvider.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HomeIntentProvider.kt new file mode 100644 index 000000000000..8751b65dce94 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HomeIntentProvider.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2025 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.common + +import android.app.ActivityManager +import android.app.ActivityOptions +import android.app.PendingIntent +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.content.Context +import android.content.Intent +import android.os.UserHandle +import android.view.Display.DEFAULT_DISPLAY +import android.window.WindowContainerTransaction +import com.android.window.flags.Flags + +/** Creates home intent **/ +class HomeIntentProvider( + private val context: Context, +) { + fun addLaunchHomePendingIntent( + wct: WindowContainerTransaction, displayId: Int, userId: Int? = null + ) { + val userHandle = + if (userId != null) UserHandle.of(userId) else UserHandle.of(ActivityManager.getCurrentUser()) + + val launchHomeIntent = Intent(Intent.ACTION_MAIN).apply { + if (displayId != DEFAULT_DISPLAY) { + addCategory(Intent.CATEGORY_SECONDARY_HOME) + } else { + addCategory(Intent.CATEGORY_HOME) + } + } + val options = ActivityOptions.makeBasic().apply { + launchWindowingMode = WINDOWING_MODE_FULLSCREEN + pendingIntentBackgroundActivityStartMode = + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS + if (Flags.enablePerDisplayDesktopWallpaperActivity()) { + launchDisplayId = displayId + } + } + val pendingIntent = PendingIntent.getActivityAsUser( + context, + /* requestCode= */ 0, + launchHomeIntent, + PendingIntent.FLAG_IMMUTABLE, + /* options= */ null, + userHandle, + ) + wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle()) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java index 214e6ad455a1..aeef211ae3f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java @@ -143,6 +143,9 @@ public class PipBoundsState { */ public final Rect mCachedLauncherShelfHeightKeepClearArea = new Rect(); + private final List<OnPipComponentChangedListener> mOnPipComponentChangedListeners = + new ArrayList<>(); + // the size of the current bounds relative to the max size spec private float mBoundsScale; @@ -156,9 +159,7 @@ public class PipBoundsState { // Update the relative proportion of the bounds compared to max possible size. Max size // spec takes the aspect ratio of the bounds into account, so both width and height // scale by the same factor. - addPipExclusionBoundsChangeCallback((bounds) -> { - updateBoundsScale(); - }); + addPipExclusionBoundsChangeCallback((bounds) -> updateBoundsScale()); } /** Reloads the resources. */ @@ -341,11 +342,14 @@ public class PipBoundsState { /** Set the last {@link ComponentName} to enter PIP mode. */ public void setLastPipComponentName(@Nullable ComponentName lastPipComponentName) { final boolean changed = !Objects.equals(mLastPipComponentName, lastPipComponentName); + if (!changed) return; + clearReentryState(); + setHasUserResizedPip(false); + setHasUserMovedPip(false); + final ComponentName oldComponentName = mLastPipComponentName; mLastPipComponentName = lastPipComponentName; - if (changed) { - clearReentryState(); - setHasUserResizedPip(false); - setHasUserMovedPip(false); + for (OnPipComponentChangedListener listener : mOnPipComponentChangedListeners) { + listener.onPipComponentChanged(oldComponentName, mLastPipComponentName); } } @@ -616,6 +620,21 @@ public class PipBoundsState { } } + /** Adds callback to listen on component change. */ + public void addOnPipComponentChangedListener(@NonNull OnPipComponentChangedListener listener) { + if (!mOnPipComponentChangedListeners.contains(listener)) { + mOnPipComponentChangedListeners.add(listener); + } + } + + /** Removes callback to listen on component change. */ + public void removeOnPipComponentChangedListener( + @NonNull OnPipComponentChangedListener listener) { + if (mOnPipComponentChangedListeners.contains(listener)) { + mOnPipComponentChangedListeners.remove(listener); + } + } + public LauncherState getLauncherState() { return mLauncherState; } @@ -695,7 +714,7 @@ public class PipBoundsState { * Represents the state of pip to potentially restore upon reentry. */ @VisibleForTesting - public static final class PipReentryState { + static final class PipReentryState { private static final String TAG = PipReentryState.class.getSimpleName(); private final float mSnapFraction; @@ -722,6 +741,22 @@ public class PipBoundsState { } } + /** + * Listener interface for PiP component change, i.e. the app in pip mode changes + * TODO: Move this out of PipBoundsState once pip1 is deprecated. + */ + public interface OnPipComponentChangedListener { + /** + * Callback when the component in pip mode changes. + * @param oldPipComponent previous component in pip mode, + * {@code null} if this is the very first time PiP appears. + * @param newPipComponent new component that enters pip mode. + */ + void onPipComponentChanged( + @Nullable ComponentName oldPipComponent, + @NonNull ComponentName newPipComponent); + } + /** Dumps internal state. */ public void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index e20a3d839def..fec1f56c76bb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -1466,11 +1466,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // Freeze the configuration size with offset to prevent app get a configuration // changed or relaunch. This is required to make sure client apps will calculate // insets properly after layout shifted. - if (mTargetYOffset == 0) { - mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this); - } else { - mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset, SplitLayout.this); - } + mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset, SplitLayout.this); } // Make {@link DividerView} non-interactive while IME showing in split mode. Listen to diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/OWNERS new file mode 100644 index 000000000000..007528e2e054 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-module crash handling owners +uysalorhan@google.com
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/ShellCrashHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/ShellCrashHandler.kt new file mode 100644 index 000000000000..2e34d043cbe1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/ShellCrashHandler.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 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.crashhandling + +import android.app.WindowConfiguration +import android.content.Context +import android.view.Display.DEFAULT_DISPLAY +import android.window.DesktopExperienceFlags +import android.window.WindowContainerTransaction +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.HomeIntentProvider +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellInit + +/** [ShellCrashHandler] for shell to use when it's being initialized. Currently it only restores + * the home task to top. + **/ +class ShellCrashHandler( + private val context: Context, + private val shellTaskOrganizer: ShellTaskOrganizer, + private val homeIntentProvider: HomeIntentProvider, + shellInit: ShellInit, +) { + init { + shellInit.addInitCallback(::onInit, this) + } + + private fun onInit() { + handleCrashIfNeeded() + } + + private fun handleCrashIfNeeded() { + // For now only handle crashes when desktop mode is enabled on the device. + if (DesktopModeStatus.canEnterDesktopMode(context) && + !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + var freeformTaskExists = false + // If there are running tasks at init, WMShell has crashed but WMCore is still alive. + for (task in shellTaskOrganizer.getRunningTasks()) { + if (task.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) { + freeformTaskExists = true + } + + if (freeformTaskExists) { + shellTaskOrganizer.applyTransaction( + addLaunchHomePendingIntent(WindowContainerTransaction(), DEFAULT_DISPLAY) + ) + break + } + } + } + } + + private fun addLaunchHomePendingIntent( + wct: WindowContainerTransaction, displayId: Int + ): WindowContainerTransaction { + // TODO: b/400462917 - Check that crashes are also handled correctly on HSUM devices. We + // might need to pass the [userId] here to launch the correct home. + homeIntentProvider.addLaunchHomePendingIntent(wct, displayId) + return wct + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 423d1750de75..d230425680ce 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -69,6 +69,7 @@ import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.HomeIntentProvider; import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController; import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorSurface; @@ -80,6 +81,7 @@ import com.android.wm.shell.common.UserProfileContexts; import com.android.wm.shell.common.split.SplitState; import com.android.wm.shell.compatui.letterbox.LetterboxCommandHandler; import com.android.wm.shell.compatui.letterbox.LetterboxTransitionObserver; +import com.android.wm.shell.crashhandling.ShellCrashHandler; import com.android.wm.shell.dagger.back.ShellBackAnimationModule; import com.android.wm.shell.dagger.pip.PipModule; import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler; @@ -169,7 +171,6 @@ import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel; import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel; import com.android.wm.shell.windowdecor.WindowDecorViewModel; import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer; -import com.android.wm.shell.windowdecor.common.AppHandleAndHeaderVisibilityHelper; import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader; import com.android.wm.shell.windowdecor.common.viewhost.DefaultWindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.common.viewhost.PooledWindowDecorViewHostSupplier; @@ -451,7 +452,8 @@ public abstract class WMShellModule { Optional<DesktopImmersiveController> desktopImmersiveController, WindowDecorViewModel windowDecorViewModel, Optional<TaskChangeListener> taskChangeListener, - FocusTransitionObserver focusTransitionObserver) { + FocusTransitionObserver focusTransitionObserver, + Optional<DesksTransitionObserver> desksTransitionObserver) { return new FreeformTaskTransitionObserver( context, shellInit, @@ -459,7 +461,8 @@ public abstract class WMShellModule { desktopImmersiveController, windowDecorViewModel, taskChangeListener, - focusTransitionObserver); + focusTransitionObserver, + desksTransitionObserver); } @WMSingleton @@ -725,9 +728,11 @@ public abstract class WMShellModule { static DesksOrganizer provideDesksOrganizer( @NonNull ShellInit shellInit, @NonNull ShellCommandHandler shellCommandHandler, - @NonNull ShellTaskOrganizer shellTaskOrganizer + @NonNull ShellTaskOrganizer shellTaskOrganizer, + @NonNull LaunchAdjacentController launchAdjacentController ) { - return new RootTaskDesksOrganizer(shellInit, shellCommandHandler, shellTaskOrganizer); + return new RootTaskDesksOrganizer(shellInit, shellCommandHandler, shellTaskOrganizer, + launchAdjacentController); } @WMSingleton @@ -772,11 +777,12 @@ public abstract class WMShellModule { Optional<BubbleController> bubbleController, OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver, DesksOrganizer desksOrganizer, - DesksTransitionObserver desksTransitionObserver, + Optional<DesksTransitionObserver> desksTransitionObserver, UserProfileContexts userProfileContexts, DesktopModeCompatPolicy desktopModeCompatPolicy, DragToDisplayTransitionHandler dragToDisplayTransitionHandler, - DesktopModeMoveToDisplayTransitionHandler moveToDisplayTransitionHandler) { + DesktopModeMoveToDisplayTransitionHandler moveToDisplayTransitionHandler, + HomeIntentProvider homeIntentProvider) { return new DesktopTasksController( context, shellInit, @@ -813,11 +819,12 @@ public abstract class WMShellModule { bubbleController, overviewToDesktopTransitionObserver, desksOrganizer, - desksTransitionObserver, + desksTransitionObserver.get(), userProfileContexts, desktopModeCompatPolicy, dragToDisplayTransitionHandler, - moveToDisplayTransitionHandler); + moveToDisplayTransitionHandler, + homeIntentProvider); } @WMSingleton @@ -874,6 +881,7 @@ public abstract class WMShellModule { Transitions transitions, @DynamicOverride DesktopUserRepositories desktopUserRepositories, ShellTaskOrganizer shellTaskOrganizer, + DesksOrganizer desksOrganizer, InteractionJankMonitor interactionJankMonitor, @ShellMainThread Handler handler) { int maxTaskLimit = DesktopModeStatus.getMaxTaskLimit(context); @@ -886,6 +894,7 @@ public abstract class WMShellModule { transitions, desktopUserRepositories, shellTaskOrganizer, + desksOrganizer, maxTaskLimit <= 0 ? null : maxTaskLimit, interactionJankMonitor, context, @@ -1012,7 +1021,6 @@ public abstract class WMShellModule { Optional<DesktopTasksLimiter> desktopTasksLimiter, AppHandleEducationController appHandleEducationController, AppToWebEducationController appToWebEducationController, - AppHandleAndHeaderVisibilityHelper appHandleAndHeaderVisibilityHelper, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, FocusTransitionObserver focusTransitionObserver, @@ -1036,10 +1044,10 @@ public abstract class WMShellModule { rootTaskDisplayAreaOrganizer, interactionJankMonitor, genericLinksParser, assistContentRequester, windowDecorViewHostSupplier, multiInstanceHelper, desktopTasksLimiter, appHandleEducationController, appToWebEducationController, - appHandleAndHeaderVisibilityHelper, windowDecorCaptionHandleRepository, - activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, - desktopModeUiEventLogger, taskResourceLoader, recentsTransitionHandler, - desktopModeCompatPolicy, desktopTilingDecorViewModel, + windowDecorCaptionHandleRepository, activityOrientationChangeHandler, + focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, + taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy, + desktopTilingDecorViewModel, multiDisplayDragMoveIndicatorController)); } @@ -1067,16 +1075,6 @@ public abstract class WMShellModule { @WMSingleton @Provides - static AppHandleAndHeaderVisibilityHelper provideAppHandleAndHeaderVisibilityHelper( - @NonNull Context context, - @NonNull DisplayController displayController, - @NonNull DesktopModeCompatPolicy desktopModeCompatPolicy) { - return new AppHandleAndHeaderVisibilityHelper(context, displayController, - desktopModeCompatPolicy); - } - - @WMSingleton - @Provides static WindowDecorTaskResourceLoader provideWindowDecorTaskResourceLoader( @NonNull Context context, @NonNull ShellInit shellInit, @NonNull ShellController shellController, @@ -1215,7 +1213,6 @@ public abstract class WMShellModule { Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler, Optional<BackAnimationController> backAnimationController, DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, - @NonNull DesksTransitionObserver desksTransitionObserver, ShellInit shellInit) { return desktopUserRepositories.flatMap( repository -> @@ -1228,17 +1225,21 @@ public abstract class WMShellModule { desktopMixedTransitionHandler.get(), backAnimationController.get(), desktopWallpaperActivityTokenProvider, - desksTransitionObserver, shellInit))); } @WMSingleton @Provides - static DesksTransitionObserver provideDesksTransitionObserver( - @NonNull @DynamicOverride DesktopUserRepositories desktopUserRepositories, + static Optional<DesksTransitionObserver> provideDesksTransitionObserver( + Context context, + @DynamicOverride DesktopUserRepositories desktopUserRepositories, @NonNull DesksOrganizer desksOrganizer ) { - return new DesksTransitionObserver(desktopUserRepositories, desksOrganizer); + if (DesktopModeStatus.canEnterDesktopMode(context)) { + return Optional.of( + new DesksTransitionObserver(desktopUserRepositories, desksOrganizer)); + } + return Optional.empty(); } @WMSingleton @@ -1547,7 +1548,8 @@ public abstract class WMShellModule { Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional, Optional<DesktopDisplayEventHandler> desktopDisplayEventHandler, Optional<DesktopModeKeyGestureHandler> desktopModeKeyGestureHandler, - Optional<SystemModalsTransitionHandler> systemModalsTransitionHandler) { + Optional<SystemModalsTransitionHandler> systemModalsTransitionHandler, + ShellCrashHandler shellCrashHandler) { return new Object(); } @@ -1567,4 +1569,20 @@ public abstract class WMShellModule { return new UserProfileContexts(context, shellController, shellInit); } + @WMSingleton + @Provides + static ShellCrashHandler provideShellCrashHandler( + Context context, + ShellTaskOrganizer shellTaskOrganizer, + HomeIntentProvider homeIntentProvider, + ShellInit shellInit) { + return new ShellCrashHandler(context, shellTaskOrganizer, homeIntentProvider, shellInit); + } + + @WMSingleton + @Provides + static HomeIntentProvider provideHomeIntentProvider(Context context) { + return new HomeIntentProvider(context); + } + } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index bbb300ea42da..f64bd757de3b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -235,6 +235,10 @@ class DesktopRepository( /** Returns the default desk in the given display. */ private fun getDefaultDesk(displayId: Int): Desk? = desktopData.getDefaultDesk(displayId) + /** Returns whether the given desk is active in its display. */ + fun isDeskActive(deskId: Int): Boolean = + desktopData.getAllActiveDesks().any { desk -> desk.deskId == deskId } + /** Sets the given desk as the active one in the given display. */ fun setActiveDesk(displayId: Int, deskId: Int) { logD("setActiveDesk for displayId=%d and deskId=%d", displayId, deskId) @@ -485,7 +489,7 @@ class DesktopRepository( fun getExpandedTasksOrdered(displayId: Int): List<Int> = getFreeformTasksInZOrder(displayId).filter { !isMinimizedTask(it) } - @VisibleForTesting + /** Returns all active non-minimized tasks for [deskId] ordered from top to bottom. */ fun getExpandedTasksIdsInDeskOrdered(deskId: Int): List<Int> = getFreeformTasksIdsInDeskInZOrder(deskId).filter { !isMinimizedTask(it) } 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 301b79afd537..2835a18cd809 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 @@ -48,6 +48,7 @@ import android.view.DragEvent import android.view.MotionEvent import android.view.SurfaceControl import android.view.SurfaceControl.Transaction +import android.view.WindowManager import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_NONE @@ -84,6 +85,7 @@ import com.android.wm.shell.bubbles.BubbleController import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.ExternalInterfaceBinder +import com.android.wm.shell.common.HomeIntentProvider import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.MultiInstanceHelper.Companion.getComponent import com.android.wm.shell.common.RemoteCallable @@ -210,6 +212,7 @@ class DesktopTasksController( private val desktopModeCompatPolicy: DesktopModeCompatPolicy, private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler, private val moveToDisplayTransitionHandler: DesktopModeMoveToDisplayTransitionHandler, + private val homeIntentProvider: HomeIntentProvider, ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, @@ -762,7 +765,9 @@ class DesktopTasksController( fun minimizeTask(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) { val wct = WindowContainerTransaction() - val isMinimizingToPip = taskInfo.pictureInPictureParams?.isAutoEnterEnabled() ?: false + val isMinimizingToPip = + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue && + (taskInfo.pictureInPictureParams?.isAutoEnterEnabled() ?: false) // If task is going to PiP, start a PiP transition instead of a minimize transition if (isMinimizingToPip) { val requestInfo = @@ -961,10 +966,13 @@ class DesktopTasksController( .apply { launchWindowingMode = WINDOWING_MODE_FREEFORM } .toBundle(), ) + val deskId = taskRepository.getDeskIdForTask(taskId) ?: getDefaultDeskId(DEFAULT_DISPLAY) startLaunchTransition( TRANSIT_OPEN, wct, taskId, + deskId = deskId, + displayId = DEFAULT_DISPLAY, remoteTransition = remoteTransition, unminimizeReason = unminimizeReason, ) @@ -982,19 +990,26 @@ class DesktopTasksController( remoteTransition: RemoteTransition? = null, unminimizeReason: UnminimizeReason = UnminimizeReason.UNKNOWN, ) { - logV("moveTaskToFront taskId=%s", taskInfo.taskId) + val deskId = + taskRepository.getDeskIdForTask(taskInfo.taskId) ?: getDefaultDeskId(taskInfo.displayId) + logV("moveTaskToFront taskId=%s deskId=%s", taskInfo.taskId, deskId) // If a task is tiled, another task should be brought to foreground with it so let // tiling controller handle the request. if (snapEventHandler.moveTaskToFrontIfTiled(taskInfo)) { return } val wct = WindowContainerTransaction() - wct.reorder(taskInfo.token, /* onTop= */ true, /* includingParents= */ true) + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + desksOrganizer.reorderTaskToFront(wct, deskId, taskInfo) + } else { + wct.reorder(taskInfo.token, /* onTop= */ true, /* includingParents= */ true) + } startLaunchTransition( transitionType = TRANSIT_TO_FRONT, wct = wct, launchingTaskId = taskInfo.taskId, remoteTransition = remoteTransition, + deskId = deskId, displayId = taskInfo.displayId, unminimizeReason = unminimizeReason, ) @@ -1006,14 +1021,22 @@ class DesktopTasksController( wct: WindowContainerTransaction, launchingTaskId: Int?, remoteTransition: RemoteTransition? = null, - displayId: Int = DEFAULT_DISPLAY, + deskId: Int, + displayId: Int, unminimizeReason: UnminimizeReason = UnminimizeReason.UNKNOWN, ): IBinder { + logV( + "startLaunchTransition type=%s launchingTaskId=%d deskId=%d displayId=%d", + WindowManager.transitTypeToString(transitionType), + launchingTaskId, + deskId, + displayId, + ) // TODO: b/397619806 - Consolidate sharable logic with [handleFreeformTaskLaunch]. var launchTransaction = wct val taskIdToMinimize = addAndGetMinimizeChanges( - displayId, + deskId, launchTransaction, newTaskId = launchingTaskId, launchingNewIntent = launchingTaskId == null, @@ -1027,21 +1050,20 @@ class DesktopTasksController( ) var activationRunOnTransitStart: RunOnTransitStart? = null val shouldActivateDesk = - (DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue || - DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) && - !isDesktopModeShowing(displayId) + when { + DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue -> + !taskRepository.isDeskActive(deskId) + DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue -> { + !isDesktopModeShowing(displayId) + } + else -> false + } if (shouldActivateDesk) { - val deskIdToActivate = - checkNotNull( - launchingTaskId?.let { taskRepository.getDeskIdForTask(it) } - ?: getDefaultDeskId(displayId) - ) val activateDeskWct = WindowContainerTransaction() // TODO: b/391485148 - pass in the launching task here to apply task-limit policy, // but make sure to not do it twice since it is also done at the start of this // function. - activationRunOnTransitStart = - addDeskActivationChanges(deskIdToActivate, activateDeskWct) + activationRunOnTransitStart = addDeskActivationChanges(deskId, activateDeskWct) // Desk activation must be handled before app launch-related transactions. activateDeskWct.merge(launchTransaction, /* transfer= */ true) launchTransaction = activateDeskWct @@ -1152,7 +1174,14 @@ class DesktopTasksController( } wct.sendPendingIntent(pendingIntent, intent, ops.toBundle()) - startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null) + val deskId = getDefaultDeskId(displayId) + startLaunchTransition( + TRANSIT_OPEN, + wct, + launchingTaskId = null, + deskId = deskId, + displayId = displayId, + ) } /** @@ -1691,34 +1720,7 @@ class DesktopTasksController( } private fun addLaunchHomePendingIntent(wct: WindowContainerTransaction, displayId: Int) { - val userHandle = UserHandle.of(userId) - val launchHomeIntent = - Intent(Intent.ACTION_MAIN).apply { - if (displayId != DEFAULT_DISPLAY) { - addCategory(Intent.CATEGORY_SECONDARY_HOME) - } else { - addCategory(Intent.CATEGORY_HOME) - } - } - val options = - ActivityOptions.makeBasic().apply { - launchWindowingMode = WINDOWING_MODE_FULLSCREEN - pendingIntentBackgroundActivityStartMode = - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS - if (Flags.enablePerDisplayDesktopWallpaperActivity()) { - launchDisplayId = displayId - } - } - val pendingIntent = - PendingIntent.getActivityAsUser( - context, - /* requestCode= */ 0, - launchHomeIntent, - PendingIntent.FLAG_IMMUTABLE, - /* options= */ null, - userHandle, - ) - wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle()) + homeIntentProvider.addLaunchHomePendingIntent(wct, displayId, userId) } private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) { @@ -2103,6 +2105,7 @@ class DesktopTasksController( // TODO(b/337915660): Add a transition handler for these; animations // need updates in some cases. val baseActivity = callingTaskInfo.baseActivity ?: return + val userHandle = UserHandle.of(callingTaskInfo.userId) val fillIn: Intent = userProfileContexts .getOrCreate(callingTaskInfo.userId) @@ -2110,11 +2113,13 @@ class DesktopTasksController( .getLaunchIntentForPackage(baseActivity.packageName) ?: return fillIn.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) val launchIntent = - PendingIntent.getActivity( + PendingIntent.getActivityAsUser( context, /* requestCode= */ 0, fillIn, PendingIntent.FLAG_IMMUTABLE, + /* options= */ null, + userHandle, ) val options = createNewWindowOptions(callingTaskInfo) when (options.launchWindowingMode) { @@ -2141,10 +2146,14 @@ class DesktopTasksController( WINDOWING_MODE_FREEFORM -> { val wct = WindowContainerTransaction() wct.sendPendingIntent(launchIntent, fillIn, options.toBundle()) + val deskId = + taskRepository.getDeskIdForTask(callingTaskInfo.taskId) + ?: getDefaultDeskId(callingTaskInfo.displayId) startLaunchTransition( transitionType = TRANSIT_OPEN, wct = wct, launchingTaskId = null, + deskId = deskId, displayId = callingTaskInfo.displayId, ) } @@ -2224,6 +2233,7 @@ class DesktopTasksController( logV("skip keyguard is locked") return null } + val deskId = getDefaultDeskId(task.displayId) val wct = WindowContainerTransaction() if (shouldFreeformTaskLaunchSwitchToFullscreen(task)) { logD("Bring desktop tasks to front on transition=taskId=%d", task.taskId) @@ -2246,7 +2256,6 @@ class DesktopTasksController( runOnTransitStart?.invoke(transition) return wct } - val deskId = getDefaultDeskId(task.displayId) val runOnTransitStart = addDeskActivationChanges(deskId, wct, task) runOnTransitStart?.invoke(transition) wct.reorder(task.token, true) @@ -2288,7 +2297,7 @@ class DesktopTasksController( reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH, ) // 2) minimize a Task if needed. - val taskIdToMinimize = addAndGetMinimizeChanges(task.displayId, wct, task.taskId) + val taskIdToMinimize = addAndGetMinimizeChanges(deskId, wct, task.taskId) addPendingAppLaunchTransition(transition, task.taskId, taskIdToMinimize) if (taskIdToMinimize != null) { addPendingMinimizeTransition(transition, taskIdToMinimize, MinimizeReason.TASK_LIMIT) @@ -2334,7 +2343,7 @@ class DesktopTasksController( // The desk was already showing and we're launching a new Task - we // might need to minimize another Task. val taskIdToMinimize = - addAndGetMinimizeChanges(task.displayId, wct, task.taskId) + addAndGetMinimizeChanges(deskId, wct, task.taskId) taskIdToMinimize?.let { minimizingTaskId -> addPendingMinimizeTransition( transition, @@ -2670,7 +2679,7 @@ class DesktopTasksController( /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */ private fun addAndGetMinimizeChanges( - displayId: Int, + deskId: Int, wct: WindowContainerTransaction, newTaskId: Int?, launchingNewIntent: Boolean = false, @@ -2679,7 +2688,7 @@ class DesktopTasksController( require(newTaskId == null || !launchingNewIntent) return desktopTasksLimiter .get() - .addAndGetMinimizeTaskChanges(displayId, wct, newTaskId, launchingNewIntent) + .addAndGetMinimizeTaskChanges(deskId, wct, newTaskId, launchingNewIntent) } private fun addPendingMinimizeTransition( @@ -2782,9 +2791,20 @@ class DesktopTasksController( taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( doesAnyTaskRequireTaskbarRounding(displayId) ) - // TODO: b/362720497 - activating a desk with the intention to move a new task to - // it means we may need to minimize something in the activating desk. Do so here - // similar to how it's done in #bringDesktopAppsToFront. + val expandedTasksOrderedFrontToBack = + taskRepository.getExpandedTasksIdsInDeskOrdered(deskId = deskId) + // If we're adding a new Task we might need to minimize an old one + val taskIdToMinimize = + desktopTasksLimiter + .getOrNull() + ?.getTaskIdToMinimize(expandedTasksOrderedFrontToBack, newTaskIdInFront) + if (taskIdToMinimize != null) { + val taskToMinimize = shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize) + // TODO(b/365725441): Handle non running task minimization + if (taskToMinimize != null) { + desksOrganizer.minimizeTask(wct, deskId, taskToMinimize) + } + } return { transition -> val activateDeskTransition = if (newTaskIdInFront != null) { @@ -2802,6 +2822,9 @@ class DesktopTasksController( ) } desksTransitionObserver.addPendingTransition(activateDeskTransition) + taskIdToMinimize?.let { minimizingTask -> + addPendingMinimizeTransition(transition, minimizingTask, MinimizeReason.TASK_LIMIT) + } } } @@ -3381,7 +3404,14 @@ class DesktopTasksController( if (windowingMode == WINDOWING_MODE_FREEFORM) { if (DesktopModeFlags.ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX.isTrue()) { // TODO b/376389593: Use a custom tab tearing transition/animation - startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null) + val deskId = getDefaultDeskId(DEFAULT_DISPLAY) + startLaunchTransition( + TRANSIT_OPEN, + wct, + launchingTaskId = null, + deskId = deskId, + displayId = DEFAULT_DISPLAY, + ) } else { desktopModeDragAndDropTransitionHandler.handleDropEvent(wct) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index da369f094405..4ca58823b52b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -22,6 +22,7 @@ import android.os.Handler import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_TO_BACK +import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags import android.window.TransitionInfo import android.window.WindowContainerTransaction @@ -31,6 +32,7 @@ import com.android.internal.protolog.ProtoLog import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.sysui.UserChangeListener @@ -48,6 +50,7 @@ class DesktopTasksLimiter( transitions: Transitions, private val desktopUserRepositories: DesktopUserRepositories, private val shellTaskOrganizer: ShellTaskOrganizer, + private val desksOrganizer: DesksOrganizer, private val maxTasksLimit: Int?, private val interactionJankMonitor: InteractionJankMonitor, private val context: Context, @@ -258,7 +261,7 @@ class DesktopTasksLimiter( * returning the task to minimize. */ fun addAndGetMinimizeTaskChanges( - displayId: Int, + deskId: Int, wct: WindowContainerTransaction, newFrontTaskId: Int?, launchingNewIntent: Boolean = false, @@ -267,15 +270,19 @@ class DesktopTasksLimiter( val taskRepository = desktopUserRepositories.current val taskIdToMinimize = getTaskIdToMinimize( - taskRepository.getExpandedTasksOrdered(displayId), + taskRepository.getExpandedTasksIdsInDeskOrdered(deskId), newFrontTaskId, launchingNewIntent, ) - // If it's a running task, reorder it to back. taskIdToMinimize ?.let { shellTaskOrganizer.getRunningTaskInfo(it) } - // TODO: b/391485148 - this won't really work with multi-desks enabled. - ?.let { wct.reorder(it.token, /* onTop= */ false) } + ?.let { task -> + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + wct.reorder(task.token, /* onTop= */ false) + } else { + desksOrganizer.minimizeTask(wct, deskId, task) + } + } return taskIdToMinimize } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index bd9c30e2a495..7dabeb7c9d15 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -36,7 +36,6 @@ import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.back.BackAnimationController import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isExitDesktopModeTransition import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider -import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.desktopmode.DesktopModeStatus @@ -58,7 +57,6 @@ class DesktopTasksTransitionObserver( private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler, private val backAnimationController: BackAnimationController, private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, - private val desksTransitionObserver: DesksTransitionObserver, shellInit: ShellInit, ) : Transitions.TransitionObserver { @@ -88,7 +86,6 @@ class DesktopTasksTransitionObserver( finishTransaction: SurfaceControl.Transaction, ) { // TODO: b/332682201 Update repository state - desksTransitionObserver.onTransitionReady(transition, info) if ( DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index e943c42dcdfc..4f511a901756 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -327,8 +327,9 @@ sealed class DragToDesktopTransitionHandler( val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null taskInfo") val dragPosition = PointF(state.dragAnimator.position) val scale = state.dragAnimator.scale + val cornerRadius = state.dragAnimator.cornerRadius state.dragAnimator.cancelAnimator() - requestBubble(wct, taskInfo, onLeft, scale, dragPosition) + requestBubble(wct, taskInfo, onLeft, scale, cornerRadius, dragPosition) } private fun requestBubble( @@ -336,13 +337,14 @@ sealed class DragToDesktopTransitionHandler( taskInfo: RunningTaskInfo, onLeft: Boolean, taskScale: Float = 1f, + cornerRadius: Float = 0f, dragPosition: PointF = PointF(0f, 0f), ) { val controller = bubbleController.orElseThrow { IllegalStateException("BubbleController not set") } controller.expandStackAndSelectBubble( taskInfo, - BubbleTransitions.DragData(onLeft, taskScale, dragPosition, wct), + BubbleTransitions.DragData(onLeft, taskScale, cornerRadius, dragPosition, wct), ) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt index fc359d7d67b6..8c4bc2598dff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt @@ -40,6 +40,13 @@ interface DesksOrganizer { task: ActivityManager.RunningTaskInfo, ) + /** Reorders a desk's task to the front. */ + fun reorderTaskToFront( + wct: WindowContainerTransaction, + deskId: Int, + task: ActivityManager.RunningTaskInfo, + ) + /** Minimizes the given task of the given deskId. */ fun minimizeTask( wct: WindowContainerTransaction, @@ -47,6 +54,13 @@ interface DesksOrganizer { task: ActivityManager.RunningTaskInfo, ) + /** Unminimize the given task of the given desk. */ + fun unminimizeTask( + wct: WindowContainerTransaction, + deskId: Int, + task: ActivityManager.RunningTaskInfo, + ) + /** Whether the change is for the given desk id. */ fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt index f576258ebdaa..49ca58e7b32a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt @@ -33,6 +33,7 @@ import androidx.core.util.forEach import com.android.internal.annotations.VisibleForTesting import com.android.internal.protolog.ProtoLog import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.LaunchAdjacentController import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.sysui.ShellCommandHandler @@ -44,6 +45,7 @@ class RootTaskDesksOrganizer( shellInit: ShellInit, shellCommandHandler: ShellCommandHandler, private val shellTaskOrganizer: ShellTaskOrganizer, + private val launchAdjacentController: LaunchAdjacentController, ) : DesksOrganizer, ShellTaskOrganizer.TaskListener { private val createDeskRootRequests = mutableListOf<CreateDeskRequest>() @@ -110,7 +112,31 @@ class RootTaskDesksOrganizer( wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true) } + override fun reorderTaskToFront( + wct: WindowContainerTransaction, + deskId: Int, + task: RunningTaskInfo, + ) { + logV("reorderTaskToFront task=${task.taskId} desk=$deskId") + val root = deskRootsByDeskId[deskId] ?: error("Root not found for desk: $deskId") + if (task.taskId in root.children) { + wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true) + return + } + val minimizationRoot = + checkNotNull(deskMinimizationRootsByDeskId[deskId]) { + "Minimization root not found for desk: $deskId" + } + if (task.taskId in minimizationRoot.children) { + unminimizeTask(wct, deskId, task) + wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true) + return + } + logE("Attempted to reorder task=${task.taskId} in desk=$deskId but it was not a child") + } + override fun minimizeTask(wct: WindowContainerTransaction, deskId: Int, task: RunningTaskInfo) { + logV("minimizeTask task=${task.taskId} desk=$deskId") val deskRoot = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" } val minimizationRoot = @@ -129,6 +155,30 @@ class RootTaskDesksOrganizer( wct.reparent(task.token, minimizationRoot.token, /* onTop= */ true) } + override fun unminimizeTask( + wct: WindowContainerTransaction, + deskId: Int, + task: RunningTaskInfo, + ) { + val taskId = task.taskId + logV("unminimizeTask task=$taskId desk=$deskId") + val deskRoot = + checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" } + val minimizationRoot = + checkNotNull(deskMinimizationRootsByDeskId[deskId]) { + "Minimization root not found for desk: $deskId" + } + if (taskId in deskRoot.children) { + logV("Task #$taskId is already unminimized in desk=$deskId") + return + } + if (taskId !in minimizationRoot.children) { + logE("Attempted to unminimize task=$taskId in desk=$deskId but it was not a child") + return + } + wct.reparent(task.token, deskRoot.token, /* onTop= */ true) + } + override fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean = (isDeskRootChange(change) && change.taskId == deskId) || (getDeskMinimizationRootInChange(change)?.deskId == deskId) @@ -164,6 +214,21 @@ class RootTaskDesksOrganizer( change.mode == TRANSIT_TO_FRONT override fun onTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) { + handleTaskAppeared(taskInfo, leash) + updateLaunchAdjacentController() + } + + override fun onTaskInfoChanged(taskInfo: RunningTaskInfo) { + handleTaskInfoChanged(taskInfo) + updateLaunchAdjacentController() + } + + override fun onTaskVanished(taskInfo: RunningTaskInfo) { + handleTaskVanished(taskInfo) + updateLaunchAdjacentController() + } + + private fun handleTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) { // Check whether this task is appearing inside a desk. if (taskInfo.parentTaskId in deskRootsByDeskId) { val deskId = taskInfo.parentTaskId @@ -216,7 +281,7 @@ class RootTaskDesksOrganizer( hideMinimizationRoot(deskMinimizationRoot) } - override fun onTaskInfoChanged(taskInfo: RunningTaskInfo) { + private fun handleTaskInfoChanged(taskInfo: RunningTaskInfo) { if (deskRootsByDeskId.contains(taskInfo.taskId)) { val deskId = taskInfo.taskId deskRootsByDeskId[deskId] = deskRootsByDeskId[deskId].copy(taskInfo = taskInfo) @@ -254,7 +319,7 @@ class RootTaskDesksOrganizer( logE("onTaskInfoChanged: unknown task: ${taskInfo.taskId}") } - override fun onTaskVanished(taskInfo: RunningTaskInfo) { + private fun handleTaskVanished(taskInfo: RunningTaskInfo) { if (deskRootsByDeskId.contains(taskInfo.taskId)) { val deskId = taskInfo.taskId val deskRoot = deskRootsByDeskId[deskId] @@ -336,6 +401,18 @@ class RootTaskDesksOrganizer( deskRootsByDeskId.forEach { _, deskRoot -> deskRoot.children -= taskId } } + private fun updateLaunchAdjacentController() { + deskRootsByDeskId.forEach { deskId, root -> + if (root.taskInfo.isVisible) { + // Disable launch adjacent handling if any desk is active, otherwise the split + // launch root and the desk root will both be eligible to take launching tasks. + launchAdjacentController.launchAdjacentEnabled = false + return + } + } + launchAdjacentController.launchAdjacentEnabled = true + } + @VisibleForTesting data class DeskRoot( val deskId: Int, @@ -377,6 +454,9 @@ class RootTaskDesksOrganizer( override fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("$prefix$TAG") + pw.println( + "${innerPrefix}launchAdjacentEnabled=" + launchAdjacentController.launchAdjacentEnabled + ) pw.println("${innerPrefix}Desk Roots:") deskRootsByDeskId.forEach { deskId, root -> val minimizationRoot = deskMinimizationRootsByDeskId[deskId] diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index 897e2d1601a5..2fe786531af5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -24,6 +24,7 @@ import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.util.SparseArray; import android.view.SurfaceControl; +import android.window.DesktopExperienceFlags; import android.window.DesktopModeFlags; import com.android.internal.protolog.ProtoLog; @@ -167,6 +168,11 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, } private void updateLaunchAdjacentController() { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) { + // With multiple desks, freeform tasks are children of a root task controlled by + // DesksOrganizer, so toggling launch-adjacent should be managed there. + return; + } for (int i = 0; i < mTasks.size(); i++) { if (mTasks.valueAt(i).mTaskInfo.isVisible) { mLaunchAdjacentController.setLaunchAdjacentEnabled(false); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java index 8059b94685ba..f89ba0a168d1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -29,6 +29,7 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.wm.shell.desktopmode.DesktopImmersiveController; +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.Transitions; @@ -52,6 +53,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs private final WindowDecorViewModel mWindowDecorViewModel; private final Optional<TaskChangeListener> mTaskChangeListener; private final FocusTransitionObserver mFocusTransitionObserver; + private final Optional<DesksTransitionObserver> mDesksTransitionObserver; private final Map<IBinder, List<ActivityManager.RunningTaskInfo>> mTransitionToTaskInfo = new HashMap<>(); @@ -63,12 +65,14 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs Optional<DesktopImmersiveController> desktopImmersiveController, WindowDecorViewModel windowDecorViewModel, Optional<TaskChangeListener> taskChangeListener, - FocusTransitionObserver focusTransitionObserver) { + FocusTransitionObserver focusTransitionObserver, + Optional<DesksTransitionObserver> desksTransitionObserver) { mTransitions = transitions; mDesktopImmersiveController = desktopImmersiveController; mWindowDecorViewModel = windowDecorViewModel; mTaskChangeListener = taskChangeListener; mFocusTransitionObserver = focusTransitionObserver; + mDesksTransitionObserver = desksTransitionObserver; if (FreeformComponents.requiresFreeformComponents(context)) { shellInit.addInitCallback(this::onInit, this); } @@ -85,6 +89,10 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT) { + // Update desk state first, otherwise [TaskChangeListener] may update desktop task state + // under an outdated active desk if a desk switch and a task update happen in the same + // transition, such as when unminimizing a task from an inactive desk. + mDesksTransitionObserver.ifPresent(o -> o.onTransitionReady(transition, info)); if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) { // TODO(b/367268953): Remove when DesktopTaskListener is introduced and the repository // is updated from there **before** the |mWindowDecorViewModel| methods are invoked. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java index 65099c2dfb9d..671eae3d84ef 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java @@ -153,7 +153,12 @@ public class PhonePipMenuController implements PipMenuController, mPipUiEventLogger = pipUiEventLogger; mPipTransitionState.addPipTransitionStateChangedListener(this); - + // Clear actions after exit PiP. Otherwise, next PiP could accidentally inherit the + // actions provided by the previous app in PiP mode. + mPipBoundsState.addOnPipComponentChangedListener(((oldPipComponent, newPipComponent) -> { + if (mAppActions != null) mAppActions.clear(); + mCloseAction = null; + })); mPipTaskListener.addParamsChangedListener(new PipTaskListener.PipParamsChangedCallback() { @Override public void onActionsChanged(List<RemoteAction> actions, RemoteAction closeAction) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java index d6634845ee21..294ef48c01d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java @@ -61,7 +61,7 @@ public class PipTaskListener implements ShellTaskOrganizer.TaskListener, private final PipBoundsState mPipBoundsState; private final PipBoundsAlgorithm mPipBoundsAlgorithm; private final ShellExecutor mMainExecutor; - private final PictureInPictureParams mPictureInPictureParams = + private PictureInPictureParams mPictureInPictureParams = new PictureInPictureParams.Builder().build(); private boolean mWaitingForAspectRatioChange = false; @@ -92,6 +92,11 @@ public class PipTaskListener implements ShellTaskOrganizer.TaskListener, } mPipResizeAnimatorSupplier = PipResizeAnimator::new; mPipScheduler.setPipParamsSupplier(this::getPictureInPictureParams); + // Reset {@link #mPictureInPictureParams} after exiting PiP. For instance, next Activity + // with null aspect ratio would accidentally inherit the aspect ratio from a previous + // PiP Activity. + mPipBoundsState.addOnPipComponentChangedListener(((oldPipComponent, newPipComponent) -> + mPictureInPictureParams = new PictureInPictureParams.Builder().build())); } void setPictureInPictureParams(@Nullable PictureInPictureParams params) { @@ -138,9 +143,8 @@ public class PipTaskListener implements ShellTaskOrganizer.TaskListener, if (mPictureInPictureParams.hasSetAspectRatio() && mPipBoundsAlgorithm.isValidPictureInPictureAspectRatio(newAspectRatio) && PipUtils.aspectRatioChanged(newAspectRatio, mPipBoundsState.getAspectRatio())) { - mPipTransitionState.setOnIdlePipTransitionStateRunnable(() -> { - onAspectRatioChanged(newAspectRatio); - }); + mPipTransitionState.setOnIdlePipTransitionStateRunnable( + () -> onAspectRatioChanged(newAspectRatio)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 7472b0ea56ca..c370c0cb0930 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -2920,7 +2920,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareEnterSplitScreen(out); mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering, SNAP_TO_2_50_50); - } else if (isSplitScreenVisible() && isOpening) { + } else if (enableFlexibleTwoAppSplit() && isSplitScreenVisible() && isOpening) { // launching into an existing split stage; possibly launchAdjacent // If we're replacing a pip-able app, we need to let mixed handler take care of // it. Otherwise we'll just treat it as an enter+resize 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 ffb81f8c33e5..7ef1a93cbe45 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 @@ -17,9 +17,11 @@ package com.android.wm.shell.windowdecor; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_HOVER_ENTER; @@ -77,6 +79,7 @@ import android.view.SurfaceControl.Transaction; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewRootImpl; +import android.view.WindowManager; import android.window.DesktopModeFlags; import android.window.TaskSnapshot; import android.window.WindowContainerToken; @@ -118,6 +121,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition; import com.android.wm.shell.desktopmode.DesktopTasksLimiter; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.DesktopWallpaperActivity; import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction; import com.android.wm.shell.desktopmode.common.ToggleTaskSizeUtilsKt; @@ -142,7 +146,6 @@ import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionRegionListener; -import com.android.wm.shell.windowdecor.common.AppHandleAndHeaderVisibilityHelper; import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier; @@ -204,7 +207,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final Optional<DesktopTasksLimiter> mDesktopTasksLimiter; private final AppHandleEducationController mAppHandleEducationController; private final AppToWebEducationController mAppToWebEducationController; - private final AppHandleAndHeaderVisibilityHelper mAppHandleAndHeaderVisibilityHelper; private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory; private boolean mTransitionDragActive; @@ -292,7 +294,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, Optional<DesktopTasksLimiter> desktopTasksLimiter, AppHandleEducationController appHandleEducationController, AppToWebEducationController appToWebEducationController, - AppHandleAndHeaderVisibilityHelper appHandleAndHeaderVisibilityHelper, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, FocusTransitionObserver focusTransitionObserver, @@ -337,7 +338,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, desktopTasksLimiter, appHandleEducationController, appToWebEducationController, - appHandleAndHeaderVisibilityHelper, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, new TaskPositionerFactory(), @@ -386,7 +386,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, Optional<DesktopTasksLimiter> desktopTasksLimiter, AppHandleEducationController appHandleEducationController, AppToWebEducationController appToWebEducationController, - AppHandleAndHeaderVisibilityHelper appHandleAndHeaderVisibilityHelper, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, TaskPositionerFactory taskPositionerFactory, @@ -432,7 +431,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopTasksLimiter = desktopTasksLimiter; mAppHandleEducationController = appHandleEducationController; mAppToWebEducationController = appToWebEducationController; - mAppHandleAndHeaderVisibilityHelper = appHandleAndHeaderVisibilityHelper; mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository; mActivityOrientationChangeHandler = activityOrientationChangeHandler; mAssistContentRequester = assistContentRequester; @@ -530,7 +528,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, @Override public void setSplitScreenController(SplitScreenController splitScreenController) { mSplitScreenController = splitScreenController; - mAppHandleAndHeaderVisibilityHelper.setSplitScreenController(splitScreenController); } @Override @@ -1720,7 +1717,32 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { - return mAppHandleAndHeaderVisibilityHelper.shouldShowAppHandleOrHeader(taskInfo); + if (mDisplayController.getDisplay(taskInfo.displayId) == null) { + // If DisplayController doesn't have it tracked, it could be a private/managed display. + return false; + } + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true; + if (mSplitScreenController != null + && mSplitScreenController.isTaskRootOrStageRoot(taskInfo.taskId)) { + return false; + } + if (mDesktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(taskInfo)) { + return false; + } + final boolean isOnLargeScreen = + mDisplayController.getDisplay(taskInfo.displayId).getMinSizeDimensionDp() + >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; + if (!DesktopModeStatus.canEnterDesktopMode(mContext) + && DesktopModeStatus.overridesShowAppHandle(mContext) && !isOnLargeScreen) { + // Devices with multiple screens may enable the app handle but it should not show on + // small screens + return false; + } + return DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(mContext) + && !DesktopWallpaperActivity.isWallpaperTask(taskInfo) + && taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED + && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD + && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop(); } private void createWindowDecoration( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt index 22bc9782170b..cf536eba8382 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt @@ -42,8 +42,6 @@ class MoveToDesktopAnimator @JvmOverloads constructor( .setDuration(ANIMATION_DURATION.toLong()) .apply { val t = SurfaceControl.Transaction() - val cornerRadius = context.resources - .getDimensionPixelSize(R.dimen.desktop_mode_dragged_task_radius).toFloat() addUpdateListener { setTaskPosition(mostRecentInput.x, mostRecentInput.y) t.setScale(taskSurface, scale, scale) @@ -57,6 +55,8 @@ class MoveToDesktopAnimator @JvmOverloads constructor( val taskId get() = taskInfo.taskId val position: PointF = PointF(0.0f, 0.0f) + val cornerRadius: Float = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_dragged_task_radius).toFloat() /** * Whether motion events from the drag gesture should affect the dragged surface or not. Used diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt deleted file mode 100644 index 39ccf5bd03a7..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2025 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.windowdecor.common - -import android.app.ActivityManager -import android.app.WindowConfiguration -import android.content.Context -import android.view.WindowManager -import android.window.DesktopExperienceFlags.ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY -import com.android.wm.shell.common.DisplayController -import com.android.wm.shell.desktopmode.DesktopWallpaperActivity.Companion.isWallpaperTask -import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy -import com.android.wm.shell.shared.desktopmode.DesktopModeStatus -import com.android.wm.shell.splitscreen.SplitScreenController - -/** - * Resolves whether, given a task and its associated display that it is currently on, to show the - * app handle/header or not. - */ -class AppHandleAndHeaderVisibilityHelper ( - private val context: Context, - private val displayController: DisplayController, - private val desktopModeCompatPolicy: DesktopModeCompatPolicy -) { - var splitScreenController: SplitScreenController? = null - - /** - * Returns, given a task's attribute and its display attribute, whether the app - * handle/header should show or not for this task. - */ - fun shouldShowAppHandleOrHeader(taskInfo: ActivityManager.RunningTaskInfo): Boolean { - if (!ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY.isTrue) { - return allowedForTask(taskInfo) - } - return allowedForTask(taskInfo) && allowedForDisplay(taskInfo.displayId) - } - - private fun allowedForTask(taskInfo: ActivityManager.RunningTaskInfo): Boolean { - // TODO (b/382023296): Remove once we no longer rely on - // Flags.enableBugFixesForSecondaryDisplay as it is taken care of in #allowedForDisplay - if (displayController.getDisplay(taskInfo.displayId) == null) { - // If DisplayController doesn't have it tracked, it could be a private/managed display. - return false - } - if (taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) return true - if (splitScreenController?.isTaskRootOrStageRoot(taskInfo.taskId) == true) { - return false - } - - if (desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(taskInfo)) { - return false - } - - // TODO (b/382023296): Remove once we no longer rely on - // Flags.enableBugFixesForSecondaryDisplay as it is taken care of in #allowedForDisplay - val isOnLargeScreen = - displayController.getDisplay(taskInfo.displayId).minSizeDimensionDp >= - WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP - if (!DesktopModeStatus.canEnterDesktopMode(context) - && DesktopModeStatus.overridesShowAppHandle(context) - && !isOnLargeScreen - ) { - // Devices with multiple screens may enable the app handle but it should not show on - // small screens - return false - } - return DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context) - && !isWallpaperTask(taskInfo) - && taskInfo.windowingMode != WindowConfiguration.WINDOWING_MODE_PINNED - && taskInfo.activityType == WindowConfiguration.ACTIVITY_TYPE_STANDARD - && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop - } - - private fun allowedForDisplay(displayId: Int): Boolean { - // If DisplayController doesn't have it tracked, it could be a private/managed display. - val display = displayController.getDisplay(displayId) - if (display == null) return false - - if (DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, display)) { - return true - } - // If on default display and on Large Screen (unfolded), show app handle - return DesktopModeStatus.overridesShowAppHandle(context) - && display.minSizeDimensionDp >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index c3f5b3f20f4a..30712b55bdfa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -466,11 +466,7 @@ class AppHeaderViewHolder( override fun onHandleMenuOpened() {} - override fun onHandleMenuClosed() { - openMenuButton.post { - openMenuButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) - } - } + override fun onHandleMenuClosed() {} fun onMaximizeWindowHoverExit() { maximizeButtonView.cancelHoverAnimation() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt index 3e98e4342b3f..7d9f2bf8fdf6 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt @@ -19,7 +19,7 @@ package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation -import android.tools.device.apphelpers.BrowserAppHelper +import android.tools.device.apphelpers.GmailAppHelper import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry @@ -44,8 +44,8 @@ abstract class UnminimizeAppFromTaskbar(val rotation: Rotation = Rotation.ROTATI private val wmHelper = WindowManagerStateHelper(instrumentation) private val device = UiDevice.getInstance(instrumentation) private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) - private val browserHelper = BrowserAppHelper(instrumentation) - private val browserApp = DesktopModeAppHelper(browserHelper) + private val gmailHelper = GmailAppHelper(instrumentation) + private val gmailApp = DesktopModeAppHelper(gmailHelper) @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) @@ -59,20 +59,20 @@ abstract class UnminimizeAppFromTaskbar(val rotation: Rotation = Rotation.ROTATI ChangeDisplayOrientationRule.setRotation(rotation) testApp.enterDesktopMode(wmHelper, device) tapl.showTaskbarIfHidden() - browserApp.launchViaIntent(wmHelper) - browserApp.minimizeDesktopApp(wmHelper, device) + gmailApp.launchViaIntent(wmHelper) + gmailApp.minimizeDesktopApp(wmHelper, device) } @Test open fun unminimizeApp() { tapl.launchedAppState.taskbar - .getAppIcon(browserHelper.appName) - .launch(browserHelper.packageName) + .getAppIcon(gmailHelper.appName) + .launch(gmailHelper.packageName) } @After fun teardown() { testApp.exit(wmHelper) - browserApp.exit(wmHelper) + gmailApp.exit(wmHelper) } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java index 3c79ea7be39f..925ca0f1638d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java @@ -221,8 +221,8 @@ public class BubbleTransitionsTest extends ShellTestCase { PointF dragPosition = new PointF(10f, 20f); BubbleTransitions.DragData dragData = new BubbleTransitions.DragData( - /* releasedOnLeft= */ false, /* taskScale= */ 0.5f, dragPosition, - pendingWct); + /* releasedOnLeft= */ false, /* taskScale= */ 0.5f, /* cornerRadius= */ 10f, + dragPosition, pendingWct); final BubbleTransitions.BubbleTransition bt = mBubbleTransitions.startConvertToBubble( mBubble, taskInfo, mExpandedViewManager, mTaskViewFactory, mBubblePositioner, @@ -253,6 +253,8 @@ public class BubbleTransitionsTest extends ShellTestCase { verify(startT).setPosition(snapshot, dragPosition.x, dragPosition.y); // Snapshot has the scale of the dragged task verify(startT).setScale(snapshot, dragData.getTaskScale(), dragData.getTaskScale()); + // Snapshot has dragged task corner radius + verify(startT).setCornerRadius(snapshot, dragData.getCornerRadius()); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java index 01b76edd9b25..1066276becc7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java @@ -19,6 +19,8 @@ package com.android.wm.shell.common.pip; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -128,6 +130,31 @@ public class PipBoundsStateTest extends ShellTestCase { } @Test + public void setLastPipComponentName_notChanged_doesNotCallbackComponentChangedListener() { + mPipBoundsState.setLastPipComponentName(mTestComponentName1); + PipBoundsState.OnPipComponentChangedListener mockListener = + mock(PipBoundsState.OnPipComponentChangedListener.class); + + mPipBoundsState.addOnPipComponentChangedListener(mockListener); + mPipBoundsState.setLastPipComponentName(mTestComponentName1); + + verify(mockListener, never()).onPipComponentChanged(any(), any()); + } + + @Test + public void setLastPipComponentName_changed_callbackComponentChangedListener() { + mPipBoundsState.setLastPipComponentName(mTestComponentName1); + PipBoundsState.OnPipComponentChangedListener mockListener = + mock(PipBoundsState.OnPipComponentChangedListener.class); + + mPipBoundsState.addOnPipComponentChangedListener(mockListener); + mPipBoundsState.setLastPipComponentName(mTestComponentName2); + + verify(mockListener).onPipComponentChanged( + eq(mTestComponentName1), eq(mTestComponentName2)); + } + + @Test public void testSetLastPipComponentName_notChanged_doesNotClearReentryState() { mPipBoundsState.setLastPipComponentName(mTestComponentName1); mPipBoundsState.saveReentryState(DEFAULT_SNAP_FRACTION); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/crashhandling/ShellCrashHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/crashhandling/ShellCrashHandlerTest.kt new file mode 100644 index 000000000000..5c77f78a7dfa --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/crashhandling/ShellCrashHandlerTest.kt @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2025 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.crashhandling + +import android.app.ActivityManager.RunningTaskInfo +import android.app.PendingIntent +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.platform.test.annotations.DisableFlags +import android.view.Display.DEFAULT_DISPLAY +import android.window.IWindowContainerToken +import android.window.WindowContainerToken +import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT +import com.android.modules.utils.testing.ExtendedMockitoRule +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.HomeIntentProvider +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellInit +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.Before +import org.junit.Rule +import org.mockito.ArgumentCaptor +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class ShellCrashHandlerTest : ShellTestCase() { + @JvmField + @Rule + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this) + .mockStatic(DesktopModeStatus::class.java) + .mockStatic(PendingIntent::class.java) + .build()!! + + private val testExecutor = mock<ShellExecutor>() + private val context = mock<Context>() + private val shellTaskOrganizer = mock<ShellTaskOrganizer>() + + private lateinit var homeIntentProvider: HomeIntentProvider + private lateinit var crashHandler: ShellCrashHandler + private lateinit var shellInit: ShellInit + + + @Before + fun setup() { + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + whenever(PendingIntent.getActivity(any(), any(), any(), any(), any())).thenReturn(mock()) + + shellInit = spy(ShellInit(testExecutor)) + + homeIntentProvider = HomeIntentProvider(context) + crashHandler = ShellCrashHandler(context, shellTaskOrganizer, homeIntentProvider, shellInit) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun init_freeformTaskExists_sendsHomeIntent() { + val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(createTaskInfo(1))) + + shellInit.init() + + verify(shellTaskOrganizer).applyTransaction( + wctCaptor.capture() + ) + wctCaptor.value.assertPendingIntentAt(0, launchHomeIntent(DEFAULT_DISPLAY)) + } + + private fun launchHomeIntent(displayId: Int): Intent { + return Intent(Intent.ACTION_MAIN).apply { + if (displayId != DEFAULT_DISPLAY) { + addCategory(Intent.CATEGORY_SECONDARY_HOME) + } else { + addCategory(Intent.CATEGORY_HOME) + } + } + } + + private fun createTaskInfo(id: Int, windowingMode: Int = WINDOWING_MODE_FREEFORM) = + RunningTaskInfo().apply { + taskId = id + displayId = DEFAULT_DISPLAY + configuration.windowConfiguration.windowingMode = windowingMode + token = WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)) + baseIntent = Intent().apply { component = ComponentName("package", "component.name") } + } + + private fun WindowContainerTransaction.assertPendingIntentAt(index: Int, intent: Intent) { + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT) + assertThat(op.activityIntent?.component).isEqualTo(intent.component) + assertThat(op.activityIntent?.categories).isEqualTo(intent.categories) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 8f499c959759..d093629000f3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -101,6 +101,7 @@ import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.bubbles.BubbleController import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.HomeIntentProvider import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue @@ -275,6 +276,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() private lateinit var testScope: CoroutineScope private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy private lateinit var spyContext: TestableContext + private lateinit var homeIntentProvider: HomeIntentProvider private val shellExecutor = TestShellExecutor() private val bgExecutor = TestShellExecutor() @@ -323,12 +325,14 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() transitions, userRepositories, shellTaskOrganizer, + desksOrganizer, MAX_TASK_LIMIT, mockInteractionJankMonitor, mContext, mockHandler, ) desktopModeCompatPolicy = spy(DesktopModeCompatPolicy(spyContext)) + homeIntentProvider = HomeIntentProvider(context) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } whenever(transitions.startTransition(anyInt(), any(), anyOrNull())).thenAnswer { Binder() } @@ -448,6 +452,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() desktopModeCompatPolicy, dragToDisplayTransitionHandler, moveToDisplayTransitionHandler, + homeIntentProvider, ) @After @@ -2363,6 +2368,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveTaskToFront_postsWctWithReorderOp() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -2385,9 +2391,34 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() { + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToFront_reordersToFront() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0) + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0) + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_TO_FRONT), + any(), + eq(task1.taskId), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(Binder()) + + controller.moveTaskToFront(task1, remoteTransition = null) + + verify(desksOrganizer).reorderTaskToFront(any(), eq(0), eq(task1)) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToFront_bringsTasksOverLimit_multiDesksDisabled_minimizesBackTask() { setUpHomeTask() - val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() } + val freeformTasks = + (1..MAX_TASK_LIMIT + 1).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0) + } whenever( desktopMixedTransitionHandler.startLaunchTransition( eq(TRANSIT_TO_FRONT), @@ -2408,6 +2439,32 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToFront_bringsTasksOverLimit_multiDesksEnabled_minimizesBackTask() { + val deskId = 0 + setUpHomeTask() + val freeformTasks = + (1..MAX_TASK_LIMIT + 1).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_TO_FRONT), + any(), + eq(freeformTasks[0].taskId), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(Binder()) + + controller.moveTaskToFront(freeformTasks[0], remoteTransition = null) + + val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT) + verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[1]) + } + + @Test fun moveTaskToFront_minimizedTask_marksTaskAsUnminimized() { val transition = Binder() val freeformTask = setUpFreeformTask() @@ -2496,8 +2553,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_minimizesBackTask() { - val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_multiDesksDisabled_minimizesBackTask() { + val deskId = 0 + val freeformTasks = + (1..MAX_TASK_LIMIT).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } val task = createRecentTaskInfo(1001) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null) whenever( @@ -2520,6 +2582,33 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_multiDesksEnabled_minimizesBackTask() { + val deskId = 0 + val freeformTasks = + (1..MAX_TASK_LIMIT).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } + val task = createRecentTaskInfo(1001) + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null) + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_OPEN), + any(), + eq(task.taskId), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(Binder()) + + controller.moveTaskToFront(task.taskId, unminimizeReason = UnminimizeReason.UNKNOWN) + + val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN) + verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[0]) + } + + @Test fun moveToNextDisplay_noOtherDisplays() { whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY)) val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -3530,7 +3619,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskToDesk_underTaskLimit_multiDesksDisabled_dontMinimize() { val freeformTask = setUpFreeformTask() markTaskVisible(freeformTask) val fullscreenTask = createFullscreenTask() @@ -3543,7 +3633,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() { + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskToDesk_underTaskLimit_multiDesksEnabled_dontMinimize() { + val deskId = 5 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + val freeformTask = + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId).also { + markTaskVisible(it) + } + + // Launch a fullscreen task while in the desk. + val fullscreenTask = createFullscreenTask() + val transition = Binder() + val wct = controller.handleRequest(transition, createTransition(fullscreenTask)) + + assertNotNull(wct) + verify(desksOrganizer, never()).minimizeTask(eq(wct), eq(deskId), any()) + assertNull(desktopTasksLimiter.getMinimizingTask(transition)) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskToDesk_bringsTasksOverLimit_multiDesksDisabled_otherTaskIsMinimized() { val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } freeformTasks.forEach { markTaskVisible(it) } val fullscreenTask = createFullscreenTask() @@ -3557,7 +3669,34 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_otherTaskIsMinimized() { + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskToDesk_bringsTasksOverLimit_multiDesksEnabled_otherTaskIsMinimized() { + val deskId = 5 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + val freeformTasks = + (1..MAX_TASK_LIMIT).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId).also { + markTaskVisible(it) + } + } + + // Launch a fullscreen task while in the desk. + setUpHomeTask() + val fullscreenTask = createFullscreenTask() + val transition = Binder() + val wct = controller.handleRequest(transition, createTransition(fullscreenTask)) + + assertNotNull(wct) + verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[0]) + val minimizingTask = + assertNotNull(desktopTasksLimiter.getMinimizingTask(transition)?.taskId) + assertThat(minimizingTask).isEqualTo(freeformTasks[0].taskId) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_multiDesksDisabled_otherTaskIsMinimized() { val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } freeformTasks.forEach { markTaskVisible(it) } val fullscreenTask = createFullscreenTask() @@ -3578,7 +3717,31 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_existingAndNewTasksAreMinimized() { + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_multiDesksEnabled_otherTaskIsMinimized() { + val deskId = 5 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + val freeformTasks = + (1..MAX_TASK_LIMIT).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } + freeformTasks.forEach { markTaskVisible(it) } + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct) + // The launching task is moved to the desk. + verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask) + // The bottom-most task is minimized. + verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[0]) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_multiDesksDisabled_existingAndNewTasksAreMinimized() { val minimizedTask = setUpFreeformTask() taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = minimizedTask.taskId) val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } @@ -3604,6 +3767,88 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_multiDesksEnabled_existingAndNewTasksAreMinimized() { + // A desk with a minimized tasks, and non-minimized tasks already at the task limit. + val deskId = 5 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + val minimizedTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + taskRepository.minimizeTaskInDesk( + displayId = DEFAULT_DISPLAY, + deskId = deskId, + taskId = minimizedTask.taskId, + ) + val freeformTasks = + (1..MAX_TASK_LIMIT).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId).also { + markTaskVisible(it) + } + } + + // Launch a fullscreen task that brings Home to front with it. + setUpHomeTask() + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct) + verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[0]) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskWithTaskOnHome_taskAddedToDesk() { + // A desk with a minimized tasks, and non-minimized tasks already at the task limit. + val deskId = 5 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + + // Launch a fullscreen task that brings Home to front with it. + setUpHomeTask() + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct) + verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + ) + fun handleRequest_fullscreenTaskWithTaskOnHome_activatesDesk() { + val deskId = 5 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + + // Launch a fullscreen task that brings Home to front with it. + val homeTask = setUpHomeTask() + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + val transition = Binder() + val wct = controller.handleRequest(transition, createTransition(fullscreenTask)) + + assertNotNull(wct) + wct.assertReorder(homeTask, toTop = true) + wct.assertReorder(wallpaperToken, toTop = true) + verify(desksOrganizer).activateDesk(wct, deskId) + verify(desksTransitionsObserver) + .addPendingTransition( + DeskTransition.ActiveDeskWithTask( + token = transition, + displayId = DEFAULT_DISPLAY, + deskId = deskId, + enterTaskId = fullscreenTask.taskId, + ) + ) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() { whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) @@ -3663,8 +3908,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() { - val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_multiDesksDisabled_minimize() { + val deskId = 0 + val freeformTasks = + (1..MAX_TASK_LIMIT).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } freeformTasks.forEach { markTaskVisible(it) } val newFreeformTask = createFreeformTask() @@ -3677,6 +3927,24 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_multiDesksEnabled_minimize() { + val deskId = 0 + val freeformTasks = + (1..MAX_TASK_LIMIT).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } + freeformTasks.forEach { markTaskVisible(it) } + val newFreeformTask = createFreeformTask() + + val wct = + controller.handleRequest(Binder(), createTransition(newFreeformTask, TRANSIT_OPEN)) + + assertNotNull(wct) + verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[0]) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun handleRequest_freeformTaskFromInactiveDesk_tracksDeskDeactivation() { val deskId = 0 val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) @@ -5578,7 +5846,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) - fun openInstance_fromFreeformAddsNewWindow() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun openInstance_fromFreeformAddsNewWindow_multiDesksDisabled() { setUpLandscapeDisplay() val task = setUpFreeformTask() val taskToRequest = setUpFreeformTask() @@ -5592,7 +5861,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() ) ) .thenReturn(Binder()) + runOpenInstance(task, taskToRequest.taskId) + verify(desktopMixedTransitionHandler) .startLaunchTransition(anyInt(), any(), anyInt(), anyOrNull(), anyOrNull()) val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT) @@ -5601,10 +5872,42 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun openInstance_fromFreeformAddsNewWindow_multiDesksEnabled() { + setUpLandscapeDisplay() + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0) + val taskToRequest = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0) + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_TO_FRONT), + any(), + eq(taskToRequest.taskId), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(Binder()) + + runOpenInstance(task, taskToRequest.taskId) + + verify(desktopMixedTransitionHandler) + .startLaunchTransition(anyInt(), any(), anyInt(), anyOrNull(), anyOrNull()) + verify(desksOrganizer).reorderTaskToFront(any(), eq(0), eq(taskToRequest)) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) - fun openInstance_fromFreeform_minimizesIfNeeded() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun openInstance_fromFreeform_multiDesksDisabled_minimizesIfNeeded() { setUpLandscapeDisplay() - val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() } + val deskId = 0 + val freeformTasks = + (1..MAX_TASK_LIMIT + 1).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } val oldestTask = freeformTasks.first() val newestTask = freeformTasks.last() @@ -5630,6 +5933,40 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun openInstance_fromFreeform_multiDesksEnabled_minimizesIfNeeded() { + setUpLandscapeDisplay() + val deskId = 0 + val freeformTasks = + (1..MAX_TASK_LIMIT + 1).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } + val oldestTask = freeformTasks.first() + val newestTask = freeformTasks.last() + + val transition = Binder() + val wctCaptor = argumentCaptor<WindowContainerTransaction>() + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + anyInt(), + wctCaptor.capture(), + anyInt(), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(transition) + + runOpenInstance(newestTask, freeformTasks[1].taskId) + + val wct = wctCaptor.firstValue + verify(desksOrganizer).minimizeTask(wct, deskId, oldestTask) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) fun openInstance_fromFreeform_exitsImmersiveIfNeeded() { setUpLandscapeDisplay() @@ -6587,7 +6924,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, ) fun startLaunchTransition_desktopNotShowing_movesWallpaperToFront() { - val launchingTask = createFreeformTask() + val launchingTask = createFreeformTask(displayId = DEFAULT_DISPLAY) val wct = WindowContainerTransaction() wct.reorder(launchingTask.token, /* onTop= */ true) whenever( @@ -6601,7 +6938,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() ) .thenReturn(Binder()) - controller.startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null) + controller.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + launchingTaskId = null, + deskId = 0, + displayId = DEFAULT_DISPLAY, + ) val latestWct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN) val launchingTaskReorderIndex = latestWct.indexOfReorder(launchingTask, toTop = true) @@ -6625,6 +6968,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() transitionType = TRANSIT_OPEN, wct = WindowContainerTransaction(), launchingTaskId = null, + deskId = 0, + displayId = DEFAULT_DISPLAY, ) verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(any()) @@ -6634,6 +6979,51 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun startLaunchTransition_launchingTaskFromInactiveDesk_otherDeskActive_activatesDesk() { + val activeDeskId = 4 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = activeDeskId) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = activeDeskId) + val inactiveDesk = 5 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = inactiveDesk) + val launchingTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = inactiveDesk) + val transition = Binder() + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_OPEN), + any(), + eq(launchingTask.taskId), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(transition) + + val wct = WindowContainerTransaction() + controller.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + launchingTaskId = launchingTask.taskId, + deskId = inactiveDesk, + displayId = DEFAULT_DISPLAY, + ) + + verify(desksOrganizer).activateDesk(any(), eq(inactiveDesk)) + verify(desksTransitionsObserver) + .addPendingTransition( + DeskTransition.ActivateDesk( + token = transition, + displayId = DEFAULT_DISPLAY, + deskId = inactiveDesk, + ) + ) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, ) fun startLaunchTransition_desktopShowing_doesNotReorderWallpaper() { val wct = WindowContainerTransaction() @@ -6648,8 +7038,14 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() ) .thenReturn(Binder()) - setUpFreeformTask() - controller.startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null) + setUpFreeformTask(deskId = 0, displayId = DEFAULT_DISPLAY) + controller.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + launchingTaskId = null, + deskId = 0, + displayId = DEFAULT_DISPLAY, + ) val latestWct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN) assertNull(latestWct.hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() }) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt index 76103640c029..eeecb00b5b08 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -39,12 +39,14 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION +import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.shared.desktopmode.DesktopModeStatus @@ -67,10 +69,13 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.any import org.mockito.Mockito.mock import org.mockito.Mockito.spy +import org.mockito.Mockito.verify import org.mockito.Mockito.`when` +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.never import org.mockito.quality.Strictness /** @@ -84,6 +89,7 @@ import org.mockito.quality.Strictness class DesktopTasksLimiterTest : ShellTestCase() { @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer + @Mock lateinit var desksOrganizer: DesksOrganizer @Mock lateinit var transitions: Transitions @Mock lateinit var interactionJankMonitor: InteractionJankMonitor @Mock lateinit var handler: Handler @@ -128,6 +134,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { transitions, userRepositories, shellTaskOrganizer, + desksOrganizer, MAX_TASK_LIMIT, interactionJankMonitor, mContext, @@ -148,6 +155,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { transitions, userRepositories, shellTaskOrganizer, + desksOrganizer, 0, interactionJankMonitor, mContext, @@ -163,6 +171,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { transitions, userRepositories, shellTaskOrganizer, + desksOrganizer, -5, interactionJankMonitor, mContext, @@ -178,6 +187,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { transitions, userRepositories, shellTaskOrganizer, + desksOrganizer, maxTasksLimit = null, interactionJankMonitor, mContext, @@ -394,7 +404,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test - fun addAndGetMinimizeTaskChanges_tasksWithinLimit_noTaskMinimized() { + @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addAndGetMinimizeTaskChanges_tasksWithinLimit_multiDesksDisabled_noTaskMinimized() { desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } @@ -402,7 +413,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { val wct = WindowContainerTransaction() val minimizedTaskId = desktopTasksLimiter.addAndGetMinimizeTaskChanges( - displayId = DEFAULT_DISPLAY, + deskId = 0, wct = wct, newFrontTaskId = setUpFreeformTask().taskId, ) @@ -412,7 +423,27 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test - fun addAndGetMinimizeTaskChanges_tasksAboveLimit_backTaskMinimized() { + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addAndGetMinimizeTaskChanges_tasksWithinLimit_multiDesksEnabled_noTaskMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } + + val wct = WindowContainerTransaction() + val minimizedTaskId = + desktopTasksLimiter.addAndGetMinimizeTaskChanges( + deskId = 0, + wct = wct, + newFrontTaskId = setUpFreeformTask().taskId, + ) + + assertThat(minimizedTaskId).isNull() + verify(desksOrganizer, never()).minimizeTask(eq(wct), eq(0), any()) + } + + @Test + @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addAndGetMinimizeTaskChanges_tasksAboveLimit_multiDesksDisabled_backTaskMinimized() { desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) // The following list will be ordered bottom -> top, as the last task is moved to top last. @@ -421,7 +452,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { val wct = WindowContainerTransaction() val minimizedTaskId = desktopTasksLimiter.addAndGetMinimizeTaskChanges( - displayId = DEFAULT_DISPLAY, + deskId = DEFAULT_DISPLAY, wct = wct, newFrontTaskId = setUpFreeformTask().taskId, ) @@ -433,7 +464,28 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test - fun addAndGetMinimizeTaskChanges_nonMinimizedTasksWithinLimit_noTaskMinimized() { + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addAndGetMinimizeTaskChanges_tasksAboveLimit_multiDesksEnabled_backTaskMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + // The following list will be ordered bottom -> top, as the last task is moved to top last. + val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } + + val wct = WindowContainerTransaction() + val minimizedTaskId = + desktopTasksLimiter.addAndGetMinimizeTaskChanges( + deskId = DEFAULT_DISPLAY, + wct = wct, + newFrontTaskId = setUpFreeformTask().taskId, + ) + + assertThat(minimizedTaskId).isEqualTo(tasks.first().taskId) + verify(desksOrganizer).minimizeTask(wct, deskId = 0, tasks.first()) + } + + @Test + @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addAndGetMinimizeTaskChanges_nonMinimizedTasksWithinLimit_multiDesksDisabled_noTaskMinimized() { desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } @@ -442,7 +494,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { val wct = WindowContainerTransaction() val minimizedTaskId = desktopTasksLimiter.addAndGetMinimizeTaskChanges( - displayId = 0, + deskId = 0, wct = wct, newFrontTaskId = setUpFreeformTask().taskId, ) @@ -452,6 +504,26 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addAndGetMinimizeTaskChanges_nonMinimizedTasksWithinLimit_multiDesksEnabled_noTaskMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } + desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = tasks[0].taskId) + + val wct = WindowContainerTransaction() + val minimizedTaskId = + desktopTasksLimiter.addAndGetMinimizeTaskChanges( + deskId = 0, + wct = wct, + newFrontTaskId = setUpFreeformTask().taskId, + ) + + assertThat(minimizedTaskId).isNull() + verify(desksOrganizer, never()).minimizeTask(eq(wct), eq(0), any()) + } + + @Test fun getTaskToMinimize_tasksWithinLimit_returnsNull() { desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) @@ -485,6 +557,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { transitions, userRepositories, shellTaskOrganizer, + desksOrganizer, MAX_TASK_LIMIT2, interactionJankMonitor, mContext, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt index fd8842b6d99b..a7dc706eb6c9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -48,13 +48,11 @@ import com.android.wm.shell.back.BackAnimationController import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider -import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP -import com.android.wm.shell.util.StubTransaction import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import org.junit.Before @@ -95,7 +93,6 @@ class DesktopTasksTransitionObserverTest { private val backAnimationController = mock<BackAnimationController>() private val desktopWallpaperActivityTokenProvider = mock<DesktopWallpaperActivityTokenProvider>() - private val desksTransitionObserver = mock<DesksTransitionObserver>() private val wallpaperToken = MockToken().token() private lateinit var transitionObserver: DesktopTasksTransitionObserver @@ -119,7 +116,6 @@ class DesktopTasksTransitionObserverTest { mixedHandler, backAnimationController, desktopWallpaperActivityTokenProvider, - desksTransitionObserver, shellInit, ) } @@ -403,21 +399,6 @@ class DesktopTasksTransitionObserverTest { verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false) } - @Test - fun onTransitionReady_forwardsToDesksTransitionObserver() { - val transition = Binder() - val info = TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0) - - transitionObserver.onTransitionReady( - transition = transition, - info = info, - StubTransaction(), - StubTransaction(), - ) - - verify(desksTransitionObserver).onTransitionReady(transition, info) - } - private fun createBackNavigationTransition( task: RunningTaskInfo?, type: Int = TRANSIT_TO_BACK, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt index c00083b0607f..c40a04c47b9b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt @@ -41,6 +41,7 @@ object DesktopTestHelpers { .setActivityType(ACTIVITY_TYPE_STANDARD) .setWindowingMode(WINDOWING_MODE_FREEFORM) .setLastActiveTime(100) + .setUserId(DEFAULT_USER_ID) .apply { bounds?.let { setBounds(it) } } .build() @@ -50,6 +51,7 @@ object DesktopTestHelpers { .setToken(MockToken().token()) .setActivityType(ACTIVITY_TYPE_STANDARD) .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setUserId(DEFAULT_USER_ID) .setLastActiveTime(100) /** Create a task that has windowing mode set to [WINDOWING_MODE_FULLSCREEN] */ @@ -63,6 +65,7 @@ object DesktopTestHelpers { .setToken(MockToken().token()) .setActivityType(ACTIVITY_TYPE_STANDARD) .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) + .setUserId(DEFAULT_USER_ID) .setLastActiveTime(100) .build() @@ -72,6 +75,7 @@ object DesktopTestHelpers { .setToken(MockToken().token()) .setActivityType(ACTIVITY_TYPE_HOME) .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setUserId(DEFAULT_USER_ID) .setLastActiveTime(100) .build() @@ -91,4 +95,6 @@ object DesktopTestHelpers { createSystemModalTask().apply { baseActivity = ComponentName("com.test.dummypackage", "TestClass") } + + const val DEFAULT_USER_ID = 10 } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt index 96b85ad2729e..6b2f90fc0a59 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt @@ -21,14 +21,17 @@ import android.view.Display import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo +import android.window.WindowContainerToken import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.Change import android.window.WindowContainerTransaction.HierarchyOp +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.common.LaunchAdjacentController import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskMinimizationRoot import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskRoot @@ -58,13 +61,19 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { private val testShellInit = ShellInit(testExecutor) private val mockShellCommandHandler = mock<ShellCommandHandler>() private val mockShellTaskOrganizer = mock<ShellTaskOrganizer>() + private val launchAdjacentController = LaunchAdjacentController(mock()) private lateinit var organizer: RootTaskDesksOrganizer @Before fun setUp() { organizer = - RootTaskDesksOrganizer(testShellInit, mockShellCommandHandler, mockShellTaskOrganizer) + RootTaskDesksOrganizer( + testShellInit, + mockShellCommandHandler, + mockShellTaskOrganizer, + launchAdjacentController, + ) } @Test @@ -473,14 +482,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { organizer.minimizeTask(wct, deskId = desk.deskRoot.deskId, task) - assertThat( - wct.hierarchyOps.any { hop -> - hop.isReparent && - hop.container == task.token.asBinder() && - hop.newParent == desk.minimizationRoot.token.asBinder() - } - ) - .isTrue() + assertThat(wct.hasMinimizationHops(desk, task.token)).isTrue() } @Test @@ -508,14 +510,205 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { assertThat(wct.isEmpty).isTrue() } + @Test + fun unminimizeTask() { + val desk = createDesk() + val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } + val wct = WindowContainerTransaction() + organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task) + organizer.onTaskAppeared(task, SurfaceControl()) + organizer.minimizeTask(wct, deskId = desk.deskRoot.deskId, task) + task.parentTaskId = desk.minimizationRoot.rootId + organizer.onTaskInfoChanged(task) + + wct.clear() + organizer.unminimizeTask(wct, deskId = desk.deskRoot.deskId, task) + + assertThat(wct.hasUnminimizationHops(desk, task.token)).isTrue() + } + + @Test + fun unminimizeTask_alreadyUnminimized_noOp() { + val desk = createDesk() + val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } + val wct = WindowContainerTransaction() + organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task) + organizer.onTaskAppeared(task, SurfaceControl()) + + wct.clear() + organizer.unminimizeTask(wct, deskId = desk.deskRoot.deskId, task) + + assertThat(wct.hasUnminimizationHops(desk, task.token)).isFalse() + } + + @Test + fun unminimizeTask_notInDesk_noOp() { + val desk = createDesk() + val task = createFreeformTask() + val wct = WindowContainerTransaction() + + organizer.unminimizeTask(wct, deskId = desk.deskRoot.deskId, task) + + assertThat(wct.hasUnminimizationHops(desk, task.token)).isFalse() + } + + @Test + fun reorderTaskToFront() { + val desk = createDesk() + val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } + val wct = WindowContainerTransaction() + organizer.onTaskAppeared(task, SurfaceControl()) + + organizer.reorderTaskToFront(wct, desk.deskRoot.deskId, task) + + assertThat( + wct.hierarchyOps.singleOrNull { hop -> + hop.container == task.token.asBinder() && + hop.type == HIERARCHY_OP_TYPE_REORDER && + hop.toTop && + hop.includingParents() + } + ) + .isNotNull() + } + + @Test + fun reorderTaskToFront_notInDesk_noOp() { + val desk = createDesk() + val task = createFreeformTask() + val wct = WindowContainerTransaction() + + organizer.reorderTaskToFront(wct, desk.deskRoot.deskId, task) + + assertThat( + wct.hierarchyOps.singleOrNull { hop -> + hop.container == task.token.asBinder() && + hop.type == HIERARCHY_OP_TYPE_REORDER && + hop.toTop && + hop.includingParents() + } + ) + .isNull() + } + + @Test + fun reorderTaskToFront_minimized_unminimizesAndReorders() { + val desk = createDesk() + val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } + val wct = WindowContainerTransaction() + organizer.onTaskAppeared(task, SurfaceControl()) + task.parentTaskId = desk.minimizationRoot.rootId + organizer.onTaskInfoChanged(task) + + organizer.reorderTaskToFront(wct, desk.deskRoot.deskId, task) + + assertThat(wct.hasUnminimizationHops(desk, task.token)).isTrue() + assertThat( + wct.hierarchyOps.singleOrNull { hop -> + hop.container == task.token.asBinder() && + hop.type == HIERARCHY_OP_TYPE_REORDER && + hop.toTop && + hop.includingParents() + } + ) + .isNotNull() + } + + @Test + fun onTaskAppeared_visibleDesk_onlyDesk_disablesLaunchAdjacent() { + launchAdjacentController.launchAdjacentEnabled = true + + createDesk(visible = true) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() + } + + @Test + fun onTaskAppeared_invisibleDesk_onlyDesk_enablesLaunchAdjacent() { + launchAdjacentController.launchAdjacentEnabled = false + + createDesk(visible = false) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isTrue() + } + + @Test + fun onTaskAppeared_invisibleDesk_otherVisibleDesk_disablesLaunchAdjacent() { + launchAdjacentController.launchAdjacentEnabled = true + + createDesk(visible = true) + createDesk(visible = false) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() + } + + @Test + fun onTaskInfoChanged_deskBecomesVisible_onlyDesk_disablesLaunchAdjacent() { + launchAdjacentController.launchAdjacentEnabled = true + + val desk = createDesk(visible = false) + desk.deskRoot.taskInfo.isVisible = true + organizer.onTaskInfoChanged(desk.deskRoot.taskInfo) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() + } + + @Test + fun onTaskInfoChanged_deskBecomesInvisible_onlyDesk_enablesLaunchAdjacent() { + launchAdjacentController.launchAdjacentEnabled = false + + val desk = createDesk(visible = true) + desk.deskRoot.taskInfo.isVisible = false + organizer.onTaskInfoChanged(desk.deskRoot.taskInfo) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isTrue() + } + + @Test + fun onTaskInfoChanged_deskBecomesInvisible_otherVisibleDesk_disablesLaunchAdjacent() { + launchAdjacentController.launchAdjacentEnabled = true + + createDesk(visible = true) + val desk = createDesk(visible = true) + desk.deskRoot.taskInfo.isVisible = false + organizer.onTaskInfoChanged(desk.deskRoot.taskInfo) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() + } + + @Test + fun onTaskVanished_visibleDeskDisappears_onlyDesk_enablesLaunchAdjacent() { + launchAdjacentController.launchAdjacentEnabled = false + + val desk = createDesk(visible = true) + organizer.onTaskVanished(desk.deskRoot.taskInfo) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isTrue() + } + + @Test + fun onTaskVanished_visibleDeskDisappears_otherDeskVisible_disablesLaunchAdjacent() { + launchAdjacentController.launchAdjacentEnabled = true + + createDesk(visible = true) + val desk = createDesk(visible = true) + organizer.onTaskVanished(desk.deskRoot.taskInfo) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() + } + private data class DeskRoots( val deskRoot: DeskRoot, val minimizationRoot: DeskMinimizationRoot, ) - private fun createDesk(): DeskRoots { + private fun createDesk(visible: Boolean = true): DeskRoots { organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + val freeformRoot = + createFreeformTask().apply { + parentTaskId = -1 + isVisible = visible + } organizer.onTaskAppeared(freeformRoot, SurfaceControl()) val minimizationRoot = createFreeformTask().apply { parentTaskId = -1 } organizer.onTaskAppeared(minimizationRoot, SurfaceControl()) @@ -525,6 +718,27 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { ) } + private fun WindowContainerTransaction.hasMinimizationHops( + desk: DeskRoots, + task: WindowContainerToken, + ): Boolean = + hierarchyOps.any { hop -> + hop.isReparent && + hop.container == task.asBinder() && + hop.newParent == desk.minimizationRoot.token.asBinder() + } + + private fun WindowContainerTransaction.hasUnminimizationHops( + desk: DeskRoots, + task: WindowContainerToken, + ): Boolean = + hierarchyOps.any { hop -> + hop.isReparent && + hop.container == task.asBinder() && + hop.newParent == desk.deskRoot.token.asBinder() && + hop.toTop + } + private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback { var deskId: Int? = null val created: Boolean diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java index 9509aaf20c9b..52c5ad1f8e49 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java @@ -41,6 +41,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; @@ -198,6 +199,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) public void visibilityTaskChanged_visible_setLaunchAdjacentDisabled() { ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); @@ -209,6 +211,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) public void visibilityTaskChanged_notVisible_setLaunchAdjacentEnabled() { ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java index bc918450a3cf..714e5f486285 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java @@ -44,10 +44,12 @@ import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.desktopmode.DesktopImmersiveController; +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.TransitionInfoBuilder; import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.util.StubTransaction; import com.android.wm.shell.windowdecor.WindowDecorViewModel; import org.junit.Before; @@ -68,6 +70,7 @@ public class FreeformTaskTransitionObserverTest extends ShellTestCase { @Mock private WindowDecorViewModel mWindowDecorViewModel; @Mock private TaskChangeListener mTaskChangeListener; @Mock private FocusTransitionObserver mFocusTransitionObserver; + @Mock private DesksTransitionObserver mDesksTransitionObserver; private FreeformTaskTransitionObserver mTransitionObserver; @@ -88,7 +91,8 @@ public class FreeformTaskTransitionObserverTest extends ShellTestCase { Optional.of(mDesktopImmersiveController), mWindowDecorViewModel, Optional.of(mTaskChangeListener), - mFocusTransitionObserver); + mFocusTransitionObserver, + Optional.of(mDesksTransitionObserver)); final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass(Runnable.class); verify(mShellInit).addInitCallback(initRunnableCaptor.capture(), same(mTransitionObserver)); @@ -357,6 +361,18 @@ public class FreeformTaskTransitionObserverTest extends ShellTestCase { verify(mDesktopImmersiveController).onTransitionFinished(transition, /* aborted= */ false); } + @Test + public void onTransitionReady_forwardsToDesksTransitionObserver() { + final IBinder transition = mock(IBinder.class); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, /* flags= */ 0) + .build(); + + mTransitionObserver.onTransitionReady(transition, info, new StubTransaction(), + new StubTransaction()); + + verify(mDesksTransitionObserver).onTransitionReady(transition, info); + } + private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) { final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.taskId = taskId; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java index 333569a7206e..5029371c3419 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java @@ -24,6 +24,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.mockito.kotlin.MatchersKt.eq; import static org.mockito.kotlin.VerificationKt.clearInvocations; @@ -35,7 +36,9 @@ import android.app.ActivityManager; import android.app.PendingIntent; import android.app.PictureInPictureParams; import android.app.RemoteAction; +import android.content.ComponentName; import android.content.Context; +import android.content.res.Resources; import android.graphics.Rect; import android.graphics.drawable.Icon; import android.os.Bundle; @@ -48,8 +51,10 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PhoneSizeSpecSource; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.pip2.animation.PipResizeAnimator; import org.junit.Before; @@ -107,6 +112,16 @@ public class PipTaskListenerTest { } @Test + public void constructor_addOnPipComponentChangedListener() { + mPipTaskListener = new PipTaskListener(mMockContext, mMockShellTaskOrganizer, + mMockPipTransitionState, mMockPipScheduler, mMockPipBoundsState, + mMockPipBoundsAlgorithm, mMockShellExecutor); + + verify(mMockPipBoundsState).addOnPipComponentChangedListener( + any(PipBoundsState.OnPipComponentChangedListener.class)); + } + + @Test public void setPictureInPictureParams_updatePictureInPictureParams() { mPipTaskListener = new PipTaskListener(mMockContext, mMockShellTaskOrganizer, mMockPipTransitionState, mMockPipScheduler, mMockPipBoundsState, @@ -359,6 +374,26 @@ public class PipTaskListenerTest { verify(mMockPipResizeAnimator, times(0)).start(); } + @Test + public void onPipComponentChanged_clearPictureInPictureParams() { + when(mMockContext.getResources()).thenReturn(mock(Resources.class)); + PipBoundsState pipBoundsState = new PipBoundsState(mMockContext, + mock(PhoneSizeSpecSource.class), mock(PipDisplayLayoutState.class)); + pipBoundsState.setLastPipComponentName(new ComponentName("org.test", "test1")); + + mPipTaskListener = new PipTaskListener(mMockContext, mMockShellTaskOrganizer, + mMockPipTransitionState, mMockPipScheduler, pipBoundsState, + mMockPipBoundsAlgorithm, mMockShellExecutor); + Rational aspectRatio = new Rational(4, 3); + String action1 = "action1"; + mPipTaskListener.setPictureInPictureParams(getPictureInPictureParams( + aspectRatio, action1)); + + pipBoundsState.setLastPipComponentName(new ComponentName("org.test", "test2")); + + assertTrue(mPipTaskListener.getPictureInPictureParams().empty()); + } + private PictureInPictureParams getPictureInPictureParams(Rational aspectRatio, String... actions) { final PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt index fd22a84dee5d..2b39262d9f00 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt @@ -53,7 +53,8 @@ class DragZoneFactoryTest { tabletPortrait.copy(windowBounds = Rect(0, 0, 800, 900), isSmallTablet = true) private val foldableLandscape = foldablePortrait.copy(windowBounds = Rect(0, 0, 900, 800), isLandscape = true) - private val splitScreenModeChecker = SplitScreenModeChecker { SplitScreenMode.NONE } + private var splitScreenMode = SplitScreenMode.NONE + private val splitScreenModeChecker = SplitScreenModeChecker { splitScreenMode } private var isDesktopWindowModeSupported = true private val desktopWindowModeChecker = DesktopWindowModeChecker { isDesktopWindowModeSupported } @@ -283,7 +284,7 @@ class DragZoneFactoryTest { } @Test - fun dragZonesForBubble_tablet_desktopModeDisabled() { + fun dragZonesForBubble_desktopModeDisabled() { isDesktopWindowModeSupported = false dragZoneFactory = DragZoneFactory( @@ -298,7 +299,7 @@ class DragZoneFactoryTest { } @Test - fun dragZonesForExpandedView_tablet_desktopModeDisabled() { + fun dragZonesForExpandedView_desktopModeDisabled() { isDesktopWindowModeSupported = false dragZoneFactory = DragZoneFactory( @@ -314,6 +315,38 @@ class DragZoneFactoryTest { assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty() } + @Test + fun dragZonesForBubble_splitScreenModeUnsupported() { + splitScreenMode = SplitScreenMode.UNSUPPORTED + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) + assertThat(dragZones.filterIsInstance<DragZone.Split>()).isEmpty() + } + + @Test + fun dragZonesForExpandedView_splitScreenModeUnsupported() { + splitScreenMode = SplitScreenMode.UNSUPPORTED + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) + val dragZones = + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) + assertThat(dragZones.filterIsInstance<DragZone.Split>()).isEmpty() + } + private inline fun <reified T> verifyInstance(): DragZoneVerifier = { dragZone -> assertThat(dragZone).isInstanceOf(T::class.java) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt index edf91fe62e7d..fb62ba75e056 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt @@ -234,23 +234,12 @@ class DesktopModeStatusTest : ShellTestCase() { assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() } - @DisableFlags(Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE) @Test fun isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktopOff_returnsFalse() { doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) - doReturn(false).whenever(mockResources) - .getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) - - assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() - } - - @EnableFlags(Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE) - @Test - fun isPDDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktopOff_returnsTrue() { - doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) doReturn(false).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) - assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue() + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index d69509faf4ec..e89a122595d5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -115,8 +115,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest .spyStatic(DragPositioningCallbackUtility::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupportedOnDisplay(Mockito.any(), - Mockito.any()) } + doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(Mockito.any()) } doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(Mockito.any()) } doReturn(false).`when` { DesktopModeStatus.overridesShowAppHandle(Mockito.any()) } @@ -395,7 +394,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN) - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupportedOnDisplay(any(), any()) } + doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(any()) } setUpMockDecorationsForTasks(task) onTaskOpening(task) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt index a1f40fdefee9..81dfaed56b6f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt @@ -79,7 +79,6 @@ import com.android.wm.shell.transition.Transitions import com.android.wm.shell.util.StubTransaction import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener -import com.android.wm.shell.windowdecor.common.AppHandleAndHeaderVisibilityHelper import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier @@ -175,7 +174,6 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { internal lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener protected lateinit var desktopModeWindowDecorViewModel: DesktopModeWindowDecorViewModel protected lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy - protected lateinit var appHandleAndHeaderVisibilityHelper: AppHandleAndHeaderVisibilityHelper fun setUpCommon() { spyContext = spy(mContext) @@ -187,13 +185,9 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { whenever(mockDesktopUserRepositories.current).thenReturn(mockDesktopRepository) whenever(mockDisplayController.getDisplayContext(any())).thenReturn(spyContext) whenever(mockDisplayController.getDisplay(any())).thenReturn(display) - whenever(display.type).thenReturn(Display.TYPE_INTERNAL) whenever(mockDesktopUserRepositories.getProfile(anyInt())) .thenReturn(mockDesktopRepository) desktopModeCompatPolicy = DesktopModeCompatPolicy(spyContext) - appHandleAndHeaderVisibilityHelper = - AppHandleAndHeaderVisibilityHelper(spyContext, mockDisplayController, - desktopModeCompatPolicy) desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel( spyContext, testShellExecutor, @@ -228,7 +222,6 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { Optional.of(mockTasksLimiter), mockAppHandleEducationController, mockAppToWebEducationController, - appHandleAndHeaderVisibilityHelper, mockCaptionHandleRepository, Optional.of(mockActivityOrientationChangeHandler), mockTaskPositionerFactory, diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index 30594dcfa939..0d45149267cf 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -1265,6 +1265,9 @@ struct ResTable_config // Varies in length from 3 to 8 chars. Zero-filled value. char localeNumberingSystem[8]; + // Mark all padding explicitly so it's clear how much we can expand it. + char endPadding[3]; + void copyFromDeviceNoSwap(const ResTable_config& o) { const auto o_size = dtohl(o.size); if (o_size >= sizeof(ResTable_config)) [[likely]] { @@ -1422,6 +1425,13 @@ struct ResTable_config void swapHtoD_slow(); }; +// Fix the struct size for backward compatibility +static_assert(sizeof(ResTable_config) == 64); + +// Make sure there's no unaccounted padding in the structure. +static_assert(offsetof(ResTable_config, endPadding) + + sizeof(ResTable_config::endPadding) == sizeof(ResTable_config)); + /** * A specification of the resources defined by a particular type. * diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp index a210ddf54b2e..7d227f793817 100644 --- a/libs/hwui/jni/Graphics.cpp +++ b/libs/hwui/jni/Graphics.cpp @@ -722,10 +722,15 @@ void RecyclingClippingPixelAllocator::copyIfNecessary() { auto canvas = SkCanvas(recycledPixels->getSkBitmap()); SkRect destination = SkRect::Make(recycledPixels->info().bounds()); - destination.intersect(SkRect::Make(mSkiaBitmap->info().bounds())); - canvas.drawImageRect(mSkiaBitmap->asImage(), *mDesiredSubset, destination, - SkSamplingOptions(SkFilterMode::kLinear), nullptr, - SkCanvas::kFast_SrcRectConstraint); + if (destination.intersect(SkRect::Make(mSkiaBitmap->info().bounds()))) { + canvas.drawImageRect(mSkiaBitmap->asImage(), *mDesiredSubset, destination, + SkSamplingOptions(SkFilterMode::kLinear), nullptr, + SkCanvas::kFast_SrcRectConstraint); + } else { + // The canvas would have discarded the draw operation automatically, but + // this case should have been detected before getting to this point. + ALOGE("Copy destination does not intersect image bounds"); + } } else { void* dst = recycledPixels->pixels(); const size_t dstRowBytes = mRecycledBitmap->rowBytes(); diff --git a/media/java/Android.bp b/media/java/Android.bp index 6878f9d61f6d..28b9d3bbc167 100644 --- a/media/java/Android.bp +++ b/media/java/Android.bp @@ -15,6 +15,7 @@ filegroup { ], exclude_srcs: [ ":framework-media-tv-tunerresourcemanager-sources-aidl", + ":framework-media-quality-sources-aidl", ], visibility: [ "//frameworks/base", diff --git a/media/java/android/media/quality/Android.bp b/media/java/android/media/quality/Android.bp new file mode 100644 index 000000000000..080d5266ccb7 --- /dev/null +++ b/media/java/android/media/quality/Android.bp @@ -0,0 +1,39 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +filegroup { + name: "framework-media-quality-sources-aidl", + srcs: [ + "aidl/android/media/quality/*.aidl", + ], + path: "aidl", +} + +aidl_interface { + name: "media_quality_aidl_interface", + unstable: true, + local_include_dir: "aidl", + backend: { + java: { + enabled: true, + }, + cpp: { + enabled: false, + }, + ndk: { + enabled: false, + }, + rust: { + enabled: false, + }, + }, + srcs: [ + ":framework-media-quality-sources-aidl", + ], +} diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java index 0d6d32a22dae..bfd01380a2ee 100644 --- a/media/java/android/media/quality/MediaQualityManager.java +++ b/media/java/android/media/quality/MediaQualityManager.java @@ -274,9 +274,9 @@ public final class MediaQualityManager { @NonNull String name, @Nullable ProfileQueryParams options) { try { - Bundle optionsBundle = options == null - ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle(); - return mService.getPictureProfile(type, name, optionsBundle, mUserHandle); + boolean includeParams = options == null || options.mParametersIncluded; + return mService.getPictureProfile( + type, name, includeParams, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -299,10 +299,9 @@ public final class MediaQualityManager { public List<PictureProfile> getPictureProfilesByPackage( @NonNull String packageName, @Nullable ProfileQueryParams options) { try { - Bundle optionsBundle = options == null - ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle(); + boolean includeParams = options == null || options.mParametersIncluded; return mService.getPictureProfilesByPackage( - packageName, optionsBundle, mUserHandle); + packageName, includeParams, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -321,9 +320,8 @@ public final class MediaQualityManager { @NonNull public List<PictureProfile> getAvailablePictureProfiles(@Nullable ProfileQueryParams options) { try { - Bundle optionsBundle = options == null - ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle(); - return mService.getAvailablePictureProfiles(optionsBundle, mUserHandle); + boolean includeParams = options == null || options.mParametersIncluded; + return mService.getAvailablePictureProfiles(includeParams, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -344,7 +342,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) public boolean setDefaultPictureProfile(@Nullable String pictureProfileId) { try { - return mService.setDefaultPictureProfile(pictureProfileId, mUserHandle); + return mService.setDefaultPictureProfile(pictureProfileId, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -361,7 +359,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) public List<String> getPictureProfilePackageNames() { try { - return mService.getPictureProfilePackageNames(mUserHandle); + return mService.getPictureProfilePackageNames(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -373,7 +371,7 @@ public final class MediaQualityManager { */ public List<PictureProfileHandle> getPictureProfileHandle(String[] id) { try { - return mService.getPictureProfileHandle(id, mUserHandle); + return mService.getPictureProfileHandle(id, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -385,7 +383,7 @@ public final class MediaQualityManager { */ public List<SoundProfileHandle> getSoundProfileHandle(String[] id) { try { - return mService.getSoundProfileHandle(id, mUserHandle); + return mService.getSoundProfileHandle(id, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -401,7 +399,7 @@ public final class MediaQualityManager { */ public void createPictureProfile(@NonNull PictureProfile pp) { try { - mService.createPictureProfile(pp, mUserHandle); + mService.createPictureProfile(pp, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -416,7 +414,7 @@ public final class MediaQualityManager { */ public void updatePictureProfile(@NonNull String profileId, @NonNull PictureProfile pp) { try { - mService.updatePictureProfile(profileId, pp, mUserHandle); + mService.updatePictureProfile(profileId, pp, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -430,7 +428,7 @@ public final class MediaQualityManager { */ public void removePictureProfile(@NonNull String profileId) { try { - mService.removePictureProfile(profileId, mUserHandle); + mService.removePictureProfile(profileId, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -484,9 +482,8 @@ public final class MediaQualityManager { @NonNull String name, @Nullable ProfileQueryParams options) { try { - Bundle optionsBundle = options == null - ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle(); - return mService.getSoundProfile(type, name, optionsBundle, mUserHandle); + boolean includeParams = options == null || options.mParametersIncluded; + return mService.getSoundProfile(type, name, includeParams, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -510,9 +507,9 @@ public final class MediaQualityManager { public List<SoundProfile> getSoundProfilesByPackage( @NonNull String packageName, @Nullable ProfileQueryParams options) { try { - Bundle optionsBundle = options == null - ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle(); - return mService.getSoundProfilesByPackage(packageName, optionsBundle, mUserHandle); + boolean includeParams = options == null || options.mParametersIncluded; + return mService.getSoundProfilesByPackage( + packageName, includeParams, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -531,9 +528,8 @@ public final class MediaQualityManager { @NonNull public List<SoundProfile> getAvailableSoundProfiles(@Nullable ProfileQueryParams options) { try { - Bundle optionsBundle = options == null - ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle(); - return mService.getAvailableSoundProfiles(optionsBundle, mUserHandle); + boolean includeParams = options == null || options.mParametersIncluded; + return mService.getAvailableSoundProfiles(includeParams, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -554,7 +550,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE) public boolean setDefaultSoundProfile(@Nullable String soundProfileId) { try { - return mService.setDefaultSoundProfile(soundProfileId, mUserHandle); + return mService.setDefaultSoundProfile(soundProfileId, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -572,7 +568,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE) public List<String> getSoundProfilePackageNames() { try { - return mService.getSoundProfilePackageNames(mUserHandle); + return mService.getSoundProfilePackageNames(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -589,7 +585,7 @@ public final class MediaQualityManager { */ public void createSoundProfile(@NonNull SoundProfile sp) { try { - mService.createSoundProfile(sp, mUserHandle); + mService.createSoundProfile(sp, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -604,7 +600,7 @@ public final class MediaQualityManager { */ public void updateSoundProfile(@NonNull String profileId, @NonNull SoundProfile sp) { try { - mService.updateSoundProfile(profileId, sp, mUserHandle); + mService.updateSoundProfile(profileId, sp, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -618,7 +614,7 @@ public final class MediaQualityManager { */ public void removeSoundProfile(@NonNull String profileId) { try { - mService.removeSoundProfile(profileId, mUserHandle); + mService.removeSoundProfile(profileId, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -636,7 +632,7 @@ public final class MediaQualityManager { @NonNull public List<ParameterCapability> getParameterCapabilities(@NonNull List<String> names) { try { - return mService.getParameterCapabilities(names, mUserHandle); + return mService.getParameterCapabilities(names, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -654,7 +650,7 @@ public final class MediaQualityManager { @NonNull public List<String> getPictureProfileAllowList() { try { - return mService.getPictureProfileAllowList(mUserHandle); + return mService.getPictureProfileAllowList(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -668,7 +664,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) public void setPictureProfileAllowList(@NonNull List<String> packageNames) { try { - mService.setPictureProfileAllowList(packageNames, mUserHandle); + mService.setPictureProfileAllowList(packageNames, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -686,7 +682,7 @@ public final class MediaQualityManager { @NonNull public List<String> getSoundProfileAllowList() { try { - return mService.getSoundProfileAllowList(mUserHandle); + return mService.getSoundProfileAllowList(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -700,7 +696,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE) public void setSoundProfileAllowList(@NonNull List<String> packageNames) { try { - mService.setSoundProfileAllowList(packageNames, mUserHandle); + mService.setSoundProfileAllowList(packageNames, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -712,7 +708,7 @@ public final class MediaQualityManager { */ public boolean isSupported() { try { - return mService.isSupported(mUserHandle); + return mService.isSupported(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -730,7 +726,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) public void setAutoPictureQualityEnabled(boolean enabled) { try { - mService.setAutoPictureQualityEnabled(enabled, mUserHandle); + mService.setAutoPictureQualityEnabled(enabled, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -741,7 +737,7 @@ public final class MediaQualityManager { */ public boolean isAutoPictureQualityEnabled() { try { - return mService.isAutoPictureQualityEnabled(mUserHandle); + return mService.isAutoPictureQualityEnabled(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -758,7 +754,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) public void setSuperResolutionEnabled(boolean enabled) { try { - mService.setSuperResolutionEnabled(enabled, mUserHandle); + mService.setSuperResolutionEnabled(enabled, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -769,7 +765,7 @@ public final class MediaQualityManager { */ public boolean isSuperResolutionEnabled() { try { - return mService.isSuperResolutionEnabled(mUserHandle); + return mService.isSuperResolutionEnabled(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -787,7 +783,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE) public void setAutoSoundQualityEnabled(boolean enabled) { try { - mService.setAutoSoundQualityEnabled(enabled, mUserHandle); + mService.setAutoSoundQualityEnabled(enabled, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -798,7 +794,7 @@ public final class MediaQualityManager { */ public boolean isAutoSoundQualityEnabled() { try { - return mService.isAutoSoundQualityEnabled(mUserHandle); + return mService.isAutoSoundQualityEnabled(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -847,7 +843,7 @@ public final class MediaQualityManager { @NonNull AmbientBacklightSettings settings) { Preconditions.checkNotNull(settings); try { - mService.setAmbientBacklightSettings(settings, mUserHandle); + mService.setAmbientBacklightSettings(settings, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -858,7 +854,7 @@ public final class MediaQualityManager { */ public boolean isAmbientBacklightEnabled() { try { - return mService.isAmbientBacklightEnabled(mUserHandle); + return mService.isAmbientBacklightEnabled(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -872,7 +868,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.READ_COLOR_ZONES) public void setAmbientBacklightEnabled(boolean enabled) { try { - mService.setAmbientBacklightEnabled(enabled, mUserHandle); + mService.setAmbientBacklightEnabled(enabled, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/media/java/android/media/quality/SoundProfileHandle.java b/media/java/android/media/quality/SoundProfileHandle.java deleted file mode 100644 index edb546efdaf3..000000000000 --- a/media/java/android/media/quality/SoundProfileHandle.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.media.quality; - -import android.annotation.NonNull; -import android.os.Parcel; -import android.os.Parcelable; - -/** - * A type-safe handle to a sound profile. - * - * @hide - */ -public final class SoundProfileHandle implements Parcelable { - public static final @NonNull SoundProfileHandle NONE = new SoundProfileHandle(-1000); - - private final long mId; - - /** @hide */ - public SoundProfileHandle(long id) { - mId = id; - } - - /** @hide */ - public long getId() { - return mId; - } - - /** @hide */ - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeLong(mId); - } - - /** @hide */ - @Override - public int describeContents() { - return 0; - } - - /** @hide */ - public static final @NonNull Creator<SoundProfileHandle> CREATOR = - new Creator<SoundProfileHandle>() { - @Override - public SoundProfileHandle createFromParcel(Parcel in) { - return new SoundProfileHandle(in); - } - - @Override - public SoundProfileHandle[] newArray(int size) { - return new SoundProfileHandle[size]; - } - }; - - private SoundProfileHandle(@NonNull Parcel in) { - mId = in.readLong(); - } -} diff --git a/media/java/android/media/quality/ActiveProcessingPicture.aidl b/media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl index 2851306f6e4d..2851306f6e4d 100644 --- a/media/java/android/media/quality/ActiveProcessingPicture.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl diff --git a/media/java/android/media/quality/AmbientBacklightEvent.aidl b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl index 174cd461e846..174cd461e846 100644 --- a/media/java/android/media/quality/AmbientBacklightEvent.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl diff --git a/media/java/android/media/quality/AmbientBacklightMetadata.aidl b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl index b95a474fbf90..b95a474fbf90 100644 --- a/media/java/android/media/quality/AmbientBacklightMetadata.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl diff --git a/media/java/android/media/quality/AmbientBacklightSettings.aidl b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl index e2cdd03194cd..e2cdd03194cd 100644 --- a/media/java/android/media/quality/AmbientBacklightSettings.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl diff --git a/media/java/android/media/quality/IAmbientBacklightCallback.aidl b/media/java/android/media/quality/aidl/android/media/quality/IAmbientBacklightCallback.aidl index 159f5b7b5e71..159f5b7b5e71 100644 --- a/media/java/android/media/quality/IAmbientBacklightCallback.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/IAmbientBacklightCallback.aidl diff --git a/media/java/android/media/quality/IMediaQualityManager.aidl b/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl index 6e9fa1dcf93d..0191ea786de0 100644 --- a/media/java/android/media/quality/IMediaQualityManager.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl @@ -25,57 +25,57 @@ import android.media.quality.PictureProfileHandle; import android.media.quality.PictureProfile; import android.media.quality.SoundProfileHandle; import android.media.quality.SoundProfile; -import android.os.Bundle; -import android.os.UserHandle; /** * Interface for Media Quality Manager * @hide */ interface IMediaQualityManager { - PictureProfile createPictureProfile(in PictureProfile pp, in UserHandle user); - void updatePictureProfile(in String id, in PictureProfile pp, in UserHandle user); - void removePictureProfile(in String id, in UserHandle user); - boolean setDefaultPictureProfile(in String id, in UserHandle user); + // TODO: use UserHandle + PictureProfile createPictureProfile(in PictureProfile pp, int userId); + void updatePictureProfile(in String id, in PictureProfile pp, int userId); + void removePictureProfile(in String id, int userId); + boolean setDefaultPictureProfile(in String id, int userId); + // TODO: use Bundle for includeParams PictureProfile getPictureProfile( - in int type, in String name, in Bundle options, in UserHandle user); + in int type, in String name, in boolean includeParams, int userId); List<PictureProfile> getPictureProfilesByPackage( - in String packageName, in Bundle options, in UserHandle user); - List<PictureProfile> getAvailablePictureProfiles(in Bundle options, in UserHandle user); - List<String> getPictureProfilePackageNames(in UserHandle user); - List<String> getPictureProfileAllowList(in UserHandle user); - void setPictureProfileAllowList(in List<String> packages, in UserHandle user); - List<PictureProfileHandle> getPictureProfileHandle(in String[] id, in UserHandle user); + in String packageName, in boolean includeParams, int userId); + List<PictureProfile> getAvailablePictureProfiles(in boolean includeParams, int userId); + List<String> getPictureProfilePackageNames(int userId); + List<String> getPictureProfileAllowList(int userId); + void setPictureProfileAllowList(in List<String> packages, int userId); + List<PictureProfileHandle> getPictureProfileHandle(in String[] id, int userId); - SoundProfile createSoundProfile(in SoundProfile pp, in UserHandle user); - void updateSoundProfile(in String id, in SoundProfile pp, in UserHandle user); - void removeSoundProfile(in String id, in UserHandle user); - boolean setDefaultSoundProfile(in String id, in UserHandle user); + SoundProfile createSoundProfile(in SoundProfile pp, int userId); + void updateSoundProfile(in String id, in SoundProfile pp, int userId); + void removeSoundProfile(in String id, int userId); + boolean setDefaultSoundProfile(in String id, int userId); SoundProfile getSoundProfile( - in int type, in String name, in Bundle options, in UserHandle user); + in int type, in String name, in boolean includeParams, int userId); List<SoundProfile> getSoundProfilesByPackage( - in String packageName, in Bundle options, in UserHandle user); - List<SoundProfile> getAvailableSoundProfiles(in Bundle options, in UserHandle user); - List<String> getSoundProfilePackageNames(in UserHandle user); - List<String> getSoundProfileAllowList(in UserHandle user); - void setSoundProfileAllowList(in List<String> packages, in UserHandle user); - List<SoundProfileHandle> getSoundProfileHandle(in String[] id, in UserHandle user); + in String packageName, in boolean includeParams, int userId); + List<SoundProfile> getAvailableSoundProfiles(in boolean includeParams, int userId); + List<String> getSoundProfilePackageNames(int userId); + List<String> getSoundProfileAllowList(int userId); + void setSoundProfileAllowList(in List<String> packages, int userId); + List<SoundProfileHandle> getSoundProfileHandle(in String[] id, int userId); void registerPictureProfileCallback(in IPictureProfileCallback cb); void registerSoundProfileCallback(in ISoundProfileCallback cb); void registerAmbientBacklightCallback(in IAmbientBacklightCallback cb); - List<ParameterCapability> getParameterCapabilities(in List<String> names, in UserHandle user); + List<ParameterCapability> getParameterCapabilities(in List<String> names, int userId); - boolean isSupported(in UserHandle user); - void setAutoPictureQualityEnabled(in boolean enabled, in UserHandle user); - boolean isAutoPictureQualityEnabled(in UserHandle user); - void setSuperResolutionEnabled(in boolean enabled, in UserHandle user); - boolean isSuperResolutionEnabled(in UserHandle user); - void setAutoSoundQualityEnabled(in boolean enabled, in UserHandle user); - boolean isAutoSoundQualityEnabled(in UserHandle user); + boolean isSupported(int userId); + void setAutoPictureQualityEnabled(in boolean enabled, int userId); + boolean isAutoPictureQualityEnabled(int userId); + void setSuperResolutionEnabled(in boolean enabled, int userId); + boolean isSuperResolutionEnabled(int userId); + void setAutoSoundQualityEnabled(in boolean enabled, int userId); + boolean isAutoSoundQualityEnabled(int userId); - void setAmbientBacklightSettings(in AmbientBacklightSettings settings, in UserHandle user); - void setAmbientBacklightEnabled(in boolean enabled, in UserHandle user); - boolean isAmbientBacklightEnabled(in UserHandle user); + void setAmbientBacklightSettings(in AmbientBacklightSettings settings, int userId); + void setAmbientBacklightEnabled(in boolean enabled, int userId); + boolean isAmbientBacklightEnabled(int userId); } diff --git a/media/java/android/media/quality/IPictureProfileCallback.aidl b/media/java/android/media/quality/aidl/android/media/quality/IPictureProfileCallback.aidl index eed77f695416..eed77f695416 100644 --- a/media/java/android/media/quality/IPictureProfileCallback.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/IPictureProfileCallback.aidl diff --git a/media/java/android/media/quality/ISoundProfileCallback.aidl b/media/java/android/media/quality/aidl/android/media/quality/ISoundProfileCallback.aidl index 3871fb212259..3871fb212259 100644 --- a/media/java/android/media/quality/ISoundProfileCallback.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/ISoundProfileCallback.aidl diff --git a/media/java/android/media/quality/ParameterCapability.aidl b/media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl index eb2ac97916f3..eb2ac97916f3 100644 --- a/media/java/android/media/quality/ParameterCapability.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl diff --git a/media/java/android/media/quality/PictureProfile.aidl b/media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl index 41d018b12f33..41d018b12f33 100644 --- a/media/java/android/media/quality/PictureProfile.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl diff --git a/media/java/android/media/quality/PictureProfileHandle.aidl b/media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl index 5d14631dbb73..5d14631dbb73 100644 --- a/media/java/android/media/quality/PictureProfileHandle.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl diff --git a/media/java/android/media/quality/SoundProfile.aidl b/media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl index e79fcaac97be..e79fcaac97be 100644 --- a/media/java/android/media/quality/SoundProfile.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl diff --git a/media/java/android/media/quality/SoundProfileHandle.aidl b/media/java/android/media/quality/aidl/android/media/quality/SoundProfileHandle.aidl index 6b8161c8cc43..ea26b19d84d7 100644 --- a/media/java/android/media/quality/SoundProfileHandle.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/SoundProfileHandle.aidl @@ -16,4 +16,7 @@ package android.media.quality; -parcelable SoundProfileHandle; +// TODO: add SoundProfileHandle.java +parcelable SoundProfileHandle { + long id; +} diff --git a/packages/SettingsLib/RestrictedLockUtils/res/values/strings.xml b/packages/SettingsLib/RestrictedLockUtils/res/values/strings.xml index 75809730a514..2ffdc930ccea 100644 --- a/packages/SettingsLib/RestrictedLockUtils/res/values/strings.xml +++ b/packages/SettingsLib/RestrictedLockUtils/res/values/strings.xml @@ -21,8 +21,4 @@ <string name="enabled_by_admin">Enabled by admin</string> <!-- Summary for switch preference to denote it is switched off by an admin [CHAR LIMIT=50] --> <string name="disabled_by_admin">Disabled by admin</string> - <!-- Summary for switch preference to denote it is switched on by Advanced protection [CHAR LIMIT=50] --> - <string name="enabled_by_advanced_protection">Enabled by Advanced Protection</string> - <!-- Summary for switch preference to denote it is switched off by Advanced protection [CHAR LIMIT=50] --> - <string name="disabled_by_advanced_protection">Disabled by Advanced Protection</string> </resources> diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt index a8483308556d..6f37f0cc5799 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt @@ -46,7 +46,7 @@ internal data class BlockedByAdminImpl( ) : BlockedByAdmin { override fun getSummary(checked: Boolean?) = when (checked) { true -> enterpriseRepository.getAdminSummaryString( - advancedProtectionStringId = R.string.enabled_by_advanced_protection, + advancedProtectionStringId = com.android.settingslib.R.string.enabled, updatableStringId = Settings.ENABLED_BY_ADMIN_SWITCH_SUMMARY, resId = R.string.enabled_by_admin, enforcedAdmin = enforcedAdmin, @@ -54,7 +54,7 @@ internal data class BlockedByAdminImpl( ) false -> enterpriseRepository.getAdminSummaryString( - advancedProtectionStringId = R.string.disabled_by_advanced_protection, + advancedProtectionStringId = com.android.settingslib.R.string.disabled, updatableStringId = Settings.DISABLED_BY_ADMIN_SWITCH_SUMMARY, resId = R.string.disabled_by_admin, enforcedAdmin = enforcedAdmin, diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemInteger.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemInteger.kt new file mode 100644 index 000000000000..db7a640be893 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemInteger.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2025 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.spaprivileged.settingsprovider + +import android.content.ContentResolver +import android.content.Context +import android.provider.Settings +import com.android.settingslib.spaprivileged.database.contentChangeFlow +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +fun Context.settingsSystemInteger( + name: String, + defaultValue: Int +): ReadWriteProperty<Any?, Int> = SettingsSystemIntegerDelegate(this, name, defaultValue) + +fun Context.settingsSystemIntegerFlow(name: String, defaultValue: Int): Flow<Int> { + val value by settingsSystemInteger(name, defaultValue) + return contentChangeFlow(Settings.System.getUriFor(name)) + .map { value } + .distinctUntilChanged() + .conflate() + .flowOn(Dispatchers.IO) +} + +private class SettingsSystemIntegerDelegate( + context: Context, + private val name: String, + private val defaultValue: Int, +) : ReadWriteProperty<Any?, Int> { + + private val contentResolver: ContentResolver = context.contentResolver + + override fun getValue(thisRef: Any?, property: KProperty<*>): Int = + Settings.System.getInt(contentResolver, name, defaultValue) + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { + Settings.System.putInt(contentResolver, name, value) + } +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/robotests/Android.bp b/packages/SettingsLib/SpaPrivileged/tests/robotests/Android.bp new file mode 100644 index 000000000000..e3faf73a69fb --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/robotests/Android.bp @@ -0,0 +1,59 @@ +// +// Copyright (C) 2025 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 { + default_applicable_licenses: ["frameworks_base_license"], +} + +android_app { + name: "SpaPrivilegedRoboTestStub", + defaults: [ + "SpaPrivilegedLib-defaults", + ], + platform_apis: true, + certificate: "platform", + privileged: true, +} + +android_robolectric_test { + name: "SpaPrivilegedRoboTests", + srcs: [ + ":SpaPrivilegedLib_srcs", + "src/**/*.java", + "src/**/*.kt", + ], + + defaults: [ + "SpaPrivilegedLib-defaults", + ], + + static_libs: [ + "SpaLibTestUtils", + "androidx.test.ext.junit", + "androidx.test.runner", + ], + + java_resource_dirs: [ + "config", + ], + + instrumentation_for: "SpaPrivilegedRoboTestStub", + + test_options: { + timeout: 36000, + }, + + strict_mode: false, +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/robotests/AndroidManifest.xml b/packages/SettingsLib/SpaPrivileged/tests/robotests/AndroidManifest.xml new file mode 100644 index 000000000000..113852d74f69 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/robotests/AndroidManifest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 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. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + coreApp="true" + package="com.android.settingslib.spaprivileged.settingsprovider"> + + <uses-permission android:name="android.permission.WRITE_SETTINGS" /> + + <application/> +</manifest>
\ No newline at end of file diff --git a/packages/SettingsLib/SpaPrivileged/tests/robotests/config/robolectric.properties b/packages/SettingsLib/SpaPrivileged/tests/robotests/config/robolectric.properties new file mode 100644 index 000000000000..95a24bde00f6 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/robotests/config/robolectric.properties @@ -0,0 +1,16 @@ +/* +* Copyright (C) 2025 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +sdk=NEWEST_SDK
\ No newline at end of file diff --git a/packages/SettingsLib/SpaPrivileged/tests/robotests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemIntegerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/robotests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemIntegerTest.kt new file mode 100644 index 000000000000..67e4180a624b --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/robotests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemIntegerTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2025 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.spaprivileged.settingsprovider + +import android.content.Context +import android.provider.Settings + +import androidx.test.core.app.ApplicationProvider + +import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull +import com.android.settingslib.spa.testutils.toListWithTimeout +import com.google.common.truth.Truth.assertThat + +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking + +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class SettingsSystemIntegerTest { + private val context: Context = ApplicationProvider.getApplicationContext() + + @Before + fun setUp() { + Settings.System.putString(context.contentResolver, TEST_NAME, null) + } + + @Test + fun setIntValue_returnSameValueByDelegate() { + val settingValue = 250 + + Settings.System.putInt(context.contentResolver, TEST_NAME, settingValue) + + val value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + + assertThat(value).isEqualTo(settingValue) + } + + @Test + fun setZero_returnZeroByDelegate() { + val settingValue = 0 + Settings.System.putInt(context.contentResolver, TEST_NAME, settingValue) + + val value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + + assertThat(value).isEqualTo(settingValue) + } + + @Test + fun setValueByDelegate_getValueFromSettings() { + val settingsValue = 5 + var value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + + value = settingsValue + + assertThat(Settings.System.getInt(context.contentResolver, TEST_NAME, TEST_SETTING_DEFAULT_VALUE)).isEqualTo(settingsValue) + } + + @Test + fun setZeroByDelegate_getZeroFromSettings() { + val settingValue = 0 + var value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + + value = settingValue + + assertThat(Settings.System.getInt(context.contentResolver, TEST_NAME, TEST_SETTING_DEFAULT_VALUE)).isEqualTo(settingValue) + } + + @Test + fun setValueByDelegate_returnValueFromsettingsSystemIntegerFlow() = runBlocking<Unit> { + val settingValue = 7 + var value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + value = settingValue + + val flow = context.settingsSystemIntegerFlow(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + + assertThat(flow.firstWithTimeoutOrNull()).isEqualTo(settingValue) + } + + @Test + fun setValueByDelegateTwice_collectAfterValueChanged_onlyKeepLatest() = runBlocking<Unit> { + val firstSettingValue = 5 + val secondSettingValue = 10 + + var value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + value = firstSettingValue + + val flow = context.settingsSystemIntegerFlow(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + value = secondSettingValue + + assertThat(flow.firstWithTimeoutOrNull()).isEqualTo(value) + } + + @Test + fun settingsSystemIntegerFlow_collectBeforeValueChanged_getBoth() = runBlocking<Unit> { + val firstSettingValue = 12 + val secondSettingValue = 17 + val delay_ms = 100L + + var value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + value = firstSettingValue + + + val listDeferred = async { + context.settingsSystemIntegerFlow(TEST_NAME, TEST_SETTING_DEFAULT_VALUE).toListWithTimeout() + } + + delay(delay_ms) + value = secondSettingValue + + assertThat(listDeferred.await()) + .containsAtLeast(firstSettingValue, secondSettingValue).inOrder() + } + + private companion object { + const val TEST_NAME = "test_system_integer_delegate" + const val TEST_SETTING_DEFAULT_VALUE = -1 + } +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt index f3245c9085e7..189bf363420c 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt @@ -77,8 +77,8 @@ class RestrictedModeTest { if (RestrictedLockUtilsInternal.isPolicyEnforcedByAdvancedProtection(context, RESTRICTION, userId)) { return when (advancedProtectionStringId) { - R.string.enabled_by_advanced_protection -> ENABLED_BY_ADVANCED_PROTECTION - R.string.disabled_by_advanced_protection -> DISABLED_BY_ADVANCED_PROTECTION + com.android.settingslib.R.string.enabled -> ENABLED + com.android.settingslib.R.string.disabled -> DISABLED else -> "" } } @@ -129,7 +129,7 @@ class RestrictedModeTest { val summary = blockedByAdmin.getSummary(true) - assertThat(summary).isEqualTo(ENABLED_BY_ADVANCED_PROTECTION) + assertThat(summary).isEqualTo(ENABLED) } @RequiresFlagsEnabled(Flags.FLAG_AAPM_API) @@ -148,7 +148,7 @@ class RestrictedModeTest { val summary = blockedByAdmin.getSummary(false) - assertThat(summary).isEqualTo(DISABLED_BY_ADVANCED_PROTECTION) + assertThat(summary).isEqualTo(DISABLED) } @RequiresFlagsEnabled(Flags.FLAG_AAPM_API) @@ -202,7 +202,7 @@ class RestrictedModeTest { const val ENABLED_BY_ADMIN = "Enabled by admin" const val DISABLED_BY_ADMIN = "Disabled by admin" - const val ENABLED_BY_ADVANCED_PROTECTION = "Enabled by advanced protection" - const val DISABLED_BY_ADVANCED_PROTECTION = "Disabled by advanced protection" + const val ENABLED = "Enabled" + const val DISABLED = "Disabled" } } diff --git a/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt index 79085af63c6d..308b285e0cfc 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt @@ -175,13 +175,7 @@ class TogglePermissionAppListPageTest { val summary = getSummary(listModel) - assertThat(summary) - .isEqualTo( - context.getString( - com.android.settingslib.widget.restricted.R.string - .disabled_by_advanced_protection - ) - ) + assertThat(summary).isEqualTo(context.getString(com.android.settingslib.R.string.disabled)) } @RequiresFlagsEnabled(Flags.FLAG_AAPM_API) diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 91ec83690722..03cb1ffbdef1 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1263,6 +1263,8 @@ <!-- [CHAR LIMIT=25] Manage applications, text telling using an application is disabled. --> <string name="disabled">Disabled</string> + <!-- Summary for a settings preference indicating it is enabled [CHAR LIMIT = 30] --> + <string name="enabled">Enabled</string> <!-- Summary of app trusted to install apps [CHAR LIMIT=45] --> <string name="external_source_trusted">Allowed</string> <!-- Summary of app not trusted to install apps [CHAR LIMIT=45] --> diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java index 1044750bae25..9d979019be58 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java @@ -120,7 +120,7 @@ public class RestrictedPreferenceHelper { final TextView summaryView = (TextView) holder.findViewById(android.R.id.summary); if (summaryView != null) { final CharSequence disabledText = getDisabledByAdminSummaryString(); - if (mDisabledByAdmin) { + if (mDisabledByAdmin && disabledText != null) { summaryView.setText(disabledText); } else if (mDisabledByEcm) { summaryView.setText(getEcmTextResId()); @@ -132,10 +132,10 @@ public class RestrictedPreferenceHelper { } } - private String getDisabledByAdminSummaryString() { + private @Nullable String getDisabledByAdminSummaryString() { if (isRestrictionEnforcedByAdvancedProtection()) { - return mContext.getString(com.android.settingslib.widget.restricted - .R.string.disabled_by_advanced_protection); + // Advanced Protection doesn't set the summary string, it keeps the current summary. + return null; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { return mContext.getSystemService(DevicePolicyManager.class).getResources().getString( @@ -321,7 +321,10 @@ public class RestrictedPreferenceHelper { } if (android.security.Flags.aapmApi() && !isEnabled && mDisabledByAdmin) { - mPreference.setSummary(getDisabledByAdminSummaryString()); + String summary = getDisabledByAdminSummaryString(); + if (summary != null) { + mPreference.setSummary(summary); + } } if (!isEnabled && mDisabledByEcm) { diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java index a5fa6a854e97..67c4207cb8be 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java @@ -36,6 +36,7 @@ import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceViewHolder; @@ -141,7 +142,7 @@ public class RestrictedSwitchPreference extends SwitchPreferenceCompat implement final TextView additionalSummaryView = (TextView) holder.findViewById( R.id.additional_summary); if (additionalSummaryView != null) { - if (isDisabledByAdmin()) { + if (isDisabledByAdmin() && switchSummary != null) { additionalSummaryView.setText(switchSummary); additionalSummaryView.setVisibility(View.VISIBLE); } else { @@ -151,7 +152,7 @@ public class RestrictedSwitchPreference extends SwitchPreferenceCompat implement } else { final TextView summaryView = (TextView) holder.findViewById(android.R.id.summary); if (summaryView != null) { - if (isDisabledByAdmin()) { + if (isDisabledByAdmin() && switchSummary != null) { summaryView.setText(switchSummary); summaryView.setVisibility(View.VISIBLE); } @@ -171,14 +172,10 @@ public class RestrictedSwitchPreference extends SwitchPreferenceCompat implement () -> context.getString(resId)); } - private String getRestrictedSwitchSummary() { + private @Nullable String getRestrictedSwitchSummary() { if (mHelper.isRestrictionEnforcedByAdvancedProtection()) { - final int apmResId = isChecked() - ? com.android.settingslib.widget.restricted.R.string - .enabled_by_advanced_protection - : com.android.settingslib.widget.restricted.R.string - .disabled_by_advanced_protection; - return getContext().getString(apmResId); + // Advanced Protection doesn't set the summary string, it keeps the current summary. + return null; } return isChecked() diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt index 88bccc9f6ebd..2ed437c94439 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt @@ -509,6 +509,7 @@ open class WifiUtils { val intent = AdvancedProtectionManager.createSupportIntent( AdvancedProtectionManager.FEATURE_ID_DISALLOW_WEP, AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_BLOCKED_INTERACTION) + intent.putExtra(DIALOG_WINDOW_TYPE, dialogWindowType) onStartActivity(intent) } else if (wifiManager.isWepSupported == true && wifiManager.queryWepAllowed()) { onAllowed() diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java index dbbbd5bf8089..f9769fa61e0d 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java @@ -23,6 +23,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.atMostOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -130,7 +131,7 @@ public class RestrictedPreferenceHelperTest { @RequiresFlagsEnabled(android.security.Flags.FLAG_AAPM_API) @Test - public void bindPreference_disabled_byAdvancedProtection_shouldDisplayDisabledSummary() { + public void bindPreference_disabled_byAdvancedProtection_shouldKeepExistingSummary() { final TextView summaryView = mock(TextView.class, RETURNS_DEEP_STUBS); final String userRestriction = UserManager.DISALLOW_UNINSTALL_APPS; final RestrictedLockUtils.EnforcedAdmin enforcedAdmin = new RestrictedLockUtils @@ -143,16 +144,14 @@ public class RestrictedPreferenceHelperTest { .thenReturn(summaryView); when(mDevicePolicyManager.getEnforcingAdmin(UserHandle.myUserId(), userRestriction)) .thenReturn(advancedProtectionEnforcingAdmin); - when(mContext.getString( - com.android.settingslib.widget.restricted.R.string.disabled_by_advanced_protection)) - .thenReturn("advanced_protection"); + summaryView.setText("existing summary"); mHelper.useAdminDisabledSummary(true); mHelper.setDisabledByAdmin(enforcedAdmin); mHelper.onBindViewHolder(mViewHolder); - verify(summaryView).setText("advanced_protection"); - verify(summaryView, never()).setVisibility(View.GONE); + verify(summaryView, atMostOnce()).setText(any()); // To set it to existing summary + verify(summaryView, never()).setVisibility(View.VISIBLE); } @RequiresFlagsEnabled(android.security.Flags.FLAG_AAPM_API) diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index dafcc729b8f1..d929b0de391a 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -187,6 +187,9 @@ <!-- Default state of tap to wake --> <bool name="def_double_tap_to_wake">true</bool> + <!-- Default setting for double tap to sleep (Settings.Secure.DOUBLE_TAP_TO_SLEEP) --> + <bool name="def_double_tap_to_sleep">false</bool> + <!-- Default for Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT --> <string name="def_nfc_payment_component"></string> diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index c0105298899b..f0a0483aa17c 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -85,6 +85,7 @@ public class SecureSettings { Settings.Secure.MOUNT_UMS_PROMPT, Settings.Secure.MOUNT_UMS_NOTIFY_ENABLED, Settings.Secure.DOUBLE_TAP_TO_WAKE, + Settings.Secure.DOUBLE_TAP_TO_SLEEP, Settings.Secure.WAKE_GESTURE_ENABLED, Settings.Secure.LONG_PRESS_TIMEOUT, Settings.Secure.KEY_REPEAT_ENABLED, @@ -293,5 +294,7 @@ public class SecureSettings { Settings.Secure.FINGERPRINT_APP_ENABLED, Settings.Secure.FINGERPRINT_KEYGUARD_ENABLED, Settings.Secure.DUAL_SHADE, + Settings.Secure.BROWSER_CONTENT_FILTERS_ENABLED, + Settings.Secure.SEARCH_CONTENT_FILTERS_ENABLED, }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 0ffdf53f2036..b5de7e719043 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -131,6 +131,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.MOUNT_UMS_PROMPT, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.MOUNT_UMS_NOTIFY_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.DOUBLE_TAP_TO_WAKE, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.DOUBLE_TAP_TO_SLEEP, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.WAKE_GESTURE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.LONG_PRESS_TIMEOUT, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.KEY_REPEAT_ENABLED, BOOLEAN_VALIDATOR); @@ -461,5 +462,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.FINGERPRINT_APP_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.FINGERPRINT_KEYGUARD_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.DUAL_SHADE, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.BROWSER_CONTENT_FILTERS_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.SEARCH_CONTENT_FILTERS_ENABLED, BOOLEAN_VALIDATOR); } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 4a225bdbd7e5..65ede9d804d0 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -4080,7 +4080,7 @@ public class SettingsProvider extends ContentProvider { @VisibleForTesting final class UpgradeController { - private static final int SETTINGS_VERSION = 227; + private static final int SETTINGS_VERSION = 228; private final int mUserId; @@ -6319,6 +6319,23 @@ public class SettingsProvider extends ContentProvider { currentVersion = 227; } + // Version 227: Add default value for DOUBLE_TAP_TO_SLEEP. + if (currentVersion == 227) { + final SettingsState secureSettings = getSecureSettingsLocked(userId); + final Setting doubleTapToSleep = secureSettings.getSettingLocked( + Settings.Secure.DOUBLE_TAP_TO_SLEEP); + if (doubleTapToSleep.isNull()) { + secureSettings.insertSettingOverrideableByRestoreLocked( + Settings.Secure.DOUBLE_TAP_TO_SLEEP, + getContext().getResources().getBoolean( + R.bool.def_double_tap_to_sleep) ? "1" : "0", + null /* tag */, + true /* makeDefault */, + SettingsState.SYSTEM_PACKAGE_NAME); + } + currentVersion = 228; + } + // vXXX: Add new settings above this point. if (currentVersion != newVersion) { diff --git a/packages/SystemUI/aconfig/predictive_back.aconfig b/packages/SystemUI/aconfig/predictive_back.aconfig index 89a0d895a17c..dd9a8b19f498 100644 --- a/packages/SystemUI/aconfig/predictive_back.aconfig +++ b/packages/SystemUI/aconfig/predictive_back.aconfig @@ -9,8 +9,11 @@ flag { } flag { - name: "predictive_back_delay_transition" + name: "predictive_back_delay_wm_transition" namespace: "systemui" description: "Slightly delays the back transition start" bug: "301195601" + metadata { + purpose: PURPOSE_BUGFIX + } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt index 96feeedb8793..e734dd26eb15 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt @@ -25,6 +25,7 @@ data class AxisDefinition( ) object GSFAxes { + @JvmStatic val WEIGHT = AxisDefinition( tag = "wght", diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt index 5b073e49192a..4a39cff388a9 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt @@ -39,6 +39,7 @@ interface TypefaceVariantCache { fun getTypefaceForVariant(fvar: String?): Typeface? companion object { + @JvmStatic fun createVariantTypeface(baseTypeface: Typeface, fVar: String?): Typeface { if (fVar.isNullOrEmpty()) { return baseTypeface diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt index 0bd51cd9822d..44f2353dcb75 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt @@ -111,8 +111,8 @@ private constructor(metadata: FailureMetadata, private val actual: TransitionSta } companion object { - fun transitionStates() = Factory { metadata, actual: TransitionState -> - TransitionStateSubject(metadata, actual) + fun transitionStates() = Factory { metadata, actual: TransitionState? -> + TransitionStateSubject(metadata, actual!!) } } } @@ -181,8 +181,8 @@ private constructor(metadata: FailureMetadata, actual: TransitionState.Transitio companion object { fun sceneTransitions() = - Factory { metadata, actual: TransitionState.Transition.ChangeScene -> - SceneTransitionSubject(metadata, actual) + Factory { metadata, actual: TransitionState.Transition.ChangeScene? -> + SceneTransitionSubject(metadata, actual!!) } } } @@ -202,8 +202,8 @@ private constructor( companion object { fun showOrHideOverlayTransitions() = - Factory { metadata, actual: TransitionState.Transition.ShowOrHideOverlay -> - ShowOrHideOverlayTransitionSubject(metadata, actual) + Factory { metadata, actual: TransitionState.Transition.ShowOrHideOverlay? -> + ShowOrHideOverlayTransitionSubject(metadata, actual!!) } } } @@ -221,8 +221,8 @@ private constructor(metadata: FailureMetadata, actual: TransitionState.Transitio companion object { fun replaceOverlayTransitions() = - Factory { metadata, actual: TransitionState.Transition.ReplaceOverlay -> - ReplaceOverlayTransitionSubject(metadata, actual) + Factory { metadata, actual: TransitionState.Transition.ReplaceOverlay? -> + ReplaceOverlayTransitionSubject(metadata, actual!!) } } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt index ab31038fac8f..368a333fbd78 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt @@ -49,8 +49,8 @@ class DpOffsetSubject(metadata: FailureMetadata, private val actual: DpOffset) : val DefaultTolerance = Dp(.5f) fun dpOffsets() = - Factory<DpOffsetSubject, DpOffset> { metadata, actual -> - DpOffsetSubject(metadata, actual) + Factory { metadata, actual: DpOffset? -> + DpOffsetSubject(metadata, actual!!) } } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt index e9e61a718f08..37acbe261f76 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt @@ -161,15 +161,7 @@ class ComposedDigitalLayerController(private val clockCtx: ClockContext) : } override fun onThemeChanged(theme: ThemeConfig) { - val color = - when { - theme.seedColor != null -> theme.seedColor!! - theme.isDarkTheme -> - clockCtx.resources.getColor(android.R.color.system_accent1_100) - else -> clockCtx.resources.getColor(android.R.color.system_accent2_600) - } - - view.updateColor(color) + view.updateColor(theme.getDefaultColor(clockCtx.context)) } override fun onFontSettingChanged(fontSizePx: Float) { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt index 365567b17ec0..bc4bdf4243cb 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -149,14 +149,7 @@ class DefaultClockController( override fun onThemeChanged(theme: ThemeConfig) { this@DefaultClockFaceController.theme = theme - val color = - when { - theme.seedColor != null -> theme.seedColor!! - theme.isDarkTheme -> - resources.getColor(android.R.color.system_accent1_100) - else -> resources.getColor(android.R.color.system_accent2_600) - } - + val color = theme.getDefaultColor(ctx) if (currentColor == color) { return } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt index 97004ef6f9a9..1d963af3ad22 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt @@ -229,15 +229,7 @@ open class SimpleDigitalHandLayerController( } override fun onThemeChanged(theme: ThemeConfig) { - val color = - when { - theme.seedColor != null -> theme.seedColor!! - theme.isDarkTheme -> - clockCtx.resources.getColor(android.R.color.system_accent1_100) - else -> clockCtx.resources.getColor(android.R.color.system_accent2_600) - } - - view.updateColor(color) + view.updateColor(theme.getDefaultColor(clockCtx.context)) refreshTime() } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt index fae17a5321ff..0ec2d188833a 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt @@ -45,6 +45,7 @@ import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.replace import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.toFVar import com.android.systemui.plugins.clocks.ClockLogger +import com.android.systemui.shared.Flags.ambientAod import com.android.systemui.shared.clocks.CanvasUtil.translate import com.android.systemui.shared.clocks.CanvasUtil.use import com.android.systemui.shared.clocks.ClockContext @@ -330,7 +331,7 @@ open class SimpleDigitalClockTextView( textAnimator.setTextStyle( TextAnimator.Style( fVar = if (isDozing) aodFontVariation else lsFontVariation, - color = if (isDozing) AOD_COLOR else lockscreenColor, + color = if (isDozing && !ambientAod()) AOD_COLOR else lockscreenColor, textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize, ), TextAnimator.Animation( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/KairosCoreStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/KairosCoreStartableTest.kt new file mode 100644 index 000000000000..4daf02332d41 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/KairosCoreStartableTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2025 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 + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.KairosNetwork +import com.android.systemui.kairos.runKairosTest +import com.android.systemui.kairos.toColdConflatedFlow +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import org.junit.runner.RunWith + +@OptIn(ExperimentalKairosApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class KairosCoreStartableTest : SysuiTestCase() { + + @Test + fun kairosNetwork_usedBeforeStarted() = + testKosmos().useUnconfinedTestDispatcher().runKairosTest { + lateinit var activatable: TestActivatable + val underTest = KairosCoreStartable(applicationCoroutineScope) { setOf(activatable) } + activatable = TestActivatable(underTest) + + // collect from the cold flow before starting the CoreStartable + var collectCount = 0 + testScope.backgroundScope.launch { activatable.coldFlow.collect { collectCount++ } } + + // start the CoreStartable + underTest.start() + + // verify emissions are received + activatable.emitEvent() + + assertThat(collectCount).isEqualTo(1) + } + + private class TestActivatable(network: KairosNetwork) : KairosBuilder by kairosBuilder() { + private val emitter = MutableSharedFlow<Unit>() + private val events = buildEvents { emitter.toEvents() } + + val coldFlow = events.toColdConflatedFlow(network) + + suspend fun emitEvent() = emitter.emit(Unit) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt index e0515000b232..454c15667f22 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt @@ -56,6 +56,7 @@ import com.android.systemui.statusbar.phone.dozeScrimController import com.android.systemui.statusbar.phone.screenOffAnimationController import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy @@ -105,7 +106,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { @Test fun nonPowerButtonFPS_vibrateSuccess() = testScope.runTest { - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC) runCurrent() enterDeviceFromFingerprintUnlockLegacy() @@ -116,7 +117,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { @Test fun powerButtonFPS_vibrateSuccess() = testScope.runTest { - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.POWER_BUTTON) kosmos.fakeKeyEventRepository.setPowerButtonDown(false) @@ -133,7 +134,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { @Test fun powerButtonFPS_powerDown_doNotVibrateSuccess() = testScope.runTest { - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.POWER_BUTTON) kosmos.fakeKeyEventRepository.setPowerButtonDown(true) // power button is currently DOWN @@ -150,7 +151,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { @Test fun powerButtonFPS_powerButtonRecentlyPressed_doNotVibrateSuccess() = testScope.runTest { - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.POWER_BUTTON) kosmos.fakeKeyEventRepository.setPowerButtonDown(false) @@ -174,14 +175,14 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { } @Test - fun nonPowerButtonFPS_coExFaceFailure_vibrateError() = + fun nonPowerButtonFPS_coExFaceFailure_doNotVibrateError() = testScope.runTest { val playErrorHaptic by collectLastValue(underTest.playErrorHaptic) enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC) enrollFace() runCurrent() faceFailure() - assertThat(playErrorHaptic).isNotNull() + assertThat(playErrorHaptic).isNull() } @Test @@ -211,7 +212,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { testScope.runTest { kosmos.configureKeyguardBypass(isBypassAvailable = false) underTest = kosmos.deviceEntryHapticsInteractor - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC) runCurrent() configureDeviceEntryFromBiometricSource(isFpUnlock = true) @@ -225,7 +226,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { testScope.runTest { kosmos.configureKeyguardBypass(isBypassAvailable = false) underTest = kosmos.deviceEntryHapticsInteractor - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.POWER_BUTTON) kosmos.fakeKeyEventRepository.setPowerButtonDown(false) @@ -246,18 +247,19 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { enrollFace() kosmos.configureKeyguardBypass(isBypassAvailable = true) underTest = kosmos.deviceEntryHapticsInteractor - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) configureDeviceEntryFromBiometricSource(isFaceUnlock = true) verifyDeviceEntryFromFaceAuth() assertThat(playSuccessHaptic).isNotNull() } + @OptIn(ExperimentalCoroutinesApi::class) @EnableSceneContainer @Test - fun playSuccessHaptic_onFaceAuthSuccess_whenBypassDisabled_sceneContainer() = + fun skipSuccessHaptic_onFaceAuthSuccess_whenBypassDisabled_sceneContainer() = testScope.runTest { underTest = kosmos.deviceEntryHapticsInteractor - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFace() kosmos.configureKeyguardBypass(isBypassAvailable = false) @@ -265,7 +267,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { configureDeviceEntryFromBiometricSource(isFaceUnlock = true, bypassEnabled = false) kosmos.fakeDeviceEntryFaceAuthRepository.isAuthenticated.value = true - assertThat(playSuccessHaptic).isNotNull() + assertThat(playSuccessHaptic).isNull() } @EnableSceneContainer @@ -274,7 +276,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { testScope.runTest { kosmos.configureKeyguardBypass(isBypassAvailable = false) underTest = kosmos.deviceEntryHapticsInteractor - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.POWER_BUTTON) // power button is currently DOWN kosmos.fakeKeyEventRepository.setPowerButtonDown(true) @@ -295,7 +297,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { testScope.runTest { kosmos.configureKeyguardBypass(isBypassAvailable = false) underTest = kosmos.deviceEntryHapticsInteractor - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.POWER_BUTTON) kosmos.fakeKeyEventRepository.setPowerButtonDown(false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt index f37306276848..684af6fc8040 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt @@ -57,4 +57,17 @@ class KeyEventInteractorTest : SysuiTestCase() { repository.setPowerButtonDown(true) assertThat(isPowerDown).isTrue() } + + @Test + fun testPowerButtonBeingLongPressedInteractor() = + runTest { + val isPowerButtonLongPressed by collectLastValue( + underTest.isPowerButtonLongPressed) + + repository.setPowerButtonBeingLongPressed(false) + assertThat(isPowerButtonLongPressed).isFalse() + + repository.setPowerButtonBeingLongPressed(true) + assertThat(isPowerButtonLongPressed).isTrue() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt index c7f1525e2946..9ab5f8948ecc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyevent.data.repository.KeyEventRepositoryImpl import com.android.systemui.statusbar.CommandQueue import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -34,10 +35,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock -import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class KeyEventRepositoryTest : SysuiTestCase() { @@ -62,6 +63,15 @@ class KeyEventRepositoryTest : SysuiTestCase() { } @Test + fun isPowerButtonBeingLongPressed_initialValueFalse() = + testScope.runTest { + val isPowerButtonLongPressed by collectLastValue( + underTest.isPowerButtonLongPressed) + runCurrent() + assertThat(isPowerButtonLongPressed).isFalse() + } + + @Test fun isPowerButtonDown_onChange() = testScope.runTest { val isPowerButtonDown by collectLastValue(underTest.isPowerButtonDown) @@ -77,4 +87,54 @@ class KeyEventRepositoryTest : SysuiTestCase() { ) assertThat(isPowerButtonDown).isFalse() } + + + @Test + fun isPowerButtonBeingLongPressed_onPowerButtonDown() = + testScope.runTest { + val isPowerButtonLongPressed by collectLastValue( + underTest.isPowerButtonLongPressed) + + runCurrent() + + verify(commandQueue).addCallback(commandQueueCallbacks.capture()) + + val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_POWER) + commandQueueCallbacks.value.handleSystemKey(keyEvent) + + assertThat(isPowerButtonLongPressed).isFalse() + } + + @Test + fun isPowerButtonBeingLongPressed_onPowerButtonUp() = + testScope.runTest { + val isPowerButtonLongPressed by collectLastValue( + underTest.isPowerButtonLongPressed) + + runCurrent() + + verify(commandQueue).addCallback(commandQueueCallbacks.capture()) + + val keyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_POWER) + commandQueueCallbacks.value.handleSystemKey(keyEvent) + + assertThat(isPowerButtonLongPressed).isFalse() + } + + @Test + fun isPowerButtonBeingLongPressed_onPowerButtonDown_longPressFlagSet() = + testScope.runTest { + val isPowerButtonBeingLongPressed by collectLastValue( + underTest.isPowerButtonLongPressed) + + runCurrent() + + verify(commandQueue).addCallback(commandQueueCallbacks.capture()) + + val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_POWER) + keyEvent.setFlags(KeyEvent.FLAG_LONG_PRESS) + commandQueueCallbacks.value.handleSystemKey(keyEvent) + + assertThat(isPowerButtonBeingLongPressed).isTrue() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/log/LogWtfHandlerRuleTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/log/LogWtfHandlerRuleTest.kt new file mode 100644 index 000000000000..d5d256e5cd97 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/log/LogWtfHandlerRuleTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2025 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 + +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.model.Statement +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +@SmallTest +class LogWtfHandlerRuleTest : SysuiTestCase() { + + val underTest = LogWtfHandlerRule() + + @Test + fun passingTestWithoutWtf_shouldPass() { + val result = runTestCodeWithRule { + Log.e(TAG, "just an error", IndexOutOfBoundsException()) + } + assertThat(result.isSuccess).isTrue() + } + + @Test + fun passingTestWithWtf_shouldFail() { + val result = runTestCodeWithRule { + Log.wtf(TAG, "some terrible failure", IllegalStateException()) + } + assertThat(result.isFailure).isTrue() + val exception = result.exceptionOrNull() + assertThat(exception).isInstanceOf(AssertionError::class.java) + assertThat(exception?.cause).isInstanceOf(Log.TerribleFailure::class.java) + assertThat(exception?.cause?.cause).isInstanceOf(IllegalStateException::class.java) + } + + @Test + fun failingTestWithoutWtf_shouldFail() { + val result = runTestCodeWithRule { + Log.e(TAG, "just an error", IndexOutOfBoundsException()) + throw NullPointerException("some npe") + } + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java) + } + + @Test + fun failingTestWithWtf_shouldFail() { + val result = runTestCodeWithRule { + Log.wtf(TAG, "some terrible failure", IllegalStateException()) + throw NullPointerException("some npe") + } + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java) + val suppressedExceptions = result.exceptionOrNull()!!.suppressedExceptions + assertThat(suppressedExceptions).hasSize(1) + val suppressed = suppressedExceptions.first() + assertThat(suppressed).isInstanceOf(AssertionError::class.java) + assertThat(suppressed.cause).isInstanceOf(Log.TerribleFailure::class.java) + assertThat(suppressed.cause?.cause).isInstanceOf(IllegalStateException::class.java) + } + + @Test + fun passingTestWithExemptWtf_shouldPass() { + underTest.addFailureLogExemption { it.tag == TAG_EXPECTED } + val result = runTestCodeWithRule { + Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException()) + } + assertThat(result.isSuccess).isTrue() + } + + @Test + fun failingTestWithExemptWtf_shouldFail() { + underTest.addFailureLogExemption { it.tag == TAG_EXPECTED } + val result = runTestCodeWithRule { + Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException()) + throw NullPointerException("some npe") + } + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java) + val suppressedExceptions = result.exceptionOrNull()!!.suppressedExceptions + assertThat(suppressedExceptions).isEmpty() + } + + @Test + fun passingTestWithOneExemptWtfOfTwo_shouldFail() { + underTest.addFailureLogExemption { it.tag == TAG_EXPECTED } + val result = runTestCodeWithRule { + Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException()) + Log.wtf(TAG, "some terrible failure", IllegalStateException()) + } + assertThat(result.isFailure).isTrue() + val exception = result.exceptionOrNull() + assertThat(exception).isInstanceOf(AssertionError::class.java) + assertThat(exception?.cause).isInstanceOf(Log.TerribleFailure::class.java) + assertThat(exception?.cause?.cause).isInstanceOf(IllegalStateException::class.java) + } + + @Test + fun failingTestWithOneExemptWtfOfTwo_shouldFail() { + underTest.addFailureLogExemption { it.tag == TAG_EXPECTED } + val result = runTestCodeWithRule { + Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException()) + Log.wtf(TAG, "some terrible failure", IllegalStateException()) + throw NullPointerException("some npe") + } + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java) + val suppressedExceptions = result.exceptionOrNull()!!.suppressedExceptions + assertThat(suppressedExceptions).hasSize(1) + val suppressed = suppressedExceptions.first() + assertThat(suppressed).isInstanceOf(AssertionError::class.java) + assertThat(suppressed.cause).isInstanceOf(Log.TerribleFailure::class.java) + assertThat(suppressed.cause?.cause).isInstanceOf(IllegalStateException::class.java) + } + + private fun runTestCodeWithRule(testCode: () -> Unit): Result<Unit> { + val testCodeStatement = + object : Statement() { + override fun evaluate() { + testCode() + } + } + val wrappedTest = underTest.apply(testCodeStatement, mock()) + return try { + wrappedTest.evaluate() + Result.success(Unit) + } catch (e: Throwable) { + Result.failure(e) + } + } + + companion object { + const val TAG = "LogWtfHandlerRuleTest" + const val TAG_EXPECTED = "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 9adf24f32c0c..1743e056b65c 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 @@ -863,7 +863,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasUdfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -885,7 +885,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasUdfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -907,7 +907,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasSfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -930,7 +930,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasSfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -1033,7 +1033,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasSfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -1056,7 +1056,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasSfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -1079,7 +1079,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasSfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -1102,7 +1102,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasSfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -1160,7 +1160,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) - fun playsFaceErrorHaptics_nonSfps_coEx() = + fun skipsFaceErrorHaptics_nonSfps_coEx() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic) @@ -1172,15 +1172,14 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() updateFaceAuthStatus(isSuccess = false) - assertThat(playErrorHaptic).isNotNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) - verify(vibratorHelper).vibrateAuthError(anyString()) + assertThat(playErrorHaptic).isNull() + verify(vibratorHelper, never()).vibrateAuthError(anyString()) verify(vibratorHelper, never()).vibrateAuthSuccess(anyString()) } @Test @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) - fun playsMSDLFaceErrorHaptics_nonSfps_coEx() = + fun skipsMSDLFaceErrorHaptics_nonSfps_coEx() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic) @@ -1192,10 +1191,9 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() updateFaceAuthStatus(isSuccess = false) - assertThat(playErrorHaptic).isNotNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) - assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.FAILURE) - assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties) + assertThat(playErrorHaptic).isNull() + assertThat(msdlPlayer.latestTokenPlayed).isNull() + assertThat(msdlPlayer.latestPropertiesPlayed).isNull() } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index 3407cd50e76f..4a304071ee97 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -41,7 +41,6 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.platform.test.flag.junit.FlagsParameterization; -import android.provider.Settings; import android.testing.TestableLooper.RunWithLooper; import android.view.View; import android.view.WindowManager; @@ -71,7 +70,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.user.domain.interactor.SelectedUserInteractor; -import com.android.systemui.util.settings.FakeSettings; import com.google.common.util.concurrent.MoreExecutors; @@ -113,7 +111,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters; @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener; - private FakeSettings mSecureSettings; private final Executor mMainExecutor = MoreExecutors.directExecutor(); private final Executor mBackgroundExecutor = MoreExecutors.directExecutor(); private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); @@ -135,9 +132,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { public void setUp() { MockitoAnnotations.initMocks(this); - mSecureSettings = new FakeSettings(); - mSecureSettings.putInt(Settings.Secure.DISABLE_SECURE_WINDOWS, 0); - // Preferred refresh rate is equal to the first displayMode's refresh rate mPreferredRefreshRate = mContext.getDisplay().getSystemSupportedModes()[0].getRefreshRate(); overrideResource( @@ -171,7 +165,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { () -> mSelectedUserInteractor, mUserTracker, mKosmos.getNotificationShadeWindowModel(), - mSecureSettings, mKosmos::getCommunalInteractor, mKosmos.getShadeLayoutParams()); mNotificationShadeWindowController.setScrimsVisibilityListener((visibility) -> {}); @@ -355,19 +348,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { } @Test - public void setKeyguardShowingWithSecureWindowsDisabled_disablesSecureFlag() { - mSecureSettings.putInt(Settings.Secure.DISABLE_SECURE_WINDOWS, 1); - mNotificationShadeWindowController.setBouncerShowing(true); - - verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); - assertThat((mLayoutParameters.getValue().flags & FLAG_SECURE) == 0).isTrue(); - assertThat( - (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_PRIVACY) - != 0) - .isTrue(); - } - - @Test public void setKeyguardNotShowing_disablesSecureFlag() { mNotificationShadeWindowController.setBouncerShowing(false); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt index fad66581682f..0642467a001b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt @@ -19,6 +19,8 @@ package com.android.systemui.shared.clocks import android.content.res.Resources import android.graphics.Color import android.graphics.drawable.Drawable +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.util.TypedValue import android.view.LayoutInflater import android.widget.FrameLayout @@ -29,6 +31,7 @@ import com.android.systemui.customization.R import com.android.systemui.plugins.clocks.ClockId import com.android.systemui.plugins.clocks.ClockSettings import com.android.systemui.plugins.clocks.ThemeConfig +import com.android.systemui.shared.Flags import com.android.systemui.shared.clocks.DefaultClockController.Companion.DOZE_COLOR import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -103,6 +106,26 @@ class DefaultClockProviderTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_AMBIENT_AOD) + fun defaultClock_initialize_flagOff() { + val clock = provider.createClock(DEFAULT_CLOCK_ID) + verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA) + verify(mockLargeClockView).setColors(DOZE_COLOR, Color.MAGENTA) + + clock.initialize(true, 0f, 0f, null) + + // This is the default darkTheme color + val expectedColor = context.resources.getColor(android.R.color.system_accent1_100) + verify(mockSmallClockView).setColors(DOZE_COLOR, expectedColor) + verify(mockLargeClockView).setColors(DOZE_COLOR, expectedColor) + verify(mockSmallClockView).onTimeZoneChanged(notNull()) + verify(mockLargeClockView).onTimeZoneChanged(notNull()) + verify(mockSmallClockView).refreshTime() + verify(mockLargeClockView).refreshTime() + } + + @Test + @EnableFlags(Flags.FLAG_AMBIENT_AOD) fun defaultClock_initialize() { val clock = provider.createClock(DEFAULT_CLOCK_ID) verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA) @@ -110,7 +133,7 @@ class DefaultClockProviderTest : SysuiTestCase() { clock.initialize(true, 0f, 0f, null) - val expectedColor = 0 + val expectedColor = Color.MAGENTA verify(mockSmallClockView).setColors(DOZE_COLOR, expectedColor) verify(mockLargeClockView).setColors(DOZE_COLOR, expectedColor) verify(mockSmallClockView).onTimeZoneChanged(notNull()) @@ -165,8 +188,10 @@ class DefaultClockProviderTest : SysuiTestCase() { } @Test - fun defaultClock_events_onThemeChanged_noSeed() { - val expectedColor = 0 + @DisableFlags(Flags.FLAG_AMBIENT_AOD) + fun defaultClock_events_onThemeChanged_noSeed_flagOff() { + // This is the default darkTheme color + val expectedColor = context.resources.getColor(android.R.color.system_accent1_100) val clock = provider.createClock(DEFAULT_CLOCK_ID) verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA) @@ -180,6 +205,22 @@ class DefaultClockProviderTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_AMBIENT_AOD) + fun defaultClock_events_onThemeChanged_noSeedn() { + val expectedColor = Color.TRANSPARENT + val clock = provider.createClock(DEFAULT_CLOCK_ID) + + verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA) + verify(mockLargeClockView).setColors(DOZE_COLOR, Color.MAGENTA) + + clock.smallClock.events.onThemeChanged(ThemeConfig(true, null)) + clock.largeClock.events.onThemeChanged(ThemeConfig(true, null)) + + verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA) + verify(mockLargeClockView).setColors(DOZE_COLOR, Color.MAGENTA) + } + + @Test fun defaultClock_events_onThemeChanged_newSeed() { val initSeedColor = 10 val newSeedColor = 20 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index fda4ab005446..b085ba4a4dad 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -26,7 +26,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Icon -import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher @@ -46,7 +46,6 @@ import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlin.test.Test -import kotlinx.coroutines.test.runTest import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.mock @@ -71,7 +70,7 @@ class CallChipViewModelTest : SysuiTestCase() { private val mockExpandable: Expandable = mock<Expandable>().apply { whenever(dialogTransitionController(any())).thenReturn(mock()) } - private val underTest by lazy { kosmos.callChipViewModel } + private val Kosmos.underTest by Kosmos.Fixture { callChipViewModel } @Test fun chip_noCall_isHidden() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt index 96c4a59f752d..670ebadcf5a7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt @@ -62,6 +62,7 @@ import com.android.systemui.statusbar.notification.data.repository.ActiveNotific import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.addNotif import com.android.systemui.statusbar.notification.data.repository.addNotifs +import com.android.systemui.statusbar.notification.data.repository.removeNotif import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.phone.SystemUIDialog @@ -349,6 +350,36 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) } + @EnableChipsModernization + @Test + fun chips_threeChips_isSmallPortrait_allSquished() = + kosmos.runTest { + screenRecordState.value = ScreenRecordModel.Recording + addOngoingCallState(key = "call") + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.shortCriticalText = "Some text here" + } + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + + val latest by collectLastValue(underTest.chips) + + // Squished chips are icon only + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat(latest!!.active[1]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat(latest!!.active[2]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + @DisableChipsModernization @Test fun chipsLegacy_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() = @@ -851,7 +882,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @EnableChipsModernization @Test - fun chips_threePromotedNotifs_topTwoActiveThirdInOverflow() = + fun chips_fourPromotedNotifs_topThreeActiveFourthInOverflow() = kosmos.runTest { val latest by collectLastValue(underTest.chips) val unused by collectLastValue(underTest.chipsLegacy) @@ -859,6 +890,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val firstIcon = createStatusBarIconViewOrNull() val secondIcon = createStatusBarIconViewOrNull() val thirdIcon = createStatusBarIconViewOrNull() + val fourthIcon = createStatusBarIconViewOrNull() setNotifs( listOf( activeNotificationModel( @@ -879,20 +911,27 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { promotedContent = PromotedNotificationContentModel.Builder("thirdNotif").build(), ), + activeNotificationModel( + key = "fourthNotif", + statusBarChipIcon = thirdIcon, + promotedContent = + PromotedNotificationContentModel.Builder("fourthNotif").build(), + ), ) ) - assertThat(latest!!.active.size).isEqualTo(2) + assertThat(latest!!.active.size).isEqualTo(3) assertIsNotifChip(latest!!.active[0], context, firstIcon, "firstNotif") assertIsNotifChip(latest!!.active[1], context, secondIcon, "secondNotif") + assertIsNotifChip(latest!!.active[2], context, thirdIcon, "thirdNotif") assertThat(latest!!.overflow.size).isEqualTo(1) - assertIsNotifChip(latest!!.overflow[0], context, thirdIcon, "thirdNotif") + assertIsNotifChip(latest!!.overflow[0], context, fourthIcon, "fourthNotif") assertThat(latest!!.inactive.size).isEqualTo(4) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) } @Test - fun visibleChipKeys_threePromotedNotifs_topTwoInList() = + fun visibleChipKeys_fourPromotedNotifs_topThreeInList() = kosmos.runTest { val latest by collectLastValue(underTest.visibleChipKeys) @@ -916,10 +955,16 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { promotedContent = PromotedNotificationContentModel.Builder("thirdNotif").build(), ), + activeNotificationModel( + key = "fourthNotif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = + PromotedNotificationContentModel.Builder("fourthNotif").build(), + ), ) ) - assertThat(latest).containsExactly("firstNotif", "secondNotif").inOrder() + assertThat(latest).containsExactly("firstNotif", "secondNotif", "thirdNotif").inOrder() } @DisableChipsModernization @@ -957,7 +1002,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @EnableChipsModernization @Test - fun chips_callAndPromotedNotifs_callAndFirstNotifActiveSecondNotifInOverflow() = + fun chips_callAndPromotedNotifs_callAndFirstTwoNotifsActive_thirdNotifInOverflow() = kosmos.runTest { val latest by collectLastValue(underTest.chips) val unused by collectLastValue(underTest.chipsLegacy) @@ -965,6 +1010,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val callNotificationKey = "call" val firstIcon = createStatusBarIconViewOrNull() val secondIcon = createStatusBarIconViewOrNull() + val thirdIcon = createStatusBarIconViewOrNull() addOngoingCallState(key = callNotificationKey) activeNotificationListRepository.addNotifs( listOf( @@ -980,21 +1026,28 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { promotedContent = PromotedNotificationContentModel.Builder("secondNotif").build(), ), + activeNotificationModel( + key = "thirdNotif", + statusBarChipIcon = thirdIcon, + promotedContent = + PromotedNotificationContentModel.Builder("thirdNotif").build(), + ), ) ) - assertThat(latest!!.active.size).isEqualTo(2) + assertThat(latest!!.active.size).isEqualTo(3) assertIsCallChip(latest!!.active[0], callNotificationKey, context) assertIsNotifChip(latest!!.active[1], context, firstIcon, "firstNotif") + assertIsNotifChip(latest!!.active[2], context, secondIcon, "secondNotif") assertThat(latest!!.overflow.size).isEqualTo(1) - assertIsNotifChip(latest!!.overflow[0], context, secondIcon, "secondNotif") + assertIsNotifChip(latest!!.overflow[0], context, thirdIcon, "thirdNotif") assertThat(latest!!.inactive.size).isEqualTo(3) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) } @DisableChipsModernization @Test - fun chipsLegacy_screenRecordAndCallAndPromotedNotifs_notifsNotShown() = + fun chipsLegacy_screenRecordAndCallAndPromotedNotif_notifNotShown() = kosmos.runTest { val callNotificationKey = "call" val latest by collectLastValue(underTest.chipsLegacy) @@ -1016,7 +1069,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { } @Test - fun visibleChipKeys_screenRecordAndCallAndPromotedNotifs_topTwoInList() = + fun visibleChipKeys_screenRecordAndCallAndPromotedNotifs_topThreeInList() = kosmos.runTest { val latest by collectLastValue(underTest.visibleChipKeys) @@ -1025,20 +1078,27 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.Recording activeNotificationListRepository.addNotif( activeNotificationModel( - key = "notif", + key = "notif1", statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), + ) + ) + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif2", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = PromotedNotificationContentModel.Builder("notif2").build(), ) ) assertThat(latest) - .containsExactly(ScreenRecordChipViewModel.KEY, callNotificationKey) + .containsExactly(ScreenRecordChipViewModel.KEY, callNotificationKey, "notif1") .inOrder() } @EnableChipsModernization @Test - fun chips_screenRecordAndCallAndPromotedNotif_notifInOverflow() = + fun chips_screenRecordAndCallAndPromotedNotifs_secondNotifInOverflow() = kosmos.runTest { val latest by collectLastValue(underTest.chips) val unused by collectLastValue(underTest.chipsLegacy) @@ -1055,11 +1115,22 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) addOngoingCallState(key = callNotificationKey) - assertThat(latest!!.active.size).isEqualTo(2) + // This is the overflow notif + val notifIcon2 = createStatusBarIconViewOrNull() + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif2", + statusBarChipIcon = notifIcon2, + promotedContent = PromotedNotificationContentModel.Builder("notif2").build(), + ) + ) + + assertThat(latest!!.active.size).isEqualTo(3) assertIsScreenRecordChip(latest!!.active[0]) assertIsCallChip(latest!!.active[1], callNotificationKey, context) + assertIsNotifChip(latest!!.active[2], context, notifIcon, "notif") assertThat(latest!!.overflow.size).isEqualTo(1) - assertIsNotifChip(latest!!.overflow[0], context, notifIcon, "notif") + assertIsNotifChip(latest!!.overflow[0], context, notifIcon2, "notif2") assertThat(latest!!.inactive.size).isEqualTo(2) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) } @@ -1234,15 +1305,16 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @Test fun chips_movesChipsAroundAccordingToPriority() = kosmos.runTest { + systemClock.setCurrentTimeMillis(10_000) val callNotificationKey = "call" // Start with just the lowest priority chip active - val notifIcon = createStatusBarIconViewOrNull() + val notif1Icon = createStatusBarIconViewOrNull() setNotifs( listOf( activeNotificationModel( - key = "notif", - statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + key = "notif1", + statusBarChipIcon = notif1Icon, + promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), ) ) ) @@ -1254,7 +1326,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val unused by collectLastValue(underTest.chipsLegacy) assertThat(latest!!.active.size).isEqualTo(1) - assertIsNotifChip(latest!!.active[0], context, notifIcon, "notif") + assertIsNotifChip(latest!!.active[0], context, notif1Icon, "notif1") assertThat(latest!!.overflow).isEmpty() assertThat(latest!!.inactive.size).isEqualTo(4) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) @@ -1262,10 +1334,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // WHEN the higher priority call chip is added addOngoingCallState(key = callNotificationKey) - // THEN the higher priority call chip and notif are active in that order + // THEN the higher priority call chip and notif1 are active in that order assertThat(latest!!.active.size).isEqualTo(2) assertIsCallChip(latest!!.active[0], callNotificationKey, context) - assertIsNotifChip(latest!!.active[1], context, notifIcon, "notif") + assertIsNotifChip(latest!!.active[1], context, notif1Icon, "notif1") assertThat(latest!!.overflow).isEmpty() assertThat(latest!!.inactive.size).isEqualTo(3) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) @@ -1278,56 +1350,63 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { createTask(taskId = 1), ) - // THEN the higher priority media projection chip and call are active in that order, and - // notif is demoted to overflow - assertThat(latest!!.active.size).isEqualTo(2) + // THEN media projection, then call, then notif1 are active + assertThat(latest!!.active.size).isEqualTo(3) assertIsShareToAppChip(latest!!.active[0]) assertIsCallChip(latest!!.active[1], callNotificationKey, context) - assertThat(latest!!.overflow.size).isEqualTo(1) - assertIsNotifChip(latest!!.overflow[0], context, notifIcon, "notif") + assertIsNotifChip(latest!!.active[2], context, notif1Icon, "notif1") + assertThat(latest!!.overflow).isEmpty() assertThat(latest!!.inactive.size).isEqualTo(2) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) - // WHEN the higher priority screen record chip is added + // WHEN the screen record chip is added, which replaces media projection screenRecordState.value = ScreenRecordModel.Recording + // AND another notification is added + systemClock.advanceTime(2_000) + val notif2Icon = createStatusBarIconViewOrNull() + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif2", + statusBarChipIcon = notif2Icon, + promotedContent = PromotedNotificationContentModel.Builder("notif2").build(), + ) + ) - // THEN the higher priority screen record chip and call are active in that order, and - // media projection and notif are demoted in overflow - assertThat(latest!!.active.size).isEqualTo(2) + // THEN screen record, then call, then notif2 are active + assertThat(latest!!.active.size).isEqualTo(3) assertIsScreenRecordChip(latest!!.active[0]) assertIsCallChip(latest!!.active[1], callNotificationKey, context) + assertIsNotifChip(latest!!.active[2], context, notif2Icon, "notif2") + + // AND notif1 and media projection is demoted in overflow assertThat(latest!!.overflow.size).isEqualTo(2) assertIsShareToAppChip(latest!!.overflow[0]) - assertIsNotifChip(latest!!.overflow[1], context, notifIcon, "notif") + assertIsNotifChip(latest!!.overflow[1], context, notif1Icon, "notif1") assertThat(latest!!.inactive.size).isEqualTo(1) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) - // WHEN screen record and call is dropped + // WHEN screen record and call are dropped screenRecordState.value = ScreenRecordModel.DoingNothing - setNotifs( - listOf( - activeNotificationModel( - key = "notif", - statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), - ) - ) - ) + removeOngoingCallState(callNotificationKey) - // THEN media projection and notif remain - assertThat(latest!!.active.size).isEqualTo(2) + // THEN media projection, notif2, and notif1 remain + assertThat(latest!!.active.size).isEqualTo(3) assertIsShareToAppChip(latest!!.active[0]) - assertIsNotifChip(latest!!.active[1], context, notifIcon, "notif") + assertIsNotifChip(latest!!.active[1], context, notif2Icon, "notif2") + assertIsNotifChip(latest!!.active[2], context, notif1Icon, "notif1") assertThat(latest!!.overflow).isEmpty() assertThat(latest!!.inactive.size).isEqualTo(3) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) // WHEN media projection is dropped mediaProjectionState.value = MediaProjectionState.NotProjecting + // AND notif2 is dropped + systemClock.advanceTime(2_000) + activeNotificationListRepository.removeNotif("notif2") - // THEN only notif is active + // THEN only notif1 is active assertThat(latest!!.active.size).isEqualTo(1) - assertIsNotifChip(latest!!.active[0], context, notifIcon, "notif") + assertIsNotifChip(latest!!.active[0], context, notif1Icon, "notif1") assertThat(latest!!.overflow).isEmpty() assertThat(latest!!.inactive.size).isEqualTo(4) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationCloseButtonTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationCloseButtonTest.kt new file mode 100644 index 000000000000..e4b2e6c9b359 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationCloseButtonTest.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2025 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 + +import android.app.Notification +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.testing.TestableLooper +import android.view.MotionEvent +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.NotificationTestHelper +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.stub +import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import org.junit.Before + +private fun getCloseButton(row: ExpandableNotificationRow): View { + val contractedView = row.showingLayout?.contractedChild!! + return contractedView.findViewById(com.android.internal.R.id.close_button) +} + +@SmallTest +@RunWith(AndroidJUnit4::class) +class NotificationCloseButtonTest : SysuiTestCase() { + private lateinit var helper: NotificationTestHelper + + @Before + fun setUp() { + helper = NotificationTestHelper( + mContext, + mDependency, + TestableLooper.get(this) + ) + } + + @Test + @DisableFlags(Flags.FLAG_NOTIFICATION_ADD_X_ON_HOVER_TO_DISMISS) + fun verifyWhenFeatureDisabled() { + // Enable the notification row to dismiss. + helper.dismissibilityProvider.stub { + on { isDismissable(any()) } doReturn true + } + + // By default, the close button should be gone. + val row = createNotificationRow() + val closeButton = getCloseButton(row) + assertThat(closeButton).isNotNull() + assertThat(closeButton.visibility).isEqualTo(View.GONE) + + val hoverEnterEvent = MotionEvent.obtain( + 0/*downTime=*/, + 0/*eventTime=*/, + MotionEvent.ACTION_HOVER_ENTER, + 0f/*x=*/, + 0f/*y=*/, + 0/*metaState*/ + ) + + // The close button should not show if the feature is disabled. + row.onInterceptHoverEvent(hoverEnterEvent) + assertThat(closeButton.visibility).isEqualTo(View.GONE) + } + + @Test + @EnableFlags(Flags.FLAG_NOTIFICATION_ADD_X_ON_HOVER_TO_DISMISS) + fun verifyOnDismissableNotification() { + // Enable the notification row to dismiss. + helper.dismissibilityProvider.stub { + on { isDismissable(any()) } doReturn true + } + + // By default, the close button should be gone. + val row = createNotificationRow() + val closeButton = getCloseButton(row) + assertThat(closeButton).isNotNull() + assertThat(closeButton.visibility).isEqualTo(View.GONE) + + val hoverEnterEvent = MotionEvent.obtain( + 0/*downTime=*/, + 0/*eventTime=*/, + MotionEvent.ACTION_HOVER_ENTER, + 0f/*x=*/, + 0f/*y=*/, + 0/*metaState*/ + ) + + // When the row is hovered, the close button should show. + row.onInterceptHoverEvent(hoverEnterEvent) + assertThat(closeButton.visibility).isEqualTo(View.VISIBLE) + + val hoverExitEvent = MotionEvent.obtain( + 0/*downTime=*/, + 0/*eventTime=*/, + MotionEvent.ACTION_HOVER_EXIT, + 0f/*x=*/, + 0f/*y=*/, + 0/*metaState*/ + ) + + // When hover exits the row, the close button should be gone again. + row.onInterceptHoverEvent(hoverExitEvent) + assertThat(closeButton.visibility).isEqualTo(View.GONE) + } + + @Test + @EnableFlags(Flags.FLAG_NOTIFICATION_ADD_X_ON_HOVER_TO_DISMISS) + fun verifyOnUndismissableNotification() { + // By default, the close button should be gone. + val row = createNotificationRow() + val closeButton = getCloseButton(row) + assertThat(closeButton).isNotNull() + assertThat(closeButton.visibility).isEqualTo(View.GONE) + + val hoverEnterEvent = MotionEvent.obtain( + 0/*downTime=*/, + 0/*eventTime=*/, + MotionEvent.ACTION_HOVER_ENTER, + 0f/*x=*/, + 0f/*y=*/, + 0/*metaState*/ + ) + + // Because the host notification cannot be dismissed, the close button should not show. + row.onInterceptHoverEvent(hoverEnterEvent) + assertThat(closeButton.visibility).isEqualTo(View.GONE) + } + + private fun createNotificationRow(): ExpandableNotificationRow { + val notification = Notification.Builder(context, "channel") + .setContentTitle("title") + .setContentText("text") + .setSmallIcon(R.drawable.ic_person) + .build() + + return helper.createRow(notification) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt index bab349aa7a74..398f3fbecbd1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt @@ -92,6 +92,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { assertThat(model.intent).isSameInstanceAs(testIntent) assertThat(model.notificationKey).isEqualTo(key) assertThat(model.promotedContent).isSameInstanceAs(testPromotedContent) + assertThat(model.isAppVisible).isFalse() } @Test @@ -114,15 +115,16 @@ class OngoingCallInteractorTest : SysuiTestCase() { promotedContent = testPromotedContent, ) - // Verify model is InCallWithVisibleApp and has the correct icon, intent, and promoted - // content. - assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java) - val model = latest as OngoingCallModel.InCallWithVisibleApp + // Verify model is InCall with visible app and has the correct icon, intent, and + // promoted content. + assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) + val model = latest as OngoingCallModel.InCall assertThat(model.startTimeMs).isEqualTo(startTimeMs) assertThat(model.notificationIconView).isSameInstanceAs(testIconView) assertThat(model.intent).isSameInstanceAs(testIntent) assertThat(model.notificationKey).isEqualTo(key) assertThat(model.promotedContent).isSameInstanceAs(testPromotedContent) + assertThat(model.isAppVisible).isTrue() } @Test @@ -144,7 +146,8 @@ class OngoingCallInteractorTest : SysuiTestCase() { addOngoingCallState(uid = UID) - assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java) + assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((latest as OngoingCallModel.InCall).isAppVisible).isTrue() } @Test @@ -156,6 +159,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { addOngoingCallState(uid = UID) assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((latest as OngoingCallModel.InCall).isAppVisible).isFalse() } @Test @@ -167,14 +171,17 @@ class OngoingCallInteractorTest : SysuiTestCase() { kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false addOngoingCallState(uid = UID) assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((latest as OngoingCallModel.InCall).isAppVisible).isFalse() // App becomes visible kosmos.activityManagerRepository.fake.setIsAppVisible(UID, true) - assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java) + assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((latest as OngoingCallModel.InCall).isAppVisible).isTrue() // App becomes invisible again kosmos.activityManagerRepository.fake.setIsAppVisible(UID, false) assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((latest as OngoingCallModel.InCall).isAppVisible).isFalse() } @Test @@ -243,13 +250,14 @@ class OngoingCallInteractorTest : SysuiTestCase() { addOngoingCallState(uid = UID) assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((ongoingCallState as OngoingCallModel.InCall).isAppVisible).isFalse() assertThat(requiresStatusBarVisibleInRepository).isTrue() assertThat(requiresStatusBarVisibleInWindowController).isTrue() kosmos.activityManagerRepository.fake.setIsAppVisible(UID, true) - assertThat(ongoingCallState) - .isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java) + assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((ongoingCallState as OngoingCallModel.InCall).isAppVisible).isTrue() assertThat(requiresStatusBarVisibleInRepository).isFalse() assertThat(requiresStatusBarVisibleInWindowController).isFalse() } @@ -265,6 +273,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { addOngoingCallState() assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((ongoingCallState as OngoingCallModel.InCall).isAppVisible).isFalse() verify(kosmos.swipeStatusBarAwayGestureHandler, never()) .addOnGestureDetectedCallback(any(), any()) } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt index 029e54658f60..20ee6c120ee8 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt @@ -13,6 +13,7 @@ */ package com.android.systemui.plugins.clocks +import android.content.Context import android.graphics.Rect import com.android.systemui.plugins.annotations.ProtectedInterface @@ -60,4 +61,12 @@ data class ThemeConfig( * value denotes that we should use the seed color for the current system theme. */ val seedColor: Int?, -) +) { + fun getDefaultColor(context: Context): Int { + return when { + seedColor != null -> seedColor!! + isDarkTheme -> context.resources.getColor(android.R.color.system_accent1_100) + else -> context.resources.getColor(android.R.color.system_accent2_600) + } + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt index f920b187e7e5..f59dda049aa1 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt @@ -24,6 +24,8 @@ data class WeatherData( @VisibleForTesting const val TEMPERATURE_KEY = "temperature" private const val INVALID_WEATHER_ICON_STATE = -1 + @JvmStatic + @JvmOverloads fun fromBundle(extras: Bundle, touchAction: WeatherTouchAction? = null): WeatherData? { val description = extras.getString(DESCRIPTION_KEY) val state = @@ -46,7 +48,7 @@ data class WeatherData( state = state, useCelsius = extras.getBoolean(USE_CELSIUS_KEY), temperature = temperature, - touchAction = touchAction + touchAction = touchAction, ) if (DEBUG) { Log.i(TAG, "Weather data parsed $result from $extras") @@ -87,53 +89,53 @@ data class WeatherData( } // Values for WeatherStateIcon must stay in sync with go/g3-WeatherStateIcon - enum class WeatherStateIcon(val id: Int) { - UNKNOWN_ICON(0), + enum class WeatherStateIcon(val id: Int, val icon: String) { + UNKNOWN_ICON(0, ""), // Clear, day & night. - SUNNY(1), - CLEAR_NIGHT(2), + SUNNY(1, "a"), + CLEAR_NIGHT(2, "f"), // Mostly clear, day & night. - MOSTLY_SUNNY(3), - MOSTLY_CLEAR_NIGHT(4), + MOSTLY_SUNNY(3, "b"), + MOSTLY_CLEAR_NIGHT(4, "n"), // Partly cloudy, day & night. - PARTLY_CLOUDY(5), - PARTLY_CLOUDY_NIGHT(6), + PARTLY_CLOUDY(5, "b"), + PARTLY_CLOUDY_NIGHT(6, "n"), // Mostly cloudy, day & night. - MOSTLY_CLOUDY_DAY(7), - MOSTLY_CLOUDY_NIGHT(8), - CLOUDY(9), - HAZE_FOG_DUST_SMOKE(10), - DRIZZLE(11), - HEAVY_RAIN(12), - SHOWERS_RAIN(13), + MOSTLY_CLOUDY_DAY(7, "e"), + MOSTLY_CLOUDY_NIGHT(8, "e"), + CLOUDY(9, "e"), + HAZE_FOG_DUST_SMOKE(10, "d"), + DRIZZLE(11, "c"), + HEAVY_RAIN(12, "c"), + SHOWERS_RAIN(13, "c"), // Scattered showers, day & night. - SCATTERED_SHOWERS_DAY(14), - SCATTERED_SHOWERS_NIGHT(15), + SCATTERED_SHOWERS_DAY(14, "c"), + SCATTERED_SHOWERS_NIGHT(15, "c"), // Isolated scattered thunderstorms, day & night. - ISOLATED_SCATTERED_TSTORMS_DAY(16), - ISOLATED_SCATTERED_TSTORMS_NIGHT(17), - STRONG_TSTORMS(18), - BLIZZARD(19), - BLOWING_SNOW(20), - FLURRIES(21), - HEAVY_SNOW(22), + ISOLATED_SCATTERED_TSTORMS_DAY(16, "i"), + ISOLATED_SCATTERED_TSTORMS_NIGHT(17, "i"), + STRONG_TSTORMS(18, "i"), + BLIZZARD(19, "j"), + BLOWING_SNOW(20, "j"), + FLURRIES(21, "h"), + HEAVY_SNOW(22, "j"), // Scattered snow showers, day & night. - SCATTERED_SNOW_SHOWERS_DAY(23), - SCATTERED_SNOW_SHOWERS_NIGHT(24), - SNOW_SHOWERS_SNOW(25), - MIXED_RAIN_HAIL_RAIN_SLEET(26), - SLEET_HAIL(27), - TORNADO(28), - TROPICAL_STORM_HURRICANE(29), - WINDY_BREEZY(30), - WINTRY_MIX_RAIN_SNOW(31); + SCATTERED_SNOW_SHOWERS_DAY(23, "h"), + SCATTERED_SNOW_SHOWERS_NIGHT(24, "h"), + SNOW_SHOWERS_SNOW(25, "g"), + MIXED_RAIN_HAIL_RAIN_SLEET(26, "h"), + SLEET_HAIL(27, "h"), + TORNADO(28, "l"), + TROPICAL_STORM_HURRICANE(29, "m"), + WINDY_BREEZY(30, "k"), + WINTRY_MIX_RAIN_SNOW(31, "h"); companion object { fun fromInt(value: Int) = values().firstOrNull { it.id == value } diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 8342a9cc244b..19e05871c035 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -2166,7 +2166,7 @@ <dimen name="volume_dialog_background_top_margin">-28dp</dimen> <dimen name="volume_dialog_window_margin">14dp</dimen> - <dimen name="volume_dialog_components_spacing">8dp</dimen> + <dimen name="volume_dialog_components_spacing">10dp</dimen> <dimen name="volume_dialog_floating_sliders_spacing">8dp</dimen> <dimen name="volume_dialog_floating_sliders_vertical_padding">10dp</dimen> <dimen name="volume_dialog_floating_sliders_vertical_padding_negative"> diff --git a/packages/SystemUI/src/com/android/systemui/KairosActivatable.kt b/packages/SystemUI/src/com/android/systemui/KairosActivatable.kt index 5e29ba91ce42..442d4abb23f7 100644 --- a/packages/SystemUI/src/com/android/systemui/KairosActivatable.kt +++ b/packages/SystemUI/src/com/android/systemui/KairosActivatable.kt @@ -19,23 +19,28 @@ package com.android.systemui import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.BuildSpec import com.android.systemui.kairos.Events import com.android.systemui.kairos.EventsLoop import com.android.systemui.kairos.ExperimentalKairosApi import com.android.systemui.kairos.Incremental import com.android.systemui.kairos.IncrementalLoop import com.android.systemui.kairos.KairosNetwork +import com.android.systemui.kairos.RootKairosNetwork import com.android.systemui.kairos.State import com.android.systemui.kairos.StateLoop +import com.android.systemui.kairos.TransactionScope +import com.android.systemui.kairos.activateSpec +import com.android.systemui.kairos.effect import com.android.systemui.kairos.launchKairosNetwork import com.android.systemui.kairos.launchScope import dagger.Binds import dagger.Module -import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap import dagger.multibindings.Multibinds import javax.inject.Inject +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -176,21 +181,40 @@ private class KairosBuilderImpl @Inject constructor() : KairosBuilder { @SysUISingleton @ExperimentalKairosApi class KairosCoreStartable -@Inject -constructor( - @Application private val appScope: CoroutineScope, - private val kairosNetwork: KairosNetwork, +private constructor( + private val appScope: CoroutineScope, private val activatables: dagger.Lazy<Set<@JvmSuppressWildcards KairosActivatable>>, -) : CoreStartable { + private val unwrappedNetwork: RootKairosNetwork, +) : CoreStartable, KairosNetwork by unwrappedNetwork { + + @Inject + constructor( + @Application appScope: CoroutineScope, + activatables: dagger.Lazy<Set<@JvmSuppressWildcards KairosActivatable>>, + ) : this(appScope, activatables, appScope.launchKairosNetwork()) + + private val started = CompletableDeferred<Unit>() + override fun start() { appScope.launch { - kairosNetwork.activateSpec { + unwrappedNetwork.activateSpec { for (activatable in activatables.get()) { launchScope { activatable.run { activate() } } } + effect { started.complete(Unit) } } } } + + override suspend fun activateSpec(spec: BuildSpec<*>) { + started.await() + unwrappedNetwork.activateSpec(spec) + } + + override suspend fun <R> transact(block: TransactionScope.() -> R): R { + started.await() + return unwrappedNetwork.transact(block) + } } @Module @@ -203,10 +227,5 @@ interface KairosCoreStartableModule { @Multibinds fun kairosActivatables(): Set<@JvmSuppressWildcards KairosActivatable> - companion object { - @Provides - @SysUISingleton - fun provideKairosNetwork(@Application scope: CoroutineScope): KairosNetwork = - scope.launchKairosNetwork() - } + @Binds fun bindKairosNetwork(impl: KairosCoreStartable): KairosNetwork } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java index 08559f2eca8d..19caead525c9 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -63,7 +63,7 @@ import com.android.systemui.accessibility.hearingaid.HearingDevicesListAdapter.H import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.bluetooth.qsdialog.ActiveHearingDeviceItemFactory; import com.android.systemui.bluetooth.qsdialog.AvailableHearingDeviceItemFactory; -import com.android.systemui.bluetooth.qsdialog.ConnectedDeviceItemFactory; +import com.android.systemui.bluetooth.qsdialog.ConnectedHearingDeviceItemFactory; import com.android.systemui.bluetooth.qsdialog.DeviceItem; import com.android.systemui.bluetooth.qsdialog.DeviceItemFactory; import com.android.systemui.bluetooth.qsdialog.DeviceItemType; @@ -142,12 +142,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, private final List<DeviceItemFactory> mHearingDeviceItemFactoryList = List.of( new ActiveHearingDeviceItemFactory(), new AvailableHearingDeviceItemFactory(), - // TODO(b/331305850): setHearingAidInfo() for connected but not connect to profile - // hearing device only called from - // settings/bluetooth/DeviceListPreferenceFragment#handleLeScanResult, so we don't know - // it is connected but not yet connect to profile hearing device in systemui. - // Show all connected but not connect to profile bluetooth device for now. - new ConnectedDeviceItemFactory(), + new ConnectedHearingDeviceItemFactory(), new SavedHearingDeviceItemFactory() ); diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt index 858cc00b86b9..208e498126c1 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt @@ -214,7 +214,7 @@ internal class AvailableHearingDeviceItemFactory : AvailableMediaDeviceItemFacto } } -internal class ConnectedDeviceItemFactory : DeviceItemFactory() { +internal open class ConnectedDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, @@ -238,6 +238,19 @@ internal class ConnectedDeviceItemFactory : DeviceItemFactory() { } } +internal class ConnectedHearingDeviceItemFactory : ConnectedDeviceItemFactory() { + override fun isFilterMatched( + context: Context, + cachedDevice: CachedBluetoothDevice, + isOngoingCall: Boolean, + audioSharingAvailable: Boolean, + ): Boolean { + return cachedDevice.isHearingDevice && + cachedDevice.bondState == BluetoothDevice.BOND_BONDED && + cachedDevice.device.isConnected + } +} + internal open class SavedDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( context: Context, @@ -274,7 +287,7 @@ internal class SavedHearingDeviceItemFactory : SavedDeviceItemFactory() { context, cachedDevice.getDevice(), ) && - cachedDevice.isHearingAidDevice && + cachedDevice.isHearingDevice && cachedDevice.bondState == BluetoothDevice.BOND_BONDED && !cachedDevice.isConnected } 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 140db7b7a0b7..62a98d7a48ea 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 @@ -377,12 +377,15 @@ constructor( MutableStateFlow(false) } - val inAllowedKeyguardState = - keyguardTransitionInteractor.startedKeyguardTransitionStep.map { - it.to == KeyguardState.LOCKSCREEN || it.to == KeyguardState.GLANCEABLE_HUB - } - - allOf(inAllowedDeviceState, inAllowedKeyguardState) + if (v2FlagEnabled()) { + val inAllowedKeyguardState = + keyguardTransitionInteractor.startedKeyguardTransitionStep.map { + it.to == KeyguardState.LOCKSCREEN || it.to == KeyguardState.GLANCEABLE_HUB + } + allOf(inAllowedDeviceState, inAllowedKeyguardState) + } else { + inAllowedDeviceState + } } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index a25faa3a7aec..1ef3a950ba0d 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -36,6 +36,7 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerImpl; import com.android.systemui.doze.DozeHost; import com.android.systemui.education.dagger.ContextualEducationModule; +import com.android.systemui.effects.dagger.TopLevelWindowEffectsModule; import com.android.systemui.emergency.EmergencyGestureModule; import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialModule; import com.android.systemui.keyboard.shortcut.ShortcutHelperModule; @@ -166,6 +167,7 @@ import javax.inject.Named; SysUIUnfoldStartableModule.class, UnfoldTransitionModule.Startables.class, ToastModule.class, + TopLevelWindowEffectsModule.class, TouchpadTutorialModule.class, VolumeModule.class, WallpaperModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt index 69da67e055fe..1e7bec257432 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt @@ -68,10 +68,4 @@ constructor( emptyFlow() } } - - /** Triggered if a face failure occurs regardless of the mode. */ - val faceFailure: Flow<FailedFaceAuthenticationStatus> = - deviceEntryFaceAuthInteractor.authenticationStatus.filterIsInstance< - FailedFaceAuthenticationStatus - >() } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt index 38e0503440f9..09936839c590 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt @@ -22,7 +22,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardBypassInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.util.kotlin.FlowDumperImpl @@ -49,8 +48,6 @@ class DeviceEntryHapticsInteractor constructor( biometricSettingsRepository: BiometricSettingsRepository, deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor, - deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, - keyguardBypassInteractor: KeyguardBypassInteractor, deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, deviceEntrySourceInteractor: DeviceEntrySourceInteractor, fingerprintPropertyRepository: FingerprintPropertyRepository, @@ -83,7 +80,12 @@ constructor( emit(recentPowerButtonPressThresholdMs * -1L - 1L) } - private val playHapticsOnDeviceEntry: Flow<Boolean> = + /** + * Indicates when success haptics should play when the device is entered. This always occurs on + * successful fingerprint authentications. It also occurs on successful face authentication but + * only if the lockscreen is bypassed. + */ + val playSuccessHapticOnDeviceEntry: Flow<Unit> = deviceEntrySourceInteractor.deviceEntryFromBiometricSource .sample( combine( @@ -93,29 +95,17 @@ constructor( ::Triple, ) ) - .map { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) -> + .filter { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) -> val sideFpsAllowsHaptic = !powerButtonDown && systemClock.uptimeMillis() - lastPowerButtonWakeup > recentPowerButtonPressThresholdMs val allowHaptic = !sideFpsEnrolled || sideFpsAllowsHaptic if (!allowHaptic) { - logger.d( - "Skip success entry haptic from power button. Recent power button press or button is down." - ) + logger.d("Skip success haptic. Recent power button press or button is down.") } allowHaptic } - - private val playHapticsOnFaceAuthSuccessAndBypassDisabled: Flow<Boolean> = - deviceEntryFaceAuthInteractor.isAuthenticated - .filter { it } - .sample(keyguardBypassInteractor.isBypassAvailable) - .map { !it } - - val playSuccessHaptic: Flow<Unit> = - merge(playHapticsOnDeviceEntry, playHapticsOnFaceAuthSuccessAndBypassDisabled) - .filter { it } // map to Unit .map {} .dumpWhileCollecting("playSuccessHaptic") @@ -123,7 +113,7 @@ constructor( private val playErrorHapticForBiometricFailure: Flow<Unit> = merge( deviceEntryFingerprintAuthInteractor.fingerprintFailure, - deviceEntryBiometricAuthInteractor.faceFailure, + deviceEntryBiometricAuthInteractor.faceOnlyFaceFailure, ) // map to Unit .map {} diff --git a/packages/SystemUI/src/com/android/systemui/effects/TopLevelWindowEffects.kt b/packages/SystemUI/src/com/android/systemui/effects/TopLevelWindowEffects.kt new file mode 100644 index 000000000000..91530078e378 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/effects/TopLevelWindowEffects.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 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.effects; + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton; +import javax.inject.Inject + +@SysUISingleton +class TopLevelWindowEffects @Inject constructor() : CoreStartable { + override fun start() { + + } +} diff --git a/packages/SystemUI/src/com/android/systemui/effects/dagger/TopLevelWindowEffectsModule.kt b/packages/SystemUI/src/com/android/systemui/effects/dagger/TopLevelWindowEffectsModule.kt new file mode 100644 index 000000000000..70c4636e25c5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/effects/dagger/TopLevelWindowEffectsModule.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 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.effects.dagger + +import com.android.systemui.CoreStartable +import com.android.systemui.effects.TopLevelWindowEffects +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +interface TopLevelWindowEffectsModule { + + @Binds + @IntoMap + @ClassKey(TopLevelWindowEffects::class) + fun bindTopLevelWindowEffectsCoreStartable(impl: TopLevelWindowEffects): CoreStartable +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt b/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt index 9da9a7328659..32257df40d02 100644 --- a/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt @@ -18,7 +18,7 @@ package com.android.systemui.keyevent.data.repository import android.view.KeyEvent import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.CommandQueue import javax.inject.Inject @@ -29,6 +29,9 @@ import kotlinx.coroutines.flow.Flow interface KeyEventRepository { /** Observable for whether the power button key is pressed/down or not. */ val isPowerButtonDown: Flow<Boolean> + + /** Observable for when the power button is being pressed but till the duration of long press */ + val isPowerButtonLongPressed: Flow<Boolean> } @SysUISingleton @@ -51,6 +54,21 @@ constructor( awaitClose { commandQueue.removeCallback(callback) } } + override val isPowerButtonLongPressed: Flow<Boolean> = conflatedCallbackFlow { + val callback = + object : CommandQueue.Callbacks { + override fun handleSystemKey(event: KeyEvent) { + if (event.keyCode == KeyEvent.KEYCODE_POWER) { + trySendWithFailureLogging(event.action == KeyEvent.ACTION_DOWN + && event.isLongPress, TAG, "updated isPowerButtonLongPressed") + } + } + } + trySendWithFailureLogging(false, TAG, "init isPowerButtonLongPressed") + commandQueue.addCallback(callback) + awaitClose { commandQueue.removeCallback(callback) } + } + companion object { private const val TAG = "KeyEventRepositoryImpl" } diff --git a/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt index 9949fa589cd5..ec9bbfb1bc77 100644 --- a/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt @@ -32,4 +32,5 @@ constructor( repository: KeyEventRepository, ) { val isPowerButtonDown = repository.isPowerButtonDown + val isPowerButtonLongPressed = repository.isPowerButtonLongPressed } 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 45801ba3517a..aeb327035c79 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 @@ -333,7 +333,7 @@ object KeyguardRootViewBinder { if (deviceEntryHapticsInteractor != null && vibratorHelper != null) { launch { - deviceEntryHapticsInteractor.playSuccessHaptic.collect { + deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry.collect { if (msdlFeedback()) { msdlPlayer?.playToken( MSDLToken.UNLOCK, @@ -474,7 +474,7 @@ object KeyguardRootViewBinder { val transition = blueprintViewModel.currentTransition.value val shouldAnimate = transition != null && transition.config.type.animateNotifChanges if (prevTransition == transition && shouldAnimate) { - logger.w("Skipping; layout during transition") + logger.w("Skipping onNotificationContainerBoundsChanged during transition") return } 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 faa6c52162ce..a85b9b04c1ce 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -327,7 +327,8 @@ public class LogModule { @SysUISingleton @KeyguardBlueprintLog public static LogBuffer provideKeyguardBlueprintLog(LogBufferFactory factory) { - return factory.create("KeyguardBlueprintLog", 100); + // TODO(b/389987229): Reduce back to 100 + return factory.create("KeyguardBlueprintLog", 1000); } /** @@ -337,7 +338,8 @@ public class LogModule { @SysUISingleton @KeyguardClockLog public static LogBuffer provideKeyguardClockLog(LogBufferFactory factory) { - return factory.create("KeyguardClockLog", 100); + // TODO(b/389987229): Reduce back to 100 + return factory.create("KeyguardClockLog", 1000); } /** @@ -347,7 +349,8 @@ public class LogModule { @SysUISingleton @KeyguardSmallClockLog public static LogBuffer provideKeyguardSmallClockLog(LogBufferFactory factory) { - return factory.create("KeyguardSmallClockLog", 100); + // TODO(b/389987229): Reduce back to 100 + return factory.create("KeyguardSmallClockLog", 1000); } /** @@ -357,7 +360,8 @@ public class LogModule { @SysUISingleton @KeyguardLargeClockLog public static LogBuffer provideKeyguardLargeClockLog(LogBufferFactory factory) { - return factory.create("KeyguardLargeClockLog", 100); + // TODO(b/389987229): Reduce back to 100 + return factory.create("KeyguardLargeClockLog", 1000); } /** diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java index 9d375809786a..02dce406bbee 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java @@ -698,6 +698,27 @@ public class MediaSwitchingController List<MediaItem> finalMediaItems = targetMediaDevices.stream() .map(MediaItem::createDeviceMediaItem) .collect(Collectors.toList()); + + boolean shouldAddFirstSeenSelectedDevice = + com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping(); + + if (shouldAddFirstSeenSelectedDevice) { + finalMediaItems.clear(); + Set<String> selectedDevicesIds = getSelectedMediaDevice().stream() + .map(MediaDevice::getId) + .collect(Collectors.toSet()); + for (MediaDevice targetMediaDevice : targetMediaDevices) { + if (shouldAddFirstSeenSelectedDevice + && selectedDevicesIds.contains(targetMediaDevice.getId())) { + finalMediaItems.add(MediaItem.createDeviceMediaItem( + targetMediaDevice, /* isFirstDeviceInGroup */ true)); + shouldAddFirstSeenSelectedDevice = false; + } else { + finalMediaItems.add(MediaItem.createDeviceMediaItem( + targetMediaDevice, /* isFirstDeviceInGroup */ false)); + } + } + } dividerItems.forEach(finalMediaItems::add); attachConnectNewDeviceItemIfNeeded(finalMediaItems); return finalMediaItems; 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 4753b9ac0457..7bb831baec20 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 @@ -687,7 +687,7 @@ constructor( if (!isDeviceEntered) { coroutineScope { launch { - deviceEntryHapticsInteractor.playSuccessHaptic + deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry .sample(sceneInteractor.currentScene) .collect { currentScene -> if (Flags.msdlFeedback()) { diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java index eae0ba66925d..ade62a9b63d3 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java @@ -164,16 +164,17 @@ public class BrightnessDialog extends Activity { container.setVisibility(View.VISIBLE); ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) container.getLayoutParams(); - int horizontalMargin = - getResources().getDimensionPixelSize(R.dimen.notification_side_paddings); - lp.leftMargin = horizontalMargin; - lp.rightMargin = horizontalMargin; - - int verticalMargin = - getResources().getDimensionPixelSize( - R.dimen.notification_guts_option_vertical_padding); - - lp.topMargin = verticalMargin; + // Remove the margin. Have the container take all the space. Instead, insert padding. + // This allows for the background to be visible around the slider. + int margin = 0; + lp.topMargin = margin; + lp.bottomMargin = margin; + lp.leftMargin = margin; + lp.rightMargin = margin; + int padding = getResources().getDimensionPixelSize( + R.dimen.rounded_slider_background_padding + ); + container.setPadding(padding, padding, padding, padding); // If in multi-window or freeform, increase the top margin so the brightness dialog // doesn't get cut off. final int windowingMode = configuration.windowConfiguration.getWindowingMode(); @@ -182,17 +183,15 @@ public class BrightnessDialog extends Activity { lp.topMargin += 50; } - lp.bottomMargin = verticalMargin; - int orientation = configuration.orientation; int windowWidth = getWindowAvailableWidth(); if (orientation == Configuration.ORIENTATION_LANDSCAPE) { boolean shouldBeFullWidth = getIntent() .getBooleanExtra(EXTRA_BRIGHTNESS_DIALOG_IS_FULL_WIDTH, false); - lp.width = (shouldBeFullWidth ? windowWidth : windowWidth / 2) - horizontalMargin * 2; + lp.width = (shouldBeFullWidth ? windowWidth : windowWidth / 2) - margin * 2; } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { - lp.width = windowWidth - horizontalMargin * 2; + lp.width = windowWidth - margin * 2; } container.setLayoutParams(lp); @@ -202,7 +201,7 @@ public class BrightnessDialog extends Activity { // Exclude this view (and its horizontal margins) from triggering gestures. // This prevents back gesture from being triggered by dragging close to the // edge of the slider (0% or 100%). - bounds.set(-horizontalMargin, 0, right - left + horizontalMargin, bottom - top); + bounds.set(-margin, 0, right - left + margin, bottom - top); v.setSystemGestureExclusionRects(List.of(bounds)); }); } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt index 9e20055de856..22449c73c921 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt @@ -17,10 +17,12 @@ package com.android.systemui.settings.brightness import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.unit.dp import com.android.compose.theme.PlatformTheme import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel @@ -44,7 +46,7 @@ private fun BrightnessSliderForDialog( rememberViewModel(traceName = "BrightnessDialog.viewModel") { brightnessSliderViewModelFactory.create(false) } - BrightnessSliderContainer(viewModel = viewModel, Modifier.fillMaxWidth()) + BrightnessSliderContainer(viewModel = viewModel, Modifier.fillMaxWidth().padding(8.dp)) } class ComposableProvider( diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 305444f7ab5e..fa17b4fad592 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -30,8 +30,6 @@ import android.graphics.Region; import android.os.IBinder; import android.os.RemoteException; import android.os.Trace; -import android.os.UserHandle; -import android.provider.Settings; import android.util.Log; import android.view.Display; import android.view.IWindow; @@ -75,7 +73,6 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; -import com.android.systemui.util.settings.SecureSettings; import dagger.Lazy; @@ -134,7 +131,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW private final SysuiColorExtractor mColorExtractor; private final NotificationShadeWindowModel mNotificationShadeWindowModel; - private final SecureSettings mSecureSettings; /** * Layout params would be aggregated and dispatched all at once if this is > 0. * @@ -168,7 +164,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW Lazy<SelectedUserInteractor> userInteractor, UserTracker userTracker, NotificationShadeWindowModel notificationShadeWindowModel, - SecureSettings secureSettings, Lazy<CommunalInteractor> communalInteractor, @ShadeDisplayAware LayoutParams shadeWindowLayoutParams) { mContext = context; @@ -186,7 +181,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mBackgroundExecutor = backgroundExecutor; mColorExtractor = colorExtractor; mNotificationShadeWindowModel = notificationShadeWindowModel; - mSecureSettings = secureSettings; // prefix with {slow} to make sure this dumps at the END of the critical section. dumpManager.registerCriticalDumpable("{slow}NotificationShadeWindowControllerImpl", this); mAuthController = authController; @@ -424,7 +418,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW (long) mLpChanged.preferredMaxDisplayRefreshRate); } - if (state.bouncerShowing && !isSecureWindowsDisabled()) { + if (state.bouncerShowing) { mLpChanged.flags |= LayoutParams.FLAG_SECURE; } else { mLpChanged.flags &= ~LayoutParams.FLAG_SECURE; @@ -437,13 +431,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } } - private boolean isSecureWindowsDisabled() { - return mSecureSettings.getIntForUser( - Settings.Secure.DISABLE_SECURE_WINDOWS, - 0, - UserHandle.USER_CURRENT) == 1; - } - private void adjustScreenOrientation(NotificationShadeWindowState state) { if (state.bouncerShowing || state.isKeyguardShowingAndNotOccluded() || state.dozing) { if (mKeyguardStateController.isKeyguardScreenRotationAllowed()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt index be3afad4e1d1..16bf43bbf272 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.call.ui.viewmodel +import android.app.PendingIntent import android.content.Context import android.view.View import com.android.internal.jank.Cuj @@ -63,63 +64,69 @@ constructor( interactor.ongoingCallState .map { state -> when (state) { - is OngoingCallModel.NoCall, - is OngoingCallModel.InCallWithVisibleApp -> OngoingActivityChipModel.Inactive() - is OngoingCallModel.InCall -> { - val key = state.notificationKey - val contentDescription = getContentDescription(state.appName) - val icon = - if (state.notificationIconView != null) { - StatusBarConnectedDisplays.assertInLegacyMode() - OngoingActivityChipModel.ChipIcon.StatusBarView( - state.notificationIconView, - contentDescription, - ) - } else if (StatusBarConnectedDisplays.isEnabled) { - OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon( - state.notificationKey, - contentDescription, - ) - } else { - OngoingActivityChipModel.ChipIcon.SingleColorIcon(phoneIcon) - } - - val colors = ColorsModel.AccentThemed - - // This block mimics OngoingCallController#updateChip. - if (state.startTimeMs <= 0L) { - // If the start time is invalid, don't show a timer and show just an - // icon. See b/192379214. - OngoingActivityChipModel.Active.IconOnly( - key = key, - icon = icon, - colors = colors, - onClickListenerLegacy = getOnClickListener(state), - clickBehavior = getClickBehavior(state), - ) + is OngoingCallModel.NoCall -> OngoingActivityChipModel.Inactive() + is OngoingCallModel.InCall -> + if (state.isAppVisible) { + OngoingActivityChipModel.Inactive() } else { - val startTimeInElapsedRealtime = - state.startTimeMs - systemClock.currentTimeMillis() + - systemClock.elapsedRealtime() - OngoingActivityChipModel.Active.Timer( - key = key, - icon = icon, - colors = colors, - startTimeMs = startTimeInElapsedRealtime, - onClickListenerLegacy = getOnClickListener(state), - clickBehavior = getClickBehavior(state), - ) + prepareChip(state, systemClock) } - } } } .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Inactive()) - private fun getOnClickListener(state: OngoingCallModel.InCall): View.OnClickListener? { - if (state.intent == null) { - return null + /** Builds an [OngoingActivityChipModel.Active] from all the relevant information. */ + private fun prepareChip( + state: OngoingCallModel.InCall, + systemClock: SystemClock, + ): OngoingActivityChipModel.Active { + val key = state.notificationKey + val contentDescription = getContentDescription(state.appName) + val icon = + if (state.notificationIconView != null) { + StatusBarConnectedDisplays.assertInLegacyMode() + OngoingActivityChipModel.ChipIcon.StatusBarView( + state.notificationIconView, + contentDescription, + ) + } else if (StatusBarConnectedDisplays.isEnabled) { + OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon( + state.notificationKey, + contentDescription, + ) + } else { + OngoingActivityChipModel.ChipIcon.SingleColorIcon(phoneIcon) + } + + val colors = ColorsModel.AccentThemed + + // This block mimics OngoingCallController#updateChip. + if (state.startTimeMs <= 0L) { + // If the start time is invalid, don't show a timer and show just an icon. + // See b/192379214. + return OngoingActivityChipModel.Active.IconOnly( + key = key, + icon = icon, + colors = colors, + onClickListenerLegacy = getOnClickListener(state.intent), + clickBehavior = getClickBehavior(state.intent), + ) + } else { + val startTimeInElapsedRealtime = + state.startTimeMs - systemClock.currentTimeMillis() + systemClock.elapsedRealtime() + return OngoingActivityChipModel.Active.Timer( + key = key, + icon = icon, + colors = colors, + startTimeMs = startTimeInElapsedRealtime, + onClickListenerLegacy = getOnClickListener(state.intent), + clickBehavior = getClickBehavior(state.intent), + ) } + } + private fun getOnClickListener(intent: PendingIntent?): View.OnClickListener? { + if (intent == null) return null return View.OnClickListener { view -> StatusBarChipsModernization.assertInLegacyMode() logger.log(TAG, LogLevel.INFO, {}, { "Chip clicked" }) @@ -127,7 +134,7 @@ constructor( view.requireViewById<ChipBackgroundContainer>(R.id.ongoing_activity_chip_background) // This mimics OngoingCallController#updateChipClickListener. activityStarter.postStartActivityDismissingKeyguard( - state.intent, + intent, ActivityTransitionAnimator.Controller.fromView( backgroundView, Cuj.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP, @@ -136,10 +143,8 @@ constructor( } } - private fun getClickBehavior( - state: OngoingCallModel.InCall - ): OngoingActivityChipModel.ClickBehavior = - if (state.intent == null) { + private fun getClickBehavior(intent: PendingIntent?): OngoingActivityChipModel.ClickBehavior = + if (intent == null) { OngoingActivityChipModel.ClickBehavior.None } else { OngoingActivityChipModel.ClickBehavior.ExpandAction( @@ -149,10 +154,7 @@ constructor( expandable.activityTransitionController( Cuj.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP ) - activityStarter.postStartActivityDismissingKeyguard( - state.intent, - animationController, - ) + activityStarter.postStartActivityDismissingKeyguard(intent, animationController) } ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt index 02e4ba4d6437..6f8552738d33 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.chips.ui.binder import android.annotation.IdRes import android.content.Context import android.content.res.ColorStateList +import android.graphics.Typeface import android.graphics.drawable.GradientDrawable import android.view.View import android.view.ViewGroup @@ -27,6 +28,7 @@ import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import androidx.annotation.UiThread +import com.android.systemui.FontStyles import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.ui.binder.ContentDescriptionViewBinder import com.android.systemui.common.ui.binder.IconViewBinder @@ -170,6 +172,16 @@ object OngoingActivityChipBinder { forceLayout() } + /** Updates the typefaces for any text shown in the chip. */ + fun updateTypefaces(binding: OngoingActivityChipViewBinding) { + binding.timeView.typeface = + Typeface.create(FontStyles.GSF_LABEL_LARGE_EMPHASIZED, Typeface.NORMAL) + binding.textView.typeface = + Typeface.create(FontStyles.GSF_LABEL_LARGE_EMPHASIZED, Typeface.NORMAL) + binding.shortTimeDeltaView.typeface = + Typeface.create(FontStyles.GSF_LABEL_LARGE_EMPHASIZED, Typeface.NORMAL) + } + private fun setChipIcon( chipModel: OngoingActivityChipModel.Active, backgroundView: ChipBackgroundContainer, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt index ac55bf2bee1c..55d753662a65 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.ui.compose +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -44,6 +45,7 @@ import com.android.systemui.statusbar.chips.ui.viewmodel.rememberChronometerStat import com.android.systemui.statusbar.chips.ui.viewmodel.rememberTimeRemainingState import kotlin.math.min +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = Modifier) { val context = LocalContext.current @@ -52,7 +54,7 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = val hasEmbeddedIcon = viewModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarView || viewModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon - val textStyle = MaterialTheme.typography.labelLarge + val textStyle = MaterialTheme.typography.labelLargeEmphasized val textColor = Color(viewModel.colors.text(context)) val maxTextWidth = dimensionResource(id = R.dimen.ongoing_activity_chip_max_text_width) val startPadding = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt index d67cb8f81a6c..4edb23dc9f0e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt @@ -18,24 +18,18 @@ package com.android.systemui.statusbar.chips.ui.compose import android.content.res.ColorStateList import android.view.ViewGroup -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource @@ -43,7 +37,6 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.viewinterop.AndroidView import com.android.compose.animation.Expandable -import com.android.compose.modifiers.thenIf import com.android.systemui.animation.Expandable import com.android.systemui.common.ui.compose.Icon import com.android.systemui.common.ui.compose.load @@ -60,32 +53,45 @@ fun OngoingActivityChip( iconViewStore: NotificationIconContainerViewBinder.IconViewStore?, modifier: Modifier = Modifier, ) { - when (val clickBehavior = model.clickBehavior) { - is OngoingActivityChipModel.ClickBehavior.ExpandAction -> { - // Wrap the chip in an Expandable so we can animate the expand transition. - ExpandableChip( - color = { Color.Transparent }, - shape = - RoundedCornerShape( - dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius) - ), - modifier = modifier, - ) { expandable -> - ChipBody(model, iconViewStore, onClick = { clickBehavior.onClick(expandable) }) - } + val contentDescription = + when (val icon = model.icon) { + is OngoingActivityChipModel.ChipIcon.StatusBarView -> icon.contentDescription.load() + is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon -> + icon.contentDescription.load() + is OngoingActivityChipModel.ChipIcon.SingleColorIcon, + null -> null } - is OngoingActivityChipModel.ClickBehavior.ShowHeadsUpNotification -> { - ChipBody( - model, - iconViewStore, - onClick = { clickBehavior.onClick() }, - modifier = modifier, - ) + + val borderStroke = + model.colors.outline(LocalContext.current)?.let { + BorderStroke(dimensionResource(R.dimen.ongoing_activity_chip_outline_width), Color(it)) } - is OngoingActivityChipModel.ClickBehavior.None -> { - ChipBody(model, iconViewStore, modifier = modifier) + val onClick = + when (val clickBehavior = model.clickBehavior) { + is OngoingActivityChipModel.ClickBehavior.ExpandAction -> { expandable: Expandable -> + clickBehavior.onClick(expandable) + } + is OngoingActivityChipModel.ClickBehavior.ShowHeadsUpNotification -> { _ -> + clickBehavior.onClick() + } + is OngoingActivityChipModel.ClickBehavior.None -> null } + + Expandable( + color = Color(model.colors.background(LocalContext.current).defaultColor), + shape = + RoundedCornerShape(dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius)), + modifier = + modifier.height(dimensionResource(R.dimen.ongoing_appops_chip_height)).semantics { + if (contentDescription != null) { + this.contentDescription = contentDescription + } + }, + borderStroke = borderStroke, + onClick = onClick, + ) { + ChipBody(model, iconViewStore, isClickable = onClick != null) } } @@ -93,22 +99,13 @@ fun OngoingActivityChip( private fun ChipBody( model: OngoingActivityChipModel.Active, iconViewStore: NotificationIconContainerViewBinder.IconViewStore?, + isClickable: Boolean, modifier: Modifier = Modifier, - onClick: (() -> Unit)? = null, ) { - val context = LocalContext.current - val isClickable = onClick != null val hasEmbeddedIcon = model.icon is OngoingActivityChipModel.ChipIcon.StatusBarView || model.icon is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon - val contentDescription = - when (val icon = model.icon) { - is OngoingActivityChipModel.ChipIcon.StatusBarView -> icon.contentDescription.load() - is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon -> - icon.contentDescription.load() - is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> null - null -> null - } + val chipSidePadding = dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding) val minWidth = if (isClickable) { @@ -119,68 +116,38 @@ private fun ChipBody( dimensionResource(id = R.dimen.ongoing_activity_chip_min_text_width) + chipSidePadding } - val outline = model.colors.outline(context) - val outlineWidth = dimensionResource(R.dimen.ongoing_activity_chip_outline_width) - - val shape = - RoundedCornerShape(dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius)) - - // Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible - // height of the chip is determined by the height of the background of the Row below. - Box( - contentAlignment = Alignment.Center, + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, modifier = modifier .fillMaxHeight() - .clickable(enabled = isClickable, onClick = onClick ?: {}) - .semantics { - if (contentDescription != null) { - this.contentDescription = contentDescription - } - }, - ) { - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier.height(dimensionResource(R.dimen.ongoing_appops_chip_height)) - .thenIf(isClickable) { Modifier.widthIn(min = minWidth) } - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - layout(placeable.width, placeable.height) { - if (constraints.maxWidth >= minWidth.roundToPx()) { - placeable.place(0, 0) - } + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + layout(placeable.width, placeable.height) { + if (constraints.maxWidth >= minWidth.roundToPx()) { + placeable.place(0, 0) } } - .background(Color(model.colors.background(context).defaultColor), shape = shape) - .thenIf(outline != null) { - Modifier.border( - width = outlineWidth, - color = Color(outline!!), - shape = shape, - ) - } - .padding( - horizontal = - if (hasEmbeddedIcon) { - dimensionResource( - R.dimen - .ongoing_activity_chip_side_padding_for_embedded_padding_icon - ) - } else { - dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding) - } - ), - ) { - model.icon?.let { - ChipIcon(viewModel = it, iconViewStore = iconViewStore, colors = model.colors) - } + } + .padding( + horizontal = + if (hasEmbeddedIcon) { + dimensionResource( + R.dimen.ongoing_activity_chip_side_padding_for_embedded_padding_icon + ) + } else { + dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding) + } + ), + ) { + model.icon?.let { + ChipIcon(viewModel = it, iconViewStore = iconViewStore, colors = model.colors) + } - val isIconOnly = model is OngoingActivityChipModel.Active.IconOnly - if (!isIconOnly) { - ChipContent(viewModel = model) - } + val isIconOnly = model is OngoingActivityChipModel.Active.IconOnly + if (!isIconOnly) { + ChipContent(viewModel = model) } } } @@ -244,13 +211,3 @@ private fun StatusBarIcon( update = { iconView -> iconView.imageTintList = colorTintList }, ) } - -@Composable -private fun ExpandableChip( - color: () -> Color, - shape: Shape, - modifier: Modifier = Modifier, - content: @Composable (Expandable) -> Unit, -) { - Expandable(color = color(), shape = shape, modifier = modifier.clip(shape)) { content(it) } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt index 4954cb0a1b24..b2683762cb2a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.chips.ui.model import android.content.Context import android.content.res.ColorStateList import androidx.annotation.ColorInt -import com.android.settingslib.Utils import com.android.systemui.res.R /** Model representing how the chip in the status bar should be colored. */ @@ -34,14 +33,14 @@ sealed interface ColorsModel { @ColorInt fun outline(context: Context): Int? /** The chip should match the theme's primary accent color. */ - // TODO(b/347717946): The chip's color isn't getting updated when the user switches theme, it - // only gets updated when a different configuration change happens, like a rotation. data object AccentThemed : ColorsModel { override fun background(context: Context): ColorStateList = - Utils.getColorAttr(context, com.android.internal.R.attr.colorAccent) + ColorStateList.valueOf( + context.getColor(com.android.internal.R.color.materialColorPrimaryFixedDim) + ) override fun text(context: Context) = - Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary) + context.getColor(com.android.internal.R.color.materialColorOnPrimaryFixed) override fun outline(context: Context) = null } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index 0cfc3216beb5..8e470742f174 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.chips.ui.model +import android.annotation.CurrentTimeMillisLong +import android.annotation.ElapsedRealtimeLong import android.view.View import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription @@ -102,7 +104,7 @@ sealed class OngoingActivityChipModel { * [ChipChronometer] is based off of elapsed realtime. See * [android.widget.Chronometer.setBase]. */ - val startTimeMs: Long, + @ElapsedRealtimeLong val startTimeMs: Long, override val onClickListenerLegacy: View.OnClickListener?, override val clickBehavior: ClickBehavior, override val isHidden: Boolean = false, @@ -129,10 +131,15 @@ sealed class OngoingActivityChipModel { override val icon: ChipIcon, override val colors: ColorsModel, /** - * The time of the event that this chip represents, relative to - * [com.android.systemui.util.time.SystemClock.currentTimeMillis]. + * The time of the event that this chip represents. Relative to + * [com.android.systemui.util.time.SystemClock.currentTimeMillis] because that's what's + * required by [android.widget.DateTimeView]. + * + * TODO(b/372657935): When the Compose chips are launched, we should convert this to be + * relative to [com.android.systemui.util.time.SystemClock.elapsedRealtime] so that + * this model and the [Timer] model use the same units. */ - val time: Long, + @CurrentTimeMillisLong val time: Long, override val onClickListenerLegacy: View.OnClickListener?, override val clickBehavior: ClickBehavior, override val isHidden: Boolean = false, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt index eae2c25d77d8..8228b5533fca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt @@ -555,7 +555,6 @@ constructor( secondary = DEFAULT_INTERNAL_INACTIVE_MODEL, ) - // TODO(b/392886257): Support 3 chips if there's space available. - private const val MAX_VISIBLE_CHIPS = 2 + private const val MAX_VISIBLE_CHIPS = 3 } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt index eb6ebcaa5796..803d422c0f0f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.chips.ui.viewmodel -import android.os.SystemClock import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -39,7 +38,14 @@ import kotlinx.coroutines.delay * Manages state and updates for the duration remaining between now and a given time in the future. */ class TimeRemainingState(private val timeSource: TimeSource, private val futureTimeMillis: Long) { - private var durationRemaining by mutableStateOf(Duration.ZERO) + // Start with the right duration from the outset so we don't use "now" as an initial value. + private var durationRemaining by + mutableStateOf( + calculateDurationRemaining( + currentTimeMillis = timeSource.getCurrentTime(), + futureTimeMillis = futureTimeMillis, + ) + ) private var startTimeMillis: Long = 0 /** @@ -56,7 +62,11 @@ class TimeRemainingState(private val timeSource: TimeSource, private val futureT while (true) { val currentTime = timeSource.getCurrentTime() durationRemaining = - (futureTimeMillis - currentTime).toDuration(DurationUnit.MILLISECONDS) + calculateDurationRemaining( + currentTimeMillis = currentTime, + futureTimeMillis = futureTimeMillis, + ) + // No need to update if duration is more than 1 minute in the past. Because, we will // stop displaying anything. if (durationRemaining.inWholeMilliseconds < -1.minutes.inWholeMilliseconds) { @@ -67,6 +77,13 @@ class TimeRemainingState(private val timeSource: TimeSource, private val futureT } } + private fun calculateDurationRemaining( + currentTimeMillis: Long, + futureTimeMillis: Long, + ): Duration { + return (futureTimeMillis - currentTimeMillis).toDuration(DurationUnit.MILLISECONDS) + } + private fun calculateNextUpdateDelay(duration: Duration): Long { val durationAbsolute = duration.absoluteValue return when { @@ -85,7 +102,7 @@ class TimeRemainingState(private val timeSource: TimeSource, private val futureT @Composable fun rememberTimeRemainingState( futureTimeMillis: Long, - timeSource: TimeSource = remember { TimeSource { SystemClock.elapsedRealtime() } }, + timeSource: TimeSource = remember { TimeSource { System.currentTimeMillis() } }, ): TimeRemainingState { val state = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt index 7fa9f0e9dcc2..a658e1d55f7a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt @@ -267,7 +267,8 @@ constructor( if (StatusBarChipsModernization.isEnabled) { ongoingProcessRequiresStatusBarVisible } else { - ongoingCallStateLegacy is OngoingCallModel.InCall + ongoingCallStateLegacy is OngoingCallModel.InCall && + !ongoingCallStateLegacy.isAppVisible } val statusBarMode = toBarMode( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt index 1abd48c0c50b..a99ca072b6c8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt @@ -70,9 +70,6 @@ constructor( private fun OngoingCallModel.getNotifData(): NotifAndPromotedContent? = when (this) { is OngoingCallModel.InCall -> NotifAndPromotedContent(notificationKey, promotedContent) - is OngoingCallModel.InCallWithVisibleApp -> - // TODO(b/395989259): support InCallWithVisibleApp when it has notif data - null is OngoingCallModel.NoCall -> null } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 6837cb2a6292..76d8fb8d3c15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -129,8 +129,13 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } private void updateColors() { - if (usesTransparentBackground()) { - mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext()); + if (notificationRowTransparency()) { + if (mIsBlurSupported) { + mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext()); + } else { + mNormalColor = mContext.getColor( + com.android.internal.R.color.materialColorSurfaceContainer); + } } else { mNormalColor = mContext.getColor( com.android.internal.R.color.materialColorSurfaceContainerHigh); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index b6628926dc4b..61b7d80a8c85 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -25,7 +25,6 @@ import android.view.View import androidx.annotation.VisibleForTesting import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable -import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main @@ -162,6 +161,10 @@ constructor( notificationKey = currentInfo.key, appName = currentInfo.appName, promotedContent = currentInfo.promotedContent, + // [hasOngoingCall()] filters out the case in which the call is ongoing but the app + // is visible (we issue [OngoingCallModel.NoCall] below in that case), so this can + // be safely made false. + isAppVisible = false, ) } else { return OngoingCallModel.NoCall diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt index d6ca656356e3..bed9e7cf4646 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt @@ -51,9 +51,7 @@ import kotlinx.coroutines.flow.stateIn * This class monitors call notifications and the visibility of call apps to determine the * appropriate chip state. It emits: * * - [OngoingCallModel.NoCall] when there is no call notification - * * - [OngoingCallModel.InCallWithVisibleApp] when there is a call notification but the call app is - * visible - * * - [OngoingCallModel.InCall] when there is a call notification and the call app is not visible + * * - [OngoingCallModel.InCall] when there is a call notification */ @SysUISingleton class OngoingCallInteractor @@ -85,12 +83,14 @@ constructor( initialValue = OngoingCallModel.NoCall, ) + // TODO(b/400720280): maybe put this inside [OngoingCallModel]. @VisibleForTesting val isStatusBarRequiredForOngoingCall = combine(ongoingCallState, isChipSwipedAway) { callState, chipSwipedAway -> - callState is OngoingCallModel.InCall && !chipSwipedAway + callState.willCallChipBeVisible() && !chipSwipedAway } + // TODO(b/400720280): maybe put this inside [OngoingCallModel]. @VisibleForTesting val isGestureListeningEnabled = combine( @@ -98,9 +98,12 @@ constructor( statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode, isChipSwipedAway, ) { callState, isFullscreen, chipSwipedAway -> - callState is OngoingCallModel.InCall && !chipSwipedAway && isFullscreen + callState.willCallChipBeVisible() && !chipSwipedAway && isFullscreen } + private fun OngoingCallModel.willCallChipBeVisible() = + this is OngoingCallModel.InCall && !isAppVisible + private fun createOngoingCallStateFlow( notification: ActiveNotificationModel? ): Flow<OngoingCallModel> { @@ -147,34 +150,23 @@ constructor( model: ActiveNotificationModel, isVisible: Boolean, ): OngoingCallModel { - return when { - isVisible -> { - logger.d({ "Call app is visible: uid=$int1" }) { int1 = model.uid } - OngoingCallModel.InCallWithVisibleApp( - startTimeMs = model.whenTime, - notificationIconView = model.statusBarChipIconView, - intent = model.contentIntent, - notificationKey = model.key, - appName = model.appName, - promotedContent = model.promotedContent, - ) - } - - else -> { - logger.d({ "Active call detected: startTime=$long1 hasIcon=$bool1" }) { - long1 = model.whenTime - bool1 = model.statusBarChipIconView != null - } - OngoingCallModel.InCall( - startTimeMs = model.whenTime, - notificationIconView = model.statusBarChipIconView, - intent = model.contentIntent, - notificationKey = model.key, - appName = model.appName, - promotedContent = model.promotedContent, - ) - } + logger.d({ + "Active call detected: uid=$int1 startTime=$long1 hasIcon=$bool1 isAppVisible=$bool2" + }) { + int1 = model.uid + long1 = model.whenTime + bool1 = model.statusBarChipIconView != null + bool2 = isVisible } + return OngoingCallModel.InCall( + startTimeMs = model.whenTime, + notificationIconView = model.statusBarChipIconView, + intent = model.contentIntent, + notificationKey = model.key, + appName = model.appName, + promotedContent = model.promotedContent, + isAppVisible = isVisible, + ) } private fun setStatusBarRequiredForOngoingCall(statusBarRequired: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt index 62f0ba032f36..322dfff8f144 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt @@ -26,25 +26,6 @@ sealed interface OngoingCallModel { data object NoCall : OngoingCallModel /** - * There is an ongoing call but the call app is currently visible, so we don't need to show the - * chip. - * - * @property startTimeMs see [InCall.startTimeMs]. - * @property notificationIconView see [InCall.notificationIconView]. - * @property intent see [InCall.intent]. - * @property appName see [InCall.appName]. - * @property promotedContent see [InCall.promotedContent]. - */ - data class InCallWithVisibleApp( - val startTimeMs: Long, - val notificationIconView: StatusBarIconView?, - val intent: PendingIntent?, - val notificationKey: String, - val appName: String, - val promotedContent: PromotedNotificationContentModel?, - ) : OngoingCallModel - - /** * There *is* an ongoing call. * * @property startTimeMs the time that the phone call started, based on the notification's @@ -58,6 +39,7 @@ sealed interface OngoingCallModel { * @property appName the user-readable name of the app that posted the call notification. * @property promotedContent if the call notification also meets promoted notification criteria, * this field is filled in with the content related to promotion. Otherwise null. + * @property isAppVisible whether the app to which the call belongs is currently visible. */ data class InCall( val startTimeMs: Long, @@ -66,5 +48,6 @@ sealed interface OngoingCallModel { val notificationKey: String, val appName: String, val promotedContent: PromotedNotificationContentModel?, + val isAppVisible: Boolean, ) : OngoingCallModel } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt index cfd50973924d..6fada264e397 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt @@ -203,6 +203,8 @@ constructor( OngoingActivityChipBinder.createBinding( view.requireViewById(R.id.ongoing_activity_chip_secondary) ) + OngoingActivityChipBinder.updateTypefaces(primaryChipViewBinding) + OngoingActivityChipBinder.updateTypefaces(secondaryChipViewBinding) launch { combine( viewModel.ongoingActivityChipsLegacy, diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt index 720d5507e224..f6582a005035 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt @@ -72,10 +72,10 @@ fun Slider( valueRange: ClosedFloatingPointRange<Float>, onValueChanged: (Float) -> Unit, onValueChangeFinished: ((Float) -> Unit)?, - stepDistance: Float, isEnabled: Boolean, accessibilityParams: AccessibilityParams, modifier: Modifier = Modifier, + stepDistance: Float = 0f, colors: SliderColors = SliderDefaults.colors(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, haptics: Haptics = Haptics.Disabled, @@ -83,7 +83,7 @@ fun Slider( isReverseDirection: Boolean = false, track: (@Composable (SliderState) -> Unit)? = null, ) { - require(stepDistance > 0) { "stepDistance must be positive" } + require(stepDistance >= 0) { "stepDistance must not be negative" } val coroutineScope = rememberCoroutineScope() val snappedValue = snapValue(value, valueRange, stepDistance) val hapticsViewModel = haptics.createViewModel(snappedValue, valueRange, interactionSource) @@ -192,16 +192,20 @@ private fun AccessibilityParams.createSemantics( setProgress { targetValue -> val targetDirection = when { - targetValue > value -> 1 - targetValue < value -> -1 - else -> 0 + targetValue > value -> 1f + targetValue < value -> -1f + else -> 0f + } + val offset = + if (stepDistance > 0) { + // advance to the next step when stepDistance is > 0 + targetDirection * stepDistance + } else { + // advance to the desired value otherwise + targetValue - value } - val newValue = - (value + targetDirection * stepDistance).coerceIn( - valueRange.start, - valueRange.endInclusive, - ) + val newValue = (value + offset).coerceIn(valueRange.start, valueRange.endInclusive) onValueChanged(newValue) true } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index e8054c07eac8..8105ae0960ad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -283,7 +283,6 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { () -> mSelectedUserInteractor, mock(UserTracker.class), mKosmos.getNotificationShadeWindowModel(), - mSecureSettings, mKosmos::getCommunalInteractor, mKosmos.getShadeLayoutParams()); mFeatureFlags = new FakeFeatureFlags(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java index 88c2697fe2f2..5c26dac5eb30 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java @@ -1573,6 +1573,25 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { assertThat(items.get(1).isFirstDeviceInGroup()).isFalse(); } + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) + @Test + public void deviceListUpdateWithDifferentDevices_firstSelectedDeviceIsFirstDeviceInGroup() { + when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true); + doReturn(mMediaDevices) + .when(mLocalMediaManager) + .getSelectedMediaDevice(); + mMediaSwitchingController.start(mCb); + reset(mCb); + mMediaSwitchingController.getMediaItemList().clear(); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); + mMediaDevices.clear(); + mMediaDevices.add(mMediaDevice2); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); + + List<MediaItem> items = mMediaSwitchingController.getMediaItemList(); + assertThat(items.get(0).isFirstDeviceInGroup()).isTrue(); + } + private int getNumberOfConnectDeviceButtons() { int numberOfConnectDeviceButtons = 0; for (MediaItem item : mMediaSwitchingController.getMediaItemList()) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt index a64ff321cd4d..11aaeefe8214 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt @@ -134,10 +134,7 @@ class BrightnessDialogTest(val flags: FlagsParameterization) : SysuiTestCase() { val frame = activityRule.activity.requireViewById<View>(viewId) val lp = frame.layoutParams as ViewGroup.MarginLayoutParams - val horizontalMargin = - activityRule.activity.resources.getDimensionPixelSize( - R.dimen.notification_side_paddings - ) + val horizontalMargin = 0 assertThat(lp.leftMargin).isEqualTo(horizontalMargin) assertThat(lp.rightMargin).isEqualTo(horizontalMargin) 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 341bd3a38999..a978ecdb3534 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -33,6 +33,8 @@ import static com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR; import static com.google.common.truth.Truth.assertThat; +import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -55,8 +57,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; - import android.app.ActivityManager; import android.app.IActivityManager; import android.app.INotificationManager; @@ -136,7 +136,6 @@ import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.collection.GroupEntry; -import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; @@ -166,7 +165,6 @@ import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvision import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import com.android.systemui.util.FakeEventLog; import com.android.systemui.util.settings.FakeGlobalSettings; -import com.android.systemui.util.settings.FakeSettings; import com.android.systemui.util.settings.SystemSettings; import com.android.systemui.util.time.SystemClock; import com.android.wm.shell.Flags; @@ -451,7 +449,6 @@ public class BubblesTest extends SysuiTestCase { () -> mSelectedUserInteractor, mUserTracker, mNotificationShadeWindowModel, - new FakeSettings(), mKosmos::getCommunalInteractor, mKosmos.getShadeLayoutParams() ); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt index 6f570a86b19e..cd4b09c5267a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt @@ -21,7 +21,6 @@ import com.android.systemui.biometrics.data.repository.fingerprintPropertyReposi import com.android.systemui.dump.dumpManager import com.android.systemui.keyevent.domain.interactor.keyEventInteractor import com.android.systemui.keyguard.data.repository.biometricSettingsRepository -import com.android.systemui.keyguard.domain.interactor.keyguardBypassInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.util.time.systemClock @@ -31,8 +30,6 @@ val Kosmos.deviceEntryHapticsInteractor by DeviceEntryHapticsInteractor( biometricSettingsRepository = biometricSettingsRepository, deviceEntryBiometricAuthInteractor = deviceEntryBiometricAuthInteractor, - deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, - keyguardBypassInteractor = keyguardBypassInteractor, deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, deviceEntrySourceInteractor = deviceEntrySourceInteractor, fingerprintPropertyRepository = fingerprintPropertyRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt index 97dab4987f6c..807bc82cbf4e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt @@ -27,9 +27,16 @@ class FakeKeyEventRepository @Inject constructor() : KeyEventRepository { private val _isPowerButtonDown = MutableStateFlow(false) override val isPowerButtonDown: Flow<Boolean> = _isPowerButtonDown.asStateFlow() + private val _isPowerButtonLongPressed = MutableStateFlow(false) + override val isPowerButtonLongPressed = _isPowerButtonLongPressed.asStateFlow() + fun setPowerButtonDown(isDown: Boolean) { _isPowerButtonDown.value = isDown } + + fun setPowerButtonBeingLongPressed(isLongPressed: Boolean) { + _isPowerButtonLongPressed.value = isLongPressed + } } @Module diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt index e639326bd7a1..0e348c88f058 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt @@ -24,21 +24,23 @@ import org.junit.runners.model.Statement class LogWtfHandlerRule : TestRule { - private var started = false - private var handler = ThrowAndFailAtEnd + private var failureLogExemptions = mutableListOf<FailureLogExemption>() override fun apply(base: Statement, description: Description): Statement { return object : Statement() { override fun evaluate() { - started = true + val handler = TerribleFailureTestHandler() val originalWtfHandler = Log.setWtfHandler(handler) var failure: Throwable? = null try { base.evaluate() } catch (ex: Throwable) { - failure = ex.runAndAddSuppressed { handler.onTestFailure(ex) } + failure = ex } finally { - failure = failure.runAndAddSuppressed { handler.onTestFinished() } + failure = + runAndAddSuppressed(failure) { + handler.onTestFinished(failureLogExemptions) + } Log.setWtfHandler(originalWtfHandler) } if (failure != null) { @@ -48,74 +50,52 @@ class LogWtfHandlerRule : TestRule { } } - fun Throwable?.runAndAddSuppressed(block: () -> Unit): Throwable? { + /** Adds a log failure exemption. Exemptions are evaluated at the end of the test. */ + fun addFailureLogExemption(exemption: FailureLogExemption) { + failureLogExemptions.add(exemption) + } + + /** Clears and sets exemptions. Exemptions are evaluated at the end of the test. */ + fun resetFailureLogExemptions(vararg exemptions: FailureLogExemption) { + failureLogExemptions = exemptions.toMutableList() + } + + private fun runAndAddSuppressed(currentError: Throwable?, block: () -> Unit): Throwable? { try { block() } catch (t: Throwable) { - if (this == null) { + if (currentError == null) { return t } - addSuppressed(t) + currentError.addSuppressed(t) } - return this + return currentError } - fun setWtfHandler(handler: TerribleFailureTestHandler) { - check(!started) { "Should only be called before the test starts" } - this.handler = handler - } - - fun interface TerribleFailureTestHandler : TerribleFailureHandler { - fun onTestFailure(failure: Throwable) {} - fun onTestFinished() {} - } - - companion object Handlers { - val ThrowAndFailAtEnd - get() = - object : TerribleFailureTestHandler { - val failures = mutableListOf<Log.TerribleFailure>() - - override fun onTerribleFailure( - tag: String, - what: Log.TerribleFailure, - system: Boolean - ) { - failures.add(what) - throw what - } + private class TerribleFailureTestHandler : TerribleFailureHandler { + private val failureLogs = mutableListOf<FailureLog>() - override fun onTestFailure(failure: Throwable) { - super.onTestFailure(failure) - } + override fun onTerribleFailure(tag: String, what: Log.TerribleFailure, system: Boolean) { + failureLogs.add(FailureLog(tag = tag, failure = what, system = system)) + } - override fun onTestFinished() { - if (failures.isNotEmpty()) { - throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0]) - } - } + fun onTestFinished(exemptions: List<FailureLogExemption>) { + val failures = + failureLogs.filter { failureLog -> + !exemptions.any { it.isFailureLogExempt(failureLog) } } + if (failures.isNotEmpty()) { + throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0].failure) + } + } + } - val JustThrow = TerribleFailureTestHandler { _, what, _ -> throw what } - - val JustFailAtEnd - get() = - object : TerribleFailureTestHandler { - val failures = mutableListOf<Log.TerribleFailure>() - - override fun onTerribleFailure( - tag: String, - what: Log.TerribleFailure, - system: Boolean - ) { - failures.add(what) - } + /** All the information from a call to [Log.wtf] that was handed to [TerribleFailureHandler] */ + data class FailureLog(val tag: String, val failure: Log.TerribleFailure, val system: Boolean) - override fun onTestFinished() { - if (failures.isNotEmpty()) { - throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0]) - } - } - } + /** An interface for exempting a [FailureLog] from causing a test failure. */ + fun interface FailureLogExemption { + /** Determines whether a log should be except from failing the test. */ + fun isFailureLogExempt(log: FailureLog): Boolean } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt index 8ff7c7d01fb3..08cfd9f385d3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt @@ -38,6 +38,7 @@ fun inCallModel( notificationKey: String = "test", appName: String = "", promotedContent: PromotedNotificationContentModel? = null, + isAppVisible: Boolean = false, ) = OngoingCallModel.InCall( startTimeMs, @@ -46,6 +47,7 @@ fun inCallModel( notificationKey, appName, promotedContent, + isAppVisible, ) object OngoingCallTestHelper { diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index 0f6f86b39458..739ea0df87ab 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -25,6 +25,7 @@ import static android.view.accessibility.AccessibilityManager.AUTOCLICK_IGNORE_M import static com.android.server.accessibility.autoclick.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AutoclickType; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.ClickPanelControllerInterface; @@ -87,6 +88,7 @@ public class AutoclickController extends BaseEventStreamTransformation { @VisibleForTesting AutoclickIndicatorScheduler mAutoclickIndicatorScheduler; @VisibleForTesting AutoclickIndicatorView mAutoclickIndicatorView; @VisibleForTesting AutoclickTypePanel mAutoclickTypePanel; + @VisibleForTesting AutoclickScrollPanel mAutoclickScrollPanel; private WindowManager mWindowManager; // Default click type is left-click. @@ -98,6 +100,11 @@ public class AutoclickController extends BaseEventStreamTransformation { @Override public void handleAutoclickTypeChange(@AutoclickType int clickType) { mActiveClickType = clickType; + + // Hide scroll panel when type is not scroll. + if (clickType != AUTOCLICK_TYPE_SCROLL && mAutoclickScrollPanel != null) { + mAutoclickScrollPanel.hide(); + } } @Override @@ -161,6 +168,7 @@ public class AutoclickController extends BaseEventStreamTransformation { mWindowManager = mContext.getSystemService(WindowManager.class); mAutoclickTypePanel = new AutoclickTypePanel(mContext, mWindowManager, mUserId, clickPanelController); + mAutoclickScrollPanel = new AutoclickScrollPanel(mContext, mWindowManager); mAutoclickTypePanel.show(); mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams()); @@ -189,6 +197,10 @@ public class AutoclickController extends BaseEventStreamTransformation { mClickScheduler.cancel(); } + if (mAutoclickScrollPanel != null) { + mAutoclickScrollPanel.hide(); + } + super.clearEvents(inputSource); } @@ -209,6 +221,11 @@ public class AutoclickController extends BaseEventStreamTransformation { mWindowManager.removeView(mAutoclickIndicatorView); mAutoclickTypePanel.hide(); } + + if (mAutoclickScrollPanel != null) { + mAutoclickScrollPanel.hide(); + mAutoclickScrollPanel = null; + } } private void handleMouseMotion(MotionEvent event, int policyFlags) { @@ -687,6 +704,14 @@ public class AutoclickController extends BaseEventStreamTransformation { return; } + // Handle scroll type specially, show scroll panel instead of sending click events. + if (mActiveClickType == AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL) { + if (mAutoclickScrollPanel != null) { + mAutoclickScrollPanel.show(); + } + return; + } + final int pointerIndex = mLastMotionEvent.getActionIndex(); if (mTempPointerProperties == null) { diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java new file mode 100644 index 000000000000..86f79a83ea28 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java @@ -0,0 +1,105 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility.autoclick; + +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + +import android.content.Context; +import android.graphics.PixelFormat; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowInsets; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.R; + +public class AutoclickScrollPanel { + private final Context mContext; + private final View mContentView; + private final WindowManager mWindowManager; + private boolean mInScrollMode = false; + + public AutoclickScrollPanel(Context context, WindowManager windowManager) { + mContext = context; + mWindowManager = windowManager; + mContentView = LayoutInflater.from(context).inflate( + R.layout.accessibility_autoclick_scroll_panel, null); + } + + /** + * Shows the autoclick scroll panel. + */ + public void show() { + if (mInScrollMode) { + return; + } + mWindowManager.addView(mContentView, getLayoutParams()); + mInScrollMode = true; + } + + /** + * Hides the autoclick scroll panel. + */ + public void hide() { + if (!mInScrollMode) { + return; + } + mWindowManager.removeView(mContentView); + mInScrollMode = false; + } + + /** + * Retrieves the layout params for AutoclickScrollPanel, used when it's added to the Window + * Manager. + */ + @NonNull + private WindowManager.LayoutParams getLayoutParams() { + final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); + layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; + layoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + layoutParams.setFitInsetsTypes(WindowInsets.Type.statusBars()); + layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + layoutParams.format = PixelFormat.TRANSLUCENT; + layoutParams.setTitle(AutoclickScrollPanel.class.getSimpleName()); + layoutParams.accessibilityTitle = + mContext.getString(R.string.accessibility_autoclick_scroll_panel_title); + layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; + layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; + layoutParams.gravity = Gravity.CENTER; + return layoutParams; + } + + @VisibleForTesting + public boolean isVisible() { + return mInScrollMode; + } + + @VisibleForTesting + public View getContentViewForTesting() { + return mContentView; + } + + @VisibleForTesting + public WindowManager.LayoutParams getLayoutParamsForTesting() { + return getLayoutParams(); + } +} diff --git a/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java b/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java index 9a353fbc45bf..867cd51e1c2b 100644 --- a/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java +++ b/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java @@ -44,6 +44,7 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.LocalServices; +import com.android.server.backup.BackupRestoreTask.CancellationReason; import com.android.server.backup.internal.LifecycleOperationStorage; import java.util.Set; @@ -298,20 +299,22 @@ public class BackupAgentConnectionManager { // Offload operation cancellation off the main thread as the cancellation callbacks // might call out to BackupTransport. Other operations started on the same package // before the cancellation callback has executed will also be cancelled by the callback. - Runnable cancellationRunnable = () -> { - // handleCancel() causes the PerformFullTransportBackupTask to go on to - // tearDownAgentAndKill: that will unbindBackupAgent in the Activity Manager, so - // that the package being backed up doesn't get stuck in restricted mode until the - // backup time-out elapses. - for (int token : mOperationStorage.operationTokensForPackage(packageName)) { - if (DEBUG) { - Slog.d(TAG, - mUserIdMsg + "agentDisconnected: will handleCancel(all) for token:" - + Integer.toHexString(token)); - } - mUserBackupManagerService.handleCancel(token, true /* cancelAll */); - } - }; + Runnable cancellationRunnable = + () -> { + // On handleCancel(), the operation will call unbindAgent() which will make + // sure the app doesn't get stuck in restricted mode. + for (int token : mOperationStorage.operationTokensForPackage(packageName)) { + if (DEBUG) { + Slog.d( + TAG, + mUserIdMsg + + "agentDisconnected: cancelling for token:" + + Integer.toHexString(token)); + } + mUserBackupManagerService.handleCancel( + token, CancellationReason.AGENT_DISCONNECTED); + } + }; getThreadForCancellation(cancellationRunnable).start(); mAgentConnectLock.notifyAll(); diff --git a/services/backup/java/com/android/server/backup/BackupRestoreTask.java b/services/backup/java/com/android/server/backup/BackupRestoreTask.java index acaab0c54191..7ec5f0d786ed 100644 --- a/services/backup/java/com/android/server/backup/BackupRestoreTask.java +++ b/services/backup/java/com/android/server/backup/BackupRestoreTask.java @@ -16,9 +16,12 @@ package com.android.server.backup; -/** - * Interface and methods used by the asynchronous-with-timeout backup/restore operations. - */ +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Interface and methods used by the asynchronous-with-timeout backup/restore operations. */ public interface BackupRestoreTask { // Execute one tick of whatever state machine the task implements @@ -27,6 +30,24 @@ public interface BackupRestoreTask { // An operation that wanted a callback has completed void operationComplete(long result); - // An operation that wanted a callback has timed out - void handleCancel(boolean cancelAll); + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + CancellationReason.TIMEOUT, + CancellationReason.AGENT_DISCONNECTED, + CancellationReason.EXTERNAL, + CancellationReason.SCHEDULED_JOB_STOPPED, + }) + @interface CancellationReason { + // The task timed out. + int TIMEOUT = 0; + // The agent went away before the task was able to finish (e.g. due to an app crash). + int AGENT_DISCONNECTED = 1; + // An external caller cancelled the operation (e.g. via BackupManager#cancelBackups). + int EXTERNAL = 2; + // The job scheduler has stopped an ongoing scheduled backup pass. + int SCHEDULED_JOB_STOPPED = 3; + } + + /** The task is cancelled for the given {@link CancellationReason}. */ + void handleCancel(@CancellationReason int cancellationReason); } diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java index 2143aaaa4cd6..b3af444ff9bd 100644 --- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java +++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java @@ -102,6 +102,7 @@ import com.android.internal.util.Preconditions; import com.android.server.AppWidgetBackupBridge; import com.android.server.EventLogTags; import com.android.server.LocalServices; +import com.android.server.backup.BackupRestoreTask.CancellationReason; import com.android.server.backup.OperationStorage.OpState; import com.android.server.backup.OperationStorage.OpType; import com.android.server.backup.fullbackup.FullBackupEntry; @@ -168,6 +169,7 @@ import java.util.Random; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntConsumer; /** System service that performs backup/restore operations. */ public class UserBackupManagerService { @@ -1816,11 +1818,9 @@ public class UserBackupManagerService { for (Integer token : operationsToCancel) { mOperationStorage.cancelOperation( - token, /* cancelAll */ - true, - operationType -> { - /* no callback needed here */ - }); + token, + operationType -> {}, // no callback needed here + CancellationReason.EXTERNAL); } // We don't want the backup jobs to kick in any time soon. // Reschedules them to run in the distant future. @@ -1897,19 +1897,17 @@ public class UserBackupManagerService { } /** Cancel the operation associated with {@code token}. */ - public void handleCancel(int token, boolean cancelAll) { + public void handleCancel(int token, @CancellationReason int cancellationReason) { // Remove all pending timeout messages of types OpType.BACKUP_WAIT and // OpType.RESTORE_WAIT. On the other hand, OP_TYPE_BACKUP cannot time out and // doesn't require cancellation. - mOperationStorage.cancelOperation( - token, - cancelAll, - operationType -> { - if (operationType == OpType.BACKUP_WAIT - || operationType == OpType.RESTORE_WAIT) { - mBackupHandler.removeMessages(getMessageIdForOperationType(operationType)); + IntConsumer timeoutCallback = + opType -> { + if (opType == OpType.BACKUP_WAIT || opType == OpType.RESTORE_WAIT) { + mBackupHandler.removeMessages(getMessageIdForOperationType(opType)); } - }); + }; + mOperationStorage.cancelOperation(token, timeoutCallback, cancellationReason); } /** Returns {@code true} if a backup is currently running, else returns {@code false}. */ @@ -2219,20 +2217,17 @@ public class UserBackupManagerService { // offload the mRunningFullBackupTask.handleCancel() call to another thread, // as we might have to wait for mCancelLock Runnable endFullBackupRunnable = - new Runnable() { - @Override - public void run() { - PerformFullTransportBackupTask pftbt = null; - synchronized (mQueueLock) { - if (mRunningFullBackupTask != null) { - pftbt = mRunningFullBackupTask; - } - } - if (pftbt != null) { - Slog.i(TAG, mLogIdMsg + "Telling running backup to stop"); - pftbt.handleCancel(true); + () -> { + PerformFullTransportBackupTask pftbt = null; + synchronized (mQueueLock) { + if (mRunningFullBackupTask != null) { + pftbt = mRunningFullBackupTask; } } + if (pftbt != null) { + Slog.i(TAG, mLogIdMsg + "Telling running backup to stop"); + pftbt.handleCancel(CancellationReason.SCHEDULED_JOB_STOPPED); + } }; new Thread(endFullBackupRunnable, "end-full-backup").start(); } diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java index 0d4364e14e03..7fc9ed3e0213 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java +++ b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java @@ -484,7 +484,7 @@ public class PerformAdbBackupTask extends FullBackupTask implements BackupRestor } @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { final PackageInfo target = mCurrentTarget; Slog.w(TAG, "adb backup cancel of " + target); if (target != null) { diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java index c182c2618fdf..f677c9dbf4d0 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java +++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java @@ -162,7 +162,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba // This is true when a backup operation for some package is in progress. private volatile boolean mIsDoingBackup; - private volatile boolean mCancelAll; + private volatile boolean mCancelled; private final int mCurrentOpToken; private final BackupAgentTimeoutParameters mAgentTimeoutParameters; private final BackupEligibilityRules mBackupEligibilityRules; @@ -199,7 +199,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba if (backupManagerService.isBackupOperationInProgress()) { Slog.d(TAG, "Skipping full backup. A backup is already in progress."); - mCancelAll = true; + mCancelled = true; return; } @@ -287,25 +287,23 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { synchronized (mCancelLock) { - // We only support 'cancelAll = true' case for this task. Cancelling of a single package - - // due to timeout is handled by SinglePackageBackupRunner and + // This callback is only used for cancelling the entire backup operation. Cancelling of + // a single package due to timeout is handled by SinglePackageBackupRunner and // SinglePackageBackupPreflight. - - if (!cancelAll) { - Slog.wtf(TAG, "Expected cancelAll to be true."); + if (cancellationReason == CancellationReason.TIMEOUT) { + Slog.wtf(TAG, "This task cannot time out"); } - if (mCancelAll) { + if (mCancelled) { Slog.d(TAG, "Ignoring duplicate cancel call."); return; } - mCancelAll = true; + mCancelled = true; if (mIsDoingBackup) { - mUserBackupManagerService.handleCancel(mBackupRunnerOpToken, cancelAll); + mUserBackupManagerService.handleCancel(mBackupRunnerOpToken, cancellationReason); try { // If we're running a backup we should be connected to a transport BackupTransportClient transport = @@ -410,7 +408,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba int backupPackageStatus; long quota = Long.MAX_VALUE; synchronized (mCancelLock) { - if (mCancelAll) { + if (mCancelled) { break; } backupPackageStatus = transport.performFullBackup(currentPackage, @@ -478,7 +476,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba if (nRead > 0) { out.write(buffer, 0, nRead); synchronized (mCancelLock) { - if (!mCancelAll) { + if (!mCancelled) { backupPackageStatus = transport.sendBackupData(nRead); } } @@ -509,7 +507,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba synchronized (mCancelLock) { mIsDoingBackup = false; // If mCancelCurrent is true, we have already called cancelFullBackup(). - if (!mCancelAll) { + if (!mCancelled) { if (backupRunnerResult == BackupTransport.TRANSPORT_OK) { // If we were otherwise in a good state, now interpret the final // result based on what finishBackup() returns. If we're in a @@ -607,7 +605,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba .sendBackupOnPackageResult(mBackupObserver, packageName, BackupManager.ERROR_BACKUP_CANCELLED); Slog.w(TAG, "Backup cancelled. package=" + packageName + - ", cancelAll=" + mCancelAll); + ", entire session cancelled=" + mCancelled); EventLog.writeEvent(EventLogTags.FULL_BACKUP_CANCELLED, packageName); mUserBackupManagerService.getBackupAgentConnectionManager().unbindAgent( currentPackage.applicationInfo, /* allowKill= */ true); @@ -654,7 +652,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } finally { - if (mCancelAll) { + if (mCancelled) { backupRunStatus = BackupManager.ERROR_BACKUP_CANCELLED; } @@ -820,7 +818,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { if (DEBUG) { Slog.i(TAG, "Preflight cancelled; failing"); } @@ -974,7 +972,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba public void operationComplete(long result) { /* intentionally empty */ } @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { Slog.w(TAG, "Full backup cancel of " + mTarget.packageName); mBackupManagerMonitorEventSender.monitorEvent( @@ -984,7 +982,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba /* extras= */ null); mIsCancelled = true; // Cancel tasks spun off by this task. - mUserBackupManagerService.handleCancel(mEphemeralToken, cancelAll); + mUserBackupManagerService.handleCancel(mEphemeralToken, cancellationReason); mUserBackupManagerService.getBackupAgentConnectionManager().unbindAgent( mTarget.applicationInfo, /* allowKill= */ true); // Free up everyone waiting on this task and its children. diff --git a/services/backup/java/com/android/server/backup/internal/BackupHandler.java b/services/backup/java/com/android/server/backup/internal/BackupHandler.java index 87cf8a313651..464dc2dfe1ec 100644 --- a/services/backup/java/com/android/server/backup/internal/BackupHandler.java +++ b/services/backup/java/com/android/server/backup/internal/BackupHandler.java @@ -34,6 +34,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.server.EventLogTags; import com.android.server.backup.BackupAgentTimeoutParameters; import com.android.server.backup.BackupRestoreTask; +import com.android.server.backup.BackupRestoreTask.CancellationReason; import com.android.server.backup.DataChangedJournal; import com.android.server.backup.OperationStorage; import com.android.server.backup.TransportManager; @@ -410,8 +411,8 @@ public class BackupHandler extends Handler { case MSG_BACKUP_OPERATION_TIMEOUT: case MSG_RESTORE_OPERATION_TIMEOUT: { - Slog.d(TAG, "Timeout message received for token=" + Integer.toHexString(msg.arg1)); - backupManagerService.handleCancel(msg.arg1, false); + Slog.d(TAG, "Timeout for token=" + Integer.toHexString(msg.arg1)); + backupManagerService.handleCancel(msg.arg1, CancellationReason.TIMEOUT); break; } diff --git a/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java b/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java index 0b974e2d0a8a..5aacb2f4f007 100644 --- a/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java +++ b/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java @@ -24,6 +24,7 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.server.backup.BackupRestoreTask; +import com.android.server.backup.BackupRestoreTask.CancellationReason; import com.android.server.backup.OperationStorage; import com.google.android.collect.Sets; @@ -296,20 +297,18 @@ public class LifecycleOperationStorage implements OperationStorage { } /** - * Cancel the operation associated with {@code token}. Cancellation may be - * propagated to the operation's callback (a {@link BackupRestoreTask}) if - * the operation has one, and the cancellation is due to the operation - * timing out. + * Cancel the operation associated with {@code token}. Cancellation may be propagated to the + * operation's callback (a {@link BackupRestoreTask}) if the operation has one, and the + * cancellation is due to the operation timing out. * * @param token the operation token specified when registering the operation - * @param cancelAll this is passed on when propagating the cancellation - * @param operationTimedOutCallback a lambda that is invoked with the - * operation type where the operation is - * cancelled due to timeout, allowing the - * caller to do type-specific clean-ups. + * @param operationTimedOutCallback a lambda that is invoked with the operation type where the + * operation is cancelled due to timeout, allowing the caller to do type-specific clean-ups. */ public void cancelOperation( - int token, boolean cancelAll, IntConsumer operationTimedOutCallback) { + int token, + IntConsumer operationTimedOutCallback, + @CancellationReason int cancellationReason) { // Notify any synchronous waiters Operation op = null; synchronized (mOperationsLock) { @@ -343,7 +342,7 @@ public class LifecycleOperationStorage implements OperationStorage { if (DEBUG) { Slog.v(TAG, "[UserID:" + mUserId + " Invoking cancel on " + op.callback); } - op.callback.handleCancel(cancelAll); + op.callback.handleCancel(cancellationReason); } } } diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java index 494b9d59a238..8e7a23ccbb25 100644 --- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java +++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java @@ -171,7 +171,7 @@ import java.util.concurrent.atomic.AtomicInteger; * complete backup should be performed. * * <p>This task is designed to run on a dedicated thread, with the exception of the {@link - * #handleCancel(boolean)} method, which can be called from any thread. + * BackupRestoreTask#handleCancel(int)} method, which can be called from any thread. */ // TODO: Stop poking into BMS state and doing things for it (e.g. synchronizing on public locks) // TODO: Consider having the caller responsible for some clean-up (like resetting state) @@ -1208,13 +1208,13 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { * * <p>Note: This method is inherently racy since there are no guarantees about how much of the * task will be executed after you made the call. - * - * @param cancelAll MUST be {@code true}. Will be removed. */ @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { // This is called in a thread different from the one that executes method run(). - Preconditions.checkArgument(cancelAll, "Can't partially cancel a key-value backup task"); + Preconditions.checkArgument( + cancellationReason != CancellationReason.TIMEOUT, + "Key-value backup task cannot time out"); markCancel(); waitCancel(); } diff --git a/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java b/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java index cb491c6f384e..f1829b6966a8 100644 --- a/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java +++ b/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java @@ -79,7 +79,7 @@ public class AdbRestoreFinishedLatch implements BackupRestoreTask { } @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { Slog.w(TAG, "adb onRestoreFinished() timed out"); mLatch.countDown(); mOperationStorage.removeOperation(mCurrentOpToken); diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java index 707ae03b3964..1263146fe405 100644 --- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java +++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java @@ -1307,7 +1307,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // The app has timed out handling a restoring file @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { mOperationStorage.removeOperation(mEphemeralOpToken); Slog.w(TAG, "Full-data restore target timed out; shutting down"); Bundle monitoringExtras = addRestoreOperationTypeToEvent(/* extras= */ null); @@ -1555,7 +1555,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // A call to agent.doRestore() or agent.doRestoreFinished() has timed out @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { mOperationStorage.removeOperation(mEphemeralOpToken); Slog.e(TAG, "Timeout restoring application " + mCurrentPackage.packageName); Bundle monitoringExtras = addRestoreOperationTypeToEvent(/* extras= */ null); diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index 93b4de856463..caf535ce7a40 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -142,14 +142,6 @@ public class VirtualDeviceManagerService extends SystemService { @GuardedBy("mVirtualDeviceManagerLock") private ArrayMap<String, AssociationInfo> mActiveAssociations = new ArrayMap<>(); - private final CompanionDeviceManager.OnAssociationsChangedListener mCdmAssociationListener = - new CompanionDeviceManager.OnAssociationsChangedListener() { - @Override - public void onAssociationsChanged(@NonNull List<AssociationInfo> associations) { - syncVirtualDevicesToCdmAssociations(associations); - } - }; - private class StrongAuthTracker extends LockPatternUtils.StrongAuthTracker { final Set<Integer> mUsersInLockdown = new ArraySet<>(); @@ -348,33 +340,6 @@ public class VirtualDeviceManagerService extends SystemService { return true; } - private void syncVirtualDevicesToCdmAssociations(List<AssociationInfo> associations) { - Set<VirtualDeviceImpl> virtualDevicesToRemove = new HashSet<>(); - synchronized (mVirtualDeviceManagerLock) { - if (mVirtualDevices.size() == 0) { - return; - } - - Set<Integer> activeAssociationIds = new HashSet<>(associations.size()); - for (AssociationInfo association : associations) { - activeAssociationIds.add(association.getId()); - } - - for (int i = 0; i < mVirtualDevices.size(); i++) { - VirtualDeviceImpl virtualDevice = mVirtualDevices.valueAt(i); - int deviceAssociationId = virtualDevice.getAssociationId(); - if (deviceAssociationId != CDM_ASSOCIATION_ID_NONE - && !activeAssociationIds.contains(deviceAssociationId)) { - virtualDevicesToRemove.add(virtualDevice); - } - } - } - - for (VirtualDeviceImpl virtualDevice : virtualDevicesToRemove) { - virtualDevice.close(); - } - } - void onCdmAssociationsChanged(List<AssociationInfo> associations) { ArrayMap<String, AssociationInfo> vdmAssociations = new ArrayMap<>(); for (int i = 0; i < associations.size(); ++i) { diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java index 96b30d4e1285..d60c6c534871 100644 --- a/services/core/java/com/android/server/Watchdog.java +++ b/services/core/java/com/android/server/Watchdog.java @@ -165,7 +165,6 @@ public class Watchdog implements Dumpable { "android.hardware.sensors@1.0::ISensors", "android.hardware.sensors@2.0::ISensors", "android.hardware.sensors@2.1::ISensors", - "android.hardware.vibrator@1.0::IVibrator", "android.hardware.vr@1.0::IVr", "android.system.suspend@1.0::ISystemSuspend" ); diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 3a041fd3b38a..cfd22fbdeece 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -187,10 +187,12 @@ public class SettingsToPropertiesMapper { "crumpet", "dck_framework", "desktop_apps", + "desktop_audio", "desktop_better_together", "desktop_bsp", "desktop_camera", "desktop_connectivity", + "desktop_dev_experience", "desktop_display", "desktop_commercial", "desktop_firmware", @@ -199,10 +201,14 @@ public class SettingsToPropertiesMapper { "desktop_input", "desktop_kernel", "desktop_ml", + "desktop_networking", "desktop_serviceability", "desktop_oobe", "desktop_peripherals", + "desktop_personalization", "desktop_pnp", + "desktop_privacy", + "desktop_release", "desktop_security", "desktop_stats", "desktop_sysui", diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java index 9dd09cef88f9..40085ed89294 100644 --- a/services/core/java/com/android/server/appop/AttributedOp.java +++ b/services/core/java/com/android/server/appop/AttributedOp.java @@ -113,7 +113,7 @@ final class AttributedOp { mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, parent.packageName, persistentDeviceId, tag, uidState, flags, accessTime, AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE, - DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP, accessCount); + accessCount); } /** @@ -257,8 +257,7 @@ final class AttributedOp { if (isStarted) { mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, parent.packageName, persistentDeviceId, tag, uidState, flags, startTime, - attributionFlags, attributionChainId, - DiscreteOpsRegistry.ACCESS_TYPE_START_OP, 1); + attributionFlags, attributionChainId, 1); } } @@ -344,9 +343,7 @@ final class AttributedOp { mAppOpsService.mHistoricalRegistry.increaseOpAccessDuration(parent.op, parent.uid, parent.packageName, persistentDeviceId, tag, event.getUidState(), event.getFlags(), finishedEvent.getNoteTime(), finishedEvent.getDuration(), - event.getAttributionFlags(), event.getAttributionChainId(), - isPausing ? DiscreteOpsRegistry.ACCESS_TYPE_PAUSE_OP - : DiscreteOpsRegistry.ACCESS_TYPE_FINISH_OP); + event.getAttributionFlags(), event.getAttributionChainId()); if (!isPausing) { mAppOpsService.mInProgressStartOpEventPool.release(event); @@ -454,7 +451,7 @@ final class AttributedOp { mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, parent.packageName, persistentDeviceId, tag, event.getUidState(), event.getFlags(), startTime, event.getAttributionFlags(), - event.getAttributionChainId(), DiscreteOpsRegistry.ACCESS_TYPE_RESUME_OP, 1); + event.getAttributionChainId(), 1); if (shouldSendActive) { mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid, parent.packageName, tag, event.getVirtualDeviceId(), true, diff --git a/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java index 12c35ae92cbe..b7599f6e40c3 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java @@ -46,22 +46,17 @@ import static android.app.AppOpsManager.OP_WRITE_SMS; import static java.lang.Long.min; import static java.lang.Math.max; -import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppOpsManager; import android.os.AsyncTask; import android.os.Build; -import android.permission.flags.Flags; import android.provider.DeviceConfig; import android.util.Slog; import com.android.internal.util.ArrayUtils; -import com.android.internal.util.FrameworkStatsLog; import java.io.PrintWriter; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.text.SimpleDateFormat; import java.time.Duration; import java.util.Date; @@ -134,27 +129,6 @@ abstract class DiscreteOpsRegistry { boolean mDebugMode = false; - static final int ACCESS_TYPE_NOTE_OP = - FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__NOTE_OP; - static final int ACCESS_TYPE_START_OP = - FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__START_OP; - static final int ACCESS_TYPE_FINISH_OP = - FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__FINISH_OP; - static final int ACCESS_TYPE_PAUSE_OP = - FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__PAUSE_OP; - static final int ACCESS_TYPE_RESUME_OP = - FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__RESUME_OP; - - @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = {"ACCESS_TYPE_"}, value = { - ACCESS_TYPE_NOTE_OP, - ACCESS_TYPE_START_OP, - ACCESS_TYPE_FINISH_OP, - ACCESS_TYPE_PAUSE_OP, - ACCESS_TYPE_RESUME_OP - }) - @interface AccessType {} - void systemReady() { DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY, AsyncTask.THREAD_POOL_EXECUTOR, (DeviceConfig.Properties p) -> { @@ -166,8 +140,7 @@ abstract class DiscreteOpsRegistry { abstract void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op, @Nullable String attributionTag, @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState, long accessTime, long accessDuration, - @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId, - @DiscreteOpsRegistry.AccessType int accessType); + @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId); /** * A periodic callback from {@link AppOpsService} to flush the in memory events to disk. @@ -227,9 +200,6 @@ abstract class DiscreteOpsRegistry { return true; } - // could this be impl detail of discrete registry, just one test is using the method - // abstract DiscreteRegistry.DiscreteOps getAllDiscreteOps(); - private void setDiscreteHistoryParameters(DeviceConfig.Properties p) { if (p.getKeyset().contains(PROPERTY_DISCRETE_HISTORY_CUTOFF)) { sDiscreteHistoryCutoff = p.getLong(PROPERTY_DISCRETE_HISTORY_CUTOFF, @@ -277,28 +247,4 @@ abstract class DiscreteOpsRegistry { } return result; } - - /** - * Whether app op access tacking is enabled and a metric event should be logged. - */ - static boolean shouldLogAccess(int op) { - return Flags.appopAccessTrackingLoggingEnabled() - && ArrayUtils.contains(sDiscreteOpsToLog, op); - } - - String getAttributionTag(String attributionTag, String packageName) { - if (attributionTag == null || packageName == null) { - return attributionTag; - } - int firstChar = 0; - if (attributionTag.startsWith(packageName)) { - firstChar = packageName.length(); - if (firstChar < attributionTag.length() && attributionTag.charAt(firstChar) - == '.') { - firstChar++; - } - } - return attributionTag.substring(firstChar); - } - } diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java index dc11be9aadb6..c897891d02c3 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java @@ -36,7 +36,6 @@ import android.util.IntArray; import android.util.LongSparseArray; import android.util.Slog; -import com.android.internal.util.FrameworkStatsLog; import com.android.server.ServiceThread; import java.io.File; @@ -97,15 +96,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op, @Nullable String attributionTag, int flags, int uidState, - long accessTime, long accessDuration, int attributionFlags, int attributionChainId, - int accessType) { - if (shouldLogAccess(op)) { - FrameworkStatsLog.write(FrameworkStatsLog.APP_OP_ACCESS_TRACKED, uid, op, accessType, - uidState, flags, attributionFlags, - getAttributionTag(attributionTag, packageName), - attributionChainId); - } - + long accessTime, long accessDuration, int attributionFlags, int attributionChainId) { if (!isDiscreteOp(op, flags)) { return; } diff --git a/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java b/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java index 1523cca86607..909a04c44ae5 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java @@ -48,15 +48,13 @@ class DiscreteOpsTestingShim extends DiscreteOpsRegistry { @Override void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op, @Nullable String attributionTag, int flags, int uidState, long accessTime, - long accessDuration, int attributionFlags, int attributionChainId, int accessType) { + long accessDuration, int attributionFlags, int attributionChainId) { long start = SystemClock.uptimeMillis(); mXmlRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, attributionTag, flags, - uidState, accessTime, accessDuration, attributionFlags, attributionChainId, - accessType); + uidState, accessTime, accessDuration, attributionFlags, attributionChainId); long start2 = SystemClock.uptimeMillis(); mSqlRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, attributionTag, flags, - uidState, accessTime, accessDuration, attributionFlags, attributionChainId, - accessType); + uidState, accessTime, accessDuration, attributionFlags, attributionChainId); long end = SystemClock.uptimeMillis(); long xmlTimeTaken = start2 - start; long sqlTimeTaken = end - start2; diff --git a/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java index a6e3fc7cc66a..20706b659ffb 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java @@ -45,7 +45,6 @@ import android.util.Xml; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.ArrayUtils; -import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -159,15 +158,7 @@ class DiscreteOpsXmlRegistry extends DiscreteOpsRegistry { void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op, @Nullable String attributionTag, @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState, long accessTime, long accessDuration, - @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId, - @AccessType int accessType) { - if (shouldLogAccess(op)) { - FrameworkStatsLog.write(FrameworkStatsLog.APP_OP_ACCESS_TRACKED, uid, op, accessType, - uidState, flags, attributionFlags, - getAttributionTag(attributionTag, packageName), - attributionChainId); - } - + @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) { if (!isDiscreteOp(op, flags)) { return; } diff --git a/services/core/java/com/android/server/appop/HistoricalRegistry.java b/services/core/java/com/android/server/appop/HistoricalRegistry.java index d267e0d9e536..06e43e8ec68d 100644 --- a/services/core/java/com/android/server/appop/HistoricalRegistry.java +++ b/services/core/java/com/android/server/appop/HistoricalRegistry.java @@ -497,7 +497,7 @@ final class HistoricalRegistry { @NonNull String deviceId, @Nullable String attributionTag, @UidState int uidState, @OpFlags int flags, long accessTime, @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId, - @DiscreteOpsRegistry.AccessType int accessType, int accessCount) { + int accessCount) { synchronized (mInMemoryLock) { if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) { if (!isPersistenceInitializedMLocked()) { @@ -510,7 +510,7 @@ final class HistoricalRegistry { mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, attributionTag, flags, uidState, accessTime, -1, attributionFlags, - attributionChainId, accessType); + attributionChainId); } } } @@ -533,8 +533,7 @@ final class HistoricalRegistry { void increaseOpAccessDuration(int op, int uid, @NonNull String packageName, @NonNull String deviceId, @Nullable String attributionTag, @UidState int uidState, @OpFlags int flags, long eventStartTime, long increment, - @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId, - @DiscreteOpsRegistry.AccessType int accessType) { + @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) { synchronized (mInMemoryLock) { if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) { if (!isPersistenceInitializedMLocked()) { @@ -546,7 +545,7 @@ final class HistoricalRegistry { attributionTag, uidState, flags, increment); mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, attributionTag, flags, uidState, eventStartTime, increment, - attributionFlags, attributionChainId, accessType); + attributionFlags, attributionChainId); } } } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 8ef79a916530..4b5f06b13885 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -1472,8 +1472,8 @@ public class AudioDeviceBroker { mAudioService.postAccessoryPlugMediaUnmute(device); } - /*package*/ int getVssVolumeForDevice(int streamType, int device) { - return mAudioService.getVssVolumeForDevice(streamType, device); + /*package*/ int getVolumeForDeviceIgnoreMute(int streamType, int device) { + return mAudioService.getVolumeForDeviceIgnoreMute(streamType, device); } /*package*/ int getMaxVssVolumeForStream(int streamType) { diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 829d9ea7495f..2e6d98485e85 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -2482,7 +2482,7 @@ public class AudioDeviceInventory { @GuardedBy("mDevicesLock") private void makeHearingAidDeviceAvailable( String address, String name, int streamType, String eventSource) { - final int hearingAidVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType, + final int hearingAidVolIndex = mDeviceBroker.getVolumeForDeviceIgnoreMute(streamType, DEVICE_OUT_HEARING_AID); mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType); @@ -2672,7 +2672,7 @@ public class AudioDeviceInventory { } final int leAudioVolIndex = (volumeIndex == -1) - ? mDeviceBroker.getVssVolumeForDevice(streamType, device) + ? mDeviceBroker.getVolumeForDeviceIgnoreMute(streamType, device) : volumeIndex; final int maxIndex = mDeviceBroker.getMaxVssVolumeForStream(streamType); mDeviceBroker.postSetLeAudioVolumeIndex(leAudioVolIndex, maxIndex, streamType); diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index ada1cd73f775..a43e4d98c077 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -529,7 +529,7 @@ public class AudioService extends IAudioService.Stub */ private InputDeviceVolumeHelper mInputDeviceVolumeHelper; - /*package*/ int getVssVolumeForDevice(int stream, int device) { + /*package*/ int getVolumeForDeviceIgnoreMute(int stream, int device) { final VolumeStreamState streamState = mStreamStates.get(stream); return streamState != null ? streamState.getIndex(device) : -1; } @@ -5098,7 +5098,7 @@ public class AudioService extends IAudioService.Stub } final int device = absVolumeDevices.toArray(new Integer[0])[0].intValue(); - final int index = getStreamVolume(streamType, device); + final int index = getVolumeForDeviceIgnoreMute(streamType, device); if (DEBUG_VOL) { Slog.i(TAG, "onUpdateContextualVolumes streamType: " + streamType diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java index 643f3308d8f5..67afff79dffd 100644 --- a/services/core/java/com/android/server/audio/SoundDoseHelper.java +++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java @@ -724,7 +724,7 @@ public class SoundDoseHelper { int device = mAudioService.getDeviceForStream(AudioSystem.STREAM_MUSIC); if (safeDevicesContains(device) && isStreamActive) { scheduleMusicActiveCheck(); - int index = mAudioService.getVssVolumeForDevice(AudioSystem.STREAM_MUSIC, + int index = mAudioService.getVolumeForDeviceIgnoreMute(AudioSystem.STREAM_MUSIC, device); if (index > safeMediaVolumeIndex(device)) { // Approximate cumulative active music time diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 7016c11b69e7..a28069bbf050 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -674,9 +674,9 @@ public final class DisplayManagerService extends SystemService { mConfigParameterProvider = new DeviceConfigParameterProvider(DeviceConfigInterface.REAL); mExtraDisplayLoggingPackageName = DisplayProperties.debug_vri_package().orElse(null); mExtraDisplayEventLogging = !TextUtils.isEmpty(mExtraDisplayLoggingPackageName); - + // TODO(b/400384229): stats service needs to react to mirror-extended switch mExternalDisplayStatsService = new ExternalDisplayStatsService(mContext, mHandler, - this::isExtendedDisplayEnabled); + this::isExtendedDisplayAllowed); mDisplayNotificationManager = new DisplayNotificationManager(mFlags, mContext, mExternalDisplayStatsService); mExternalDisplayPolicy = new ExternalDisplayPolicy(new ExternalDisplayPolicyInjector()); @@ -690,7 +690,7 @@ public final class DisplayManagerService extends SystemService { deliverTopologyUpdate(update.first); }; mDisplayTopologyCoordinator = new DisplayTopologyCoordinator( - this::isExtendedDisplayEnabled, topologyChangedCallback, + this::isExtendedDisplayAllowed, topologyChangedCallback, new HandlerExecutor(mHandler), mSyncRoot, backupManager::dataChanged); } else { mDisplayTopologyCoordinator = null; @@ -2411,7 +2411,10 @@ public final class DisplayManagerService extends SystemService { updateLogicalDisplayState(display); } - private boolean isExtendedDisplayEnabled() { + private boolean isExtendedDisplayAllowed() { + if (mFlags.isDisplayContentModeManagementEnabled()) { + return true; + } try { return 0 != Settings.Global.getInt( mContext.getContentResolver(), @@ -6045,7 +6048,13 @@ public final class DisplayManagerService extends SystemService { return; } if (inTopology) { - mDisplayTopologyCoordinator.onDisplayAdded(getDisplayInfo(displayId)); + var info = getDisplayInfo(displayId); + if (info == null) { + Slog.w(TAG, "onDisplayBelongToTopologyChanged: cancelled displayId=" + + displayId + " info=null"); + return; + } + mDisplayTopologyCoordinator.onDisplayAdded(info); } else { mDisplayTopologyCoordinator.onDisplayRemoved(displayId); } diff --git a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java index 997fff58b952..b4df1f76dccb 100644 --- a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java +++ b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java @@ -69,9 +69,9 @@ class DisplayTopologyCoordinator { private final SparseArray<String> mDisplayIdToUniqueIdMapping = new SparseArray<>(); /** - * Check if extended displays are enabled. If not, a topology is not needed. + * Check if extended displays are allowed. If not, a topology is not needed. */ - private final BooleanSupplier mIsExtendedDisplayEnabled; + private final BooleanSupplier mIsExtendedDisplayAllowed; /** * Callback used to send topology updates. @@ -83,21 +83,21 @@ class DisplayTopologyCoordinator { private final DisplayManagerService.SyncRoot mSyncRoot; private final Runnable mTopologySavedCallback; - DisplayTopologyCoordinator(BooleanSupplier isExtendedDisplayEnabled, + DisplayTopologyCoordinator(BooleanSupplier isExtendedDisplayAllowed, Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> onTopologyChangedCallback, Executor topologyChangeExecutor, DisplayManagerService.SyncRoot syncRoot, Runnable topologySavedCallback) { - this(new Injector(), isExtendedDisplayEnabled, onTopologyChangedCallback, + this(new Injector(), isExtendedDisplayAllowed, onTopologyChangedCallback, topologyChangeExecutor, syncRoot, topologySavedCallback); } @VisibleForTesting - DisplayTopologyCoordinator(Injector injector, BooleanSupplier isExtendedDisplayEnabled, + DisplayTopologyCoordinator(Injector injector, BooleanSupplier isExtendedDisplayAllowed, Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> onTopologyChangedCallback, Executor topologyChangeExecutor, DisplayManagerService.SyncRoot syncRoot, Runnable topologySavedCallback) { mTopology = injector.getTopology(); - mIsExtendedDisplayEnabled = isExtendedDisplayEnabled; + mIsExtendedDisplayAllowed = isExtendedDisplayAllowed; mOnTopologyChangedCallback = onTopologyChangedCallback; mTopologyChangeExecutor = topologyChangeExecutor; mSyncRoot = syncRoot; @@ -262,9 +262,9 @@ class DisplayTopologyCoordinator { return false; } if ((info.type == Display.TYPE_EXTERNAL || info.type == Display.TYPE_OVERLAY) - && !mIsExtendedDisplayEnabled.getAsBoolean()) { + && !mIsExtendedDisplayAllowed.getAsBoolean()) { Slog.d(TAG, "Display " + info.displayId + " not allowed in topology because " - + "type is EXTERNAL or OVERLAY and !mIsExtendedDisplayEnabled"); + + "type is EXTERNAL or OVERLAY and !mIsExtendedDisplayAllowed"); return false; } return true; diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java index 600cf7f06981..1a4ead22f658 100644 --- a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java +++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java @@ -22,6 +22,7 @@ import static com.android.internal.inputmethod.SoftInputShowHideReason.REMOVE_IM import static com.android.internal.inputmethod.SoftInputShowHideReason.SHOW_IME_SCREENSHOT_FROM_IMMS; import static com.android.server.EventLogTags.IMF_HIDE_IME; import static com.android.server.EventLogTags.IMF_SHOW_IME; +import static com.android.server.inputmethod.ImeProtoLogGroup.IME_VISIBILITY_APPLIER_DEBUG; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_EXPLICIT; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_NOT_ALWAYS; @@ -36,7 +37,6 @@ import android.annotation.UserIdInt; import android.os.IBinder; import android.os.ResultReceiver; import android.util.EventLog; -import android.util.Slog; import android.view.MotionEvent; import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; @@ -46,6 +46,7 @@ import android.view.inputmethod.InputMethodManager; import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.InputMethodDebug; import com.android.internal.inputmethod.SoftInputShowHideReason; +import com.android.internal.protolog.ProtoLog; import com.android.server.LocalServices; import com.android.server.wm.ImeTargetVisibilityPolicy; import com.android.server.wm.WindowManagerInternal; @@ -58,9 +59,7 @@ import java.util.Objects; */ final class DefaultImeVisibilityApplier { - private static final String TAG = "DefaultImeVisibilityApplier"; - - private static final boolean DEBUG = InputMethodManagerService.DEBUG; + static final String TAG = "DefaultImeVisibilityApplier"; private InputMethodManagerService mService; @@ -93,11 +92,10 @@ final class DefaultImeVisibilityApplier { final var bindingController = userData.mBindingController; final IInputMethodInvoker curMethod = bindingController.getCurMethod(); if (curMethod != null) { - if (DEBUG) { - Slog.v(TAG, "Calling " + curMethod + ".showSoftInput(" + showInputToken - + ", " + showFlags + ", " + resultReceiver + ") for reason: " - + InputMethodDebug.softInputDisplayReasonToString(reason)); - } + ProtoLog.v(IME_VISIBILITY_APPLIER_DEBUG, + "Calling %s.showSoftInput(%s, %s, %s) for reason: %s", curMethod, + showInputToken, showFlags, resultReceiver, + InputMethodDebug.softInputDisplayReasonToString(reason)); // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not. if (curMethod.showSoftInput(showInputToken, statsToken, showFlags, resultReceiver)) { if (DEBUG_IME_VISIBILITY) { @@ -136,11 +134,9 @@ final class DefaultImeVisibilityApplier { // delivered to the IME process as an IPC. Hence the inconsistency between // IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in // the final state. - if (DEBUG) { - Slog.v(TAG, "Calling " + curMethod + ".hideSoftInput(0, " + hideInputToken - + ", " + resultReceiver + ") for reason: " - + InputMethodDebug.softInputDisplayReasonToString(reason)); - } + ProtoLog.v(IME_VISIBILITY_APPLIER_DEBUG, + "Calling %s.hideSoftInput(0, %s, %s) for reason: %s", curMethod, hideInputToken, + resultReceiver, InputMethodDebug.softInputDisplayReasonToString(reason)); // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not. if (curMethod.hideSoftInput(hideInputToken, statsToken, 0, resultReceiver)) { if (DEBUG_IME_VISIBILITY) { diff --git a/services/core/java/com/android/server/inputmethod/ImeProtoLogGroup.java b/services/core/java/com/android/server/inputmethod/ImeProtoLogGroup.java index f9a56effc800..ea4e29564cc0 100644 --- a/services/core/java/com/android/server/inputmethod/ImeProtoLogGroup.java +++ b/services/core/java/com/android/server/inputmethod/ImeProtoLogGroup.java @@ -23,7 +23,11 @@ import java.util.UUID; public enum ImeProtoLogGroup implements IProtoLogGroup { // TODO(b/393561240): add info/warn/error log level and replace in IMMS IMMS_DEBUG(Consts.ENABLE_DEBUG, false, false, - InputMethodManagerService.TAG); + InputMethodManagerService.TAG), + IME_VISIBILITY_APPLIER_DEBUG(Consts.ENABLE_DEBUG, false, false, + DefaultImeVisibilityApplier.TAG), + IME_VIS_STATE_COMPUTER_DEBUG(Consts.ENABLE_DEBUG, false, false, + ImeVisibilityStateComputer.TAG); private final boolean mEnabled; private volatile boolean mLogToProto; diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java index 5fe8318dbb3f..69353becc692 100644 --- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java +++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java @@ -32,6 +32,7 @@ import static android.view.WindowManager.LayoutParams.SoftInputModeFlags; import static com.android.internal.inputmethod.InputMethodDebug.softInputModeToString; import static com.android.internal.inputmethod.SoftInputShowHideReason.REMOVE_IME_SCREENSHOT_FROM_IMMS; import static com.android.internal.inputmethod.SoftInputShowHideReason.SHOW_IME_SCREENSHOT_FROM_IMMS; +import static com.android.server.inputmethod.ImeProtoLogGroup.IME_VIS_STATE_COMPUTER_DEBUG; import static com.android.server.inputmethod.InputMethodManagerService.computeImeDisplayIdForTarget; import android.accessibilityservice.AccessibilityService; @@ -58,6 +59,7 @@ import android.view.inputmethod.InputMethodManager; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.inputmethod.SoftInputShowHideReason; +import com.android.internal.protolog.ProtoLog; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.wm.WindowManagerInternal; @@ -72,9 +74,7 @@ import java.util.WeakHashMap; */ public final class ImeVisibilityStateComputer { - private static final String TAG = "ImeVisibilityStateComputer"; - - private static final boolean DEBUG = InputMethodManagerService.DEBUG; + static final String TAG = "ImeVisibilityStateComputer"; @UserIdInt private final int mUserId; @@ -292,12 +292,14 @@ public final class ImeVisibilityStateComputer { @InputMethodManager.HideFlags int hideFlags) { if ((hideFlags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0 && (mRequestedShowExplicitly || mShowForced)) { - if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, + "Not hiding: explicit show not cancelled by non-explicit hide"); ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT); return false; } if (mShowForced && (hideFlags & InputMethodManager.HIDE_NOT_ALWAYS) != 0) { - if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, + "Not hiding: forced show not cancelled by not-always hide"); ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS); return false; } @@ -417,8 +419,8 @@ public final class ImeVisibilityStateComputer { @GuardedBy("ImfLock.class") private void setWindowStateInner(IBinder windowToken, @NonNull ImeTargetWindowState newState) { - if (DEBUG) Slog.d(TAG, "setWindowStateInner, windowToken=" + windowToken - + ", state=" + newState); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "setWindowStateInner, windowToken=%s, state=%s", + windowToken, newState); mRequestWindowStateMap.put(windowToken, newState); } @@ -466,7 +468,7 @@ public final class ImeVisibilityStateComputer { // Because the app might leverage these flags to hide soft-keyboard with showing their own // UI for input. if (state.hasEditorFocused() && shouldRestoreImeVisibility(state)) { - if (DEBUG) Slog.v(TAG, "Will show input to restore visibility"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "Will show input to restore visibility"); // Inherit the last requested IME visible state when the target window is still // focused with an editor. state.setRequestedImeVisible(true); @@ -483,7 +485,8 @@ public final class ImeVisibilityStateComputer { // There is no focus view, and this window will // be behind any soft input window, so hide the // soft input window if it is shown. - if (DEBUG) Slog.v(TAG, "Unspecified window will hide input"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, + "Unspecified window will hide input"); return new ImeVisibilityResult(STATE_HIDE_IME_NOT_ALWAYS, SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW); } @@ -495,7 +498,7 @@ public final class ImeVisibilityStateComputer { // them good context without input information being obscured // by the IME) or if running on a large screen where there // is more room for the target window + IME. - if (DEBUG) Slog.v(TAG, "Unspecified window will show input"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "Unspecified window will show input"); return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT, SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV); } @@ -513,7 +516,8 @@ public final class ImeVisibilityStateComputer { // the WindowState, as they're already in the correct state break; } else if (isForwardNavigation) { - if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, + "Window asks to hide input going forward"); return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT, SoftInputShowHideReason.HIDE_STATE_HIDDEN_FORWARD_NAV); } @@ -524,7 +528,7 @@ public final class ImeVisibilityStateComputer { // the WindowState, as they're already in the correct state break; } else if (state.hasImeFocusChanged()) { - if (DEBUG) Slog.v(TAG, "Window asks to hide input"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "Window asks to hide input"); return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT, SoftInputShowHideReason.HIDE_ALWAYS_HIDDEN_STATE); } @@ -532,7 +536,8 @@ public final class ImeVisibilityStateComputer { case WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE: if (isForwardNavigation) { if (allowVisible) { - if (DEBUG) Slog.v(TAG, "Window asks to show input going forward"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, + "Window asks to show input going forward"); return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT, SoftInputShowHideReason.SHOW_STATE_VISIBLE_FORWARD_NAV); } else { @@ -543,7 +548,7 @@ public final class ImeVisibilityStateComputer { } break; case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE: - if (DEBUG) Slog.v(TAG, "Window asks to always show input"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "Window asks to always show input"); if (allowVisible) { if (state.hasImeFocusChanged()) { return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT, @@ -565,7 +570,8 @@ public final class ImeVisibilityStateComputer { // To maintain compatibility, we are now hiding the IME when we don't have // an editor upon refocusing a window. if (state.isStartInputByGainFocus()) { - if (DEBUG) Slog.v(TAG, "Same window without editor will hide input"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, + "Same window without editor will hide input"); return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT, SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR); } @@ -579,7 +585,7 @@ public final class ImeVisibilityStateComputer { // 1) SOFT_INPUT_STATE_UNCHANGED state without an editor // 2) SOFT_INPUT_STATE_VISIBLE state without an editor // 3) SOFT_INPUT_STATE_ALWAYS_VISIBLE state without an editor - if (DEBUG) Slog.v(TAG, "Window without editor will hide input"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "Window without editor will hide input"); if (Flags.refactorInsetsController()) { state.setRequestedImeVisible(false); } diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index a0e543300ce7..42d0a5c4757a 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -3618,6 +3618,12 @@ public class LockSettingsService extends ILockSettings.Stub { return; } + UserInfo userInfo = mInjector.getUserManagerInternal().getUserInfo(userId); + if (userInfo != null && userInfo.isForTesting()) { + Slog.i(TAG, "Keeping escrow data for test-only user"); + return; + } + // Disable escrow token permanently on all other device/user types. Slogf.i(TAG, "Permanently disabling support for escrow tokens on user %d", userId); mSpManager.destroyEscrowData(userId); diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 91a2843ccaf7..9e38435ff7f1 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -48,7 +48,6 @@ import android.media.quality.IMediaQualityManager; import android.media.quality.IPictureProfileCallback; import android.media.quality.ISoundProfileCallback; import android.media.quality.MediaQualityContract.BaseParameters; -import android.media.quality.MediaQualityManager; import android.media.quality.ParameterCapability; import android.media.quality.PictureProfile; import android.media.quality.PictureProfileHandle; @@ -187,7 +186,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public PictureProfile createPictureProfile(PictureProfile pp, UserHandle user) { + public PictureProfile createPictureProfile(PictureProfile pp, int userId) { if ((pp.getPackageName() != null && !pp.getPackageName().isEmpty() && !incomingPackageEqualsCallingUidPackage(pp.getPackageName())) && !hasGlobalPictureQualityServicePermission()) { @@ -221,7 +220,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public void updatePictureProfile(String id, PictureProfile pp, UserHandle user) { + public void updatePictureProfile(String id, PictureProfile pp, int userId) { Long dbId = mPictureProfileTempIdMap.getKey(id); if (!hasPermissionToUpdatePictureProfile(dbId, pp)) { mMqManagerNotifier.notifyOnPictureProfileError(id, @@ -249,7 +248,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public void removePictureProfile(String id, UserHandle user) { + public void removePictureProfile(String id, int userId) { synchronized (mPictureProfileLock) { Long dbId = mPictureProfileTempIdMap.getKey(id); @@ -290,10 +289,8 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public PictureProfile getPictureProfile(int type, String name, Bundle options, - UserHandle user) { - boolean includeParams = - options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); + public PictureProfile getPictureProfile(int type, String name, boolean includeParams, + int userId) { String selection = BaseParameters.PARAMETER_TYPE + " = ? AND " + BaseParameters.PARAMETER_NAME + " = ? AND " + BaseParameters.PARAMETER_PACKAGE + " = ?"; @@ -327,7 +324,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override public List<PictureProfile> getPictureProfilesByPackage( - String packageName, Bundle options, UserHandle user) { + String packageName, boolean includeParams, int userId) { if (!hasGlobalPictureQualityServicePermission()) { mMqManagerNotifier.notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, @@ -335,8 +332,6 @@ public class MediaQualityService extends SystemService { } synchronized (mPictureProfileLock) { - boolean includeParams = - options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); String selection = BaseParameters.PARAMETER_PACKAGE + " = ?"; String[] selectionArguments = {packageName}; return mMqDatabaseUtils.getPictureProfilesBasedOnConditions(MediaQualityUtils @@ -347,17 +342,17 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public List<PictureProfile> getAvailablePictureProfiles(Bundle options, UserHandle user) { + public List<PictureProfile> getAvailablePictureProfiles(boolean includeParams, int userId) { String packageName = getPackageOfCallingUid(); if (packageName != null) { - return getPictureProfilesByPackage(packageName, options, user); + return getPictureProfilesByPackage(packageName, includeParams, userId); } return new ArrayList<>(); } @GuardedBy("mPictureProfileLock") @Override - public boolean setDefaultPictureProfile(String profileId, UserHandle user) { + public boolean setDefaultPictureProfile(String profileId, int userId) { if (!hasGlobalPictureQualityServicePermission()) { mMqManagerNotifier.notifyOnPictureProfileError(profileId, PictureProfile.ERROR_NO_PERMISSION, @@ -387,7 +382,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public List<String> getPictureProfilePackageNames(UserHandle user) { + public List<String> getPictureProfilePackageNames(int userId) { if (!hasGlobalPictureQualityServicePermission()) { mMqManagerNotifier.notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, @@ -406,7 +401,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public List<PictureProfileHandle> getPictureProfileHandle(String[] ids, UserHandle user) { + public List<PictureProfileHandle> getPictureProfileHandle(String[] ids, int userId) { List<PictureProfileHandle> toReturn = new ArrayList<>(); synchronized (mPictureProfileLock) { for (String id : ids) { @@ -423,13 +418,13 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public List<SoundProfileHandle> getSoundProfileHandle(String[] ids, UserHandle user) { + public List<SoundProfileHandle> getSoundProfileHandle(String[] ids, int userId) { List<SoundProfileHandle> toReturn = new ArrayList<>(); synchronized (mSoundProfileLock) { for (String id : ids) { Long key = mSoundProfileTempIdMap.getKey(id); if (key != null) { - toReturn.add(new SoundProfileHandle(key)); + toReturn.add(MediaQualityUtils.SOUND_PROFILE_HANDLE_NONE); } else { toReturn.add(null); } @@ -440,7 +435,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public SoundProfile createSoundProfile(SoundProfile sp, UserHandle user) { + public SoundProfile createSoundProfile(SoundProfile sp, int userId) { if ((sp.getPackageName() != null && !sp.getPackageName().isEmpty() && !incomingPackageEqualsCallingUidPackage(sp.getPackageName())) && !hasGlobalPictureQualityServicePermission()) { @@ -473,7 +468,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public void updateSoundProfile(String id, SoundProfile sp, UserHandle user) { + public void updateSoundProfile(String id, SoundProfile sp, int userId) { Long dbId = mSoundProfileTempIdMap.getKey(id); if (!hasPermissionToUpdateSoundProfile(dbId, sp)) { mMqManagerNotifier.notifyOnSoundProfileError(id, SoundProfile.ERROR_NO_PERMISSION, @@ -502,7 +497,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public void removeSoundProfile(String id, UserHandle user) { + public void removeSoundProfile(String id, int userId) { synchronized (mSoundProfileLock) { Long dbId = mSoundProfileTempIdMap.getKey(id); SoundProfile toDelete = mMqDatabaseUtils.getSoundProfile(dbId); @@ -542,10 +537,8 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public SoundProfile getSoundProfile(int type, String name, Bundle options, - UserHandle user) { - boolean includeParams = - options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); + public SoundProfile getSoundProfile(int type, String name, boolean includeParams, + int userId) { String selection = BaseParameters.PARAMETER_TYPE + " = ? AND " + BaseParameters.PARAMETER_NAME + " = ? AND " + BaseParameters.PARAMETER_PACKAGE + " = ?"; @@ -579,15 +572,13 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override public List<SoundProfile> getSoundProfilesByPackage( - String packageName, Bundle options, UserHandle user) { + String packageName, boolean includeParams, int userId) { if (!hasGlobalSoundQualityServicePermission()) { mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } synchronized (mSoundProfileLock) { - boolean includeParams = - options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); String selection = BaseParameters.PARAMETER_PACKAGE + " = ?"; String[] selectionArguments = {packageName}; return mMqDatabaseUtils.getSoundProfilesBasedOnConditions(MediaQualityUtils @@ -598,17 +589,17 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public List<SoundProfile> getAvailableSoundProfiles(Bundle options, UserHandle user) { + public List<SoundProfile> getAvailableSoundProfiles(boolean includeParams, int userId) { String packageName = getPackageOfCallingUid(); if (packageName != null) { - return getSoundProfilesByPackage(packageName, options, user); + return getSoundProfilesByPackage(packageName, includeParams, userId); } return new ArrayList<>(); } @GuardedBy("mSoundProfileLock") @Override - public boolean setDefaultSoundProfile(String profileId, UserHandle user) { + public boolean setDefaultSoundProfile(String profileId, int userId) { if (!hasGlobalSoundQualityServicePermission()) { mMqManagerNotifier.notifyOnSoundProfileError(profileId, SoundProfile.ERROR_NO_PERMISSION, @@ -638,7 +629,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public List<String> getSoundProfilePackageNames(UserHandle user) { + public List<String> getSoundProfilePackageNames(int userId) { if (!hasGlobalSoundQualityServicePermission()) { mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); @@ -737,7 +728,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mAmbientBacklightLock") @Override public void setAmbientBacklightSettings( - AmbientBacklightSettings settings, UserHandle user) { + AmbientBacklightSettings settings, int userId) { if (DEBUG) { Slogf.d(TAG, "setAmbientBacklightSettings " + settings); } @@ -775,7 +766,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mAmbientBacklightLock") @Override - public void setAmbientBacklightEnabled(boolean enabled, UserHandle user) { + public void setAmbientBacklightEnabled(boolean enabled, int userId) { if (DEBUG) { Slogf.d(TAG, "setAmbientBacklightEnabled " + enabled); } @@ -795,7 +786,7 @@ public class MediaQualityService extends SystemService { @Override public List<ParameterCapability> getParameterCapabilities( - List<String> names, UserHandle user) { + List<String> names, int userId) { byte[] byteArray = MediaQualityUtils.convertParameterToByteArray(names); ParamCapability[] caps = new ParamCapability[byteArray.length]; try { @@ -828,7 +819,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public List<String> getPictureProfileAllowList(UserHandle user) { + public List<String> getPictureProfileAllowList(int userId) { if (!hasGlobalPictureQualityServicePermission()) { mMqManagerNotifier.notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, @@ -844,7 +835,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public void setPictureProfileAllowList(List<String> packages, UserHandle user) { + public void setPictureProfileAllowList(List<String> packages, int userId) { if (!hasGlobalPictureQualityServicePermission()) { mMqManagerNotifier.notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, @@ -857,7 +848,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public List<String> getSoundProfileAllowList(UserHandle user) { + public List<String> getSoundProfileAllowList(int userId) { if (!hasGlobalSoundQualityServicePermission()) { mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); @@ -872,7 +863,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public void setSoundProfileAllowList(List<String> packages, UserHandle user) { + public void setSoundProfileAllowList(List<String> packages, int userId) { if (!hasGlobalSoundQualityServicePermission()) { mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); @@ -883,13 +874,13 @@ public class MediaQualityService extends SystemService { } @Override - public boolean isSupported(UserHandle user) { + public boolean isSupported(int userId) { return false; } @GuardedBy("mPictureProfileLock") @Override - public void setAutoPictureQualityEnabled(boolean enabled, UserHandle user) { + public void setAutoPictureQualityEnabled(boolean enabled, int userId) { if (!hasGlobalPictureQualityServicePermission()) { mMqManagerNotifier.notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, @@ -910,7 +901,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public boolean isAutoPictureQualityEnabled(UserHandle user) { + public boolean isAutoPictureQualityEnabled(int userId) { synchronized (mPictureProfileLock) { try { if (mMediaQuality != null) { @@ -927,7 +918,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public void setSuperResolutionEnabled(boolean enabled, UserHandle user) { + public void setSuperResolutionEnabled(boolean enabled, int userId) { if (!hasGlobalPictureQualityServicePermission()) { mMqManagerNotifier.notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, @@ -948,7 +939,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public boolean isSuperResolutionEnabled(UserHandle user) { + public boolean isSuperResolutionEnabled(int userId) { synchronized (mPictureProfileLock) { try { if (mMediaQuality != null) { @@ -965,7 +956,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public void setAutoSoundQualityEnabled(boolean enabled, UserHandle user) { + public void setAutoSoundQualityEnabled(boolean enabled, int userId) { if (!hasGlobalSoundQualityServicePermission()) { mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); @@ -986,7 +977,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public boolean isAutoSoundQualityEnabled(UserHandle user) { + public boolean isAutoSoundQualityEnabled(int userId) { synchronized (mSoundProfileLock) { try { if (mMediaQuality != null) { @@ -1003,7 +994,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mAmbientBacklightLock") @Override - public boolean isAmbientBacklightEnabled(UserHandle user) { + public boolean isAmbientBacklightEnabled(int userId) { return false; } } diff --git a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java index 88d3f1ff7c52..303c96750098 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java @@ -60,6 +60,11 @@ public final class MediaQualityUtils { private static final String TAG = "MediaQualityUtils"; public static final String SETTINGS = "settings"; + public static final SoundProfileHandle SOUND_PROFILE_HANDLE_NONE = new SoundProfileHandle(); + static { + SOUND_PROFILE_HANDLE_NONE.id = -10000; + } + /** * Convert PictureParameter List to PersistableBundle. */ @@ -1022,7 +1027,7 @@ public final class MediaQualityUtils { getInputId(cursor), getPackageName(cursor), jsonToPersistableBundle(getSettingsString(cursor)), - SoundProfileHandle.NONE + SOUND_PROFILE_HANDLE_NONE ); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index bfe0d32f4cb6..7a544cf1c26c 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -1075,6 +1075,7 @@ public class NotificationManagerService extends SystemService { summary.getSbn().getNotification().getGroupAlertBehavior(); if (notificationForceGrouping()) { + summary.getNotification().flags |= Notification.FLAG_SILENT; if (!summary.getChannel().getId().equals(summaryAttr.channelId)) { NotificationChannel newChannel = mPreferencesHelper.getNotificationChannel(pkg, summary.getUid(), summaryAttr.channelId, false); @@ -7450,6 +7451,7 @@ public class NotificationManagerService extends SystemService { // Override group key early for forced grouped notifications r.setOverrideGroupKey(groupName); } + r.getNotification().flags |= Notification.FLAG_SILENT; } addAutoGroupAdjustment(r, groupName); diff --git a/services/core/java/com/android/server/power/ScreenUndimDetector.java b/services/core/java/com/android/server/power/ScreenUndimDetector.java index c4929c210e2c..b376417061db 100644 --- a/services/core/java/com/android/server/power/ScreenUndimDetector.java +++ b/services/core/java/com/android/server/power/ScreenUndimDetector.java @@ -30,11 +30,11 @@ import android.provider.DeviceConfig; import android.util.Slog; import android.view.Display; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; import java.util.Set; -import java.util.concurrent.TimeUnit; /** * Detects when user manually undims the screen (x times) and acquires a wakelock to keep the screen @@ -48,7 +48,6 @@ public class ScreenUndimDetector { /** DeviceConfig flag: is keep screen on feature enabled. */ static final String KEY_KEEP_SCREEN_ON_ENABLED = "keep_screen_on_enabled"; - private static final boolean DEFAULT_KEEP_SCREEN_ON_ENABLED = true; private static final int OUTCOME_POWER_BUTTON = FrameworkStatsLog.TIMEOUT_AUTO_EXTENDED_REPORTED__OUTCOME__POWER_BUTTON; private static final int OUTCOME_TIMEOUT = @@ -58,15 +57,11 @@ public class ScreenUndimDetector { /** DeviceConfig flag: how long should we keep the screen on. */ @VisibleForTesting static final String KEY_KEEP_SCREEN_ON_FOR_MILLIS = "keep_screen_on_for_millis"; - @VisibleForTesting - static final long DEFAULT_KEEP_SCREEN_ON_FOR_MILLIS = TimeUnit.MINUTES.toMillis(10); private long mKeepScreenOnForMillis; /** DeviceConfig flag: how many user undims required to trigger keeping the screen on. */ @VisibleForTesting static final String KEY_UNDIMS_REQUIRED = "undims_required"; - @VisibleForTesting - static final int DEFAULT_UNDIMS_REQUIRED = 2; private int mUndimsRequired; /** @@ -76,8 +71,6 @@ public class ScreenUndimDetector { @VisibleForTesting static final String KEY_MAX_DURATION_BETWEEN_UNDIMS_MILLIS = "max_duration_between_undims_millis"; - @VisibleForTesting - static final long DEFAULT_MAX_DURATION_BETWEEN_UNDIMS_MILLIS = TimeUnit.MINUTES.toMillis(5); private long mMaxDurationBetweenUndimsMillis; @VisibleForTesting @@ -92,6 +85,7 @@ public class ScreenUndimDetector { private long mUndimOccurredTime = -1; private long mInteractionAfterUndimTime = -1; private InternalClock mClock; + private Context mContext; public ScreenUndimDetector() { mClock = new InternalClock(); @@ -109,12 +103,13 @@ public class ScreenUndimDetector { /** Should be called in parent's systemReady() */ public void systemReady(Context context) { + mContext = context; readValuesFromDeviceConfig(); DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_ATTENTION_MANAGER_SERVICE, - context.getMainExecutor(), + mContext.getMainExecutor(), (properties) -> onDeviceConfigChange(properties.getKeyset())); - final PowerManager powerManager = context.getSystemService(PowerManager.class); + final PowerManager powerManager = mContext.getSystemService(PowerManager.class); mWakeLock = powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, UNDIM_DETECTOR_WAKE_LOCK); @@ -203,36 +198,44 @@ public class ScreenUndimDetector { } } - private boolean readKeepScreenOnNotificationEnabled() { + private boolean readKeepScreenOnEnabled() { + boolean defaultKeepScreenOnEnabled = mContext.getResources().getBoolean( + R.bool.config_defaultPreventScreenTimeoutEnabled); return DeviceConfig.getBoolean(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_KEEP_SCREEN_ON_ENABLED, - DEFAULT_KEEP_SCREEN_ON_ENABLED); + defaultKeepScreenOnEnabled); } private long readKeepScreenOnForMillis() { + long defaultKeepScreenOnDuration = mContext.getResources().getInteger( + R.integer.config_defaultPreventScreenTimeoutForMillis); return DeviceConfig.getLong(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_KEEP_SCREEN_ON_FOR_MILLIS, - DEFAULT_KEEP_SCREEN_ON_FOR_MILLIS); + defaultKeepScreenOnDuration); } private int readUndimsRequired() { + int defaultUndimsRequired = mContext.getResources().getInteger( + R.integer.config_defaultUndimsRequired); int undimsRequired = DeviceConfig.getInt(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_UNDIMS_REQUIRED, - DEFAULT_UNDIMS_REQUIRED); + defaultUndimsRequired); if (undimsRequired < 1 || undimsRequired > 5) { Slog.e(TAG, "Provided undimsRequired=" + undimsRequired - + " is not allowed [1, 5]; using the default=" + DEFAULT_UNDIMS_REQUIRED); - return DEFAULT_UNDIMS_REQUIRED; + + " is not allowed [1, 5]; using the default=" + defaultUndimsRequired); + return defaultUndimsRequired; } return undimsRequired; } private long readMaxDurationBetweenUndimsMillis() { + long defaultMaxDurationBetweenUndimsMillis = mContext.getResources().getInteger( + R.integer.config_defaultMaxDurationBetweenUndimsMillis); return DeviceConfig.getLong(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_MAX_DURATION_BETWEEN_UNDIMS_MILLIS, - DEFAULT_MAX_DURATION_BETWEEN_UNDIMS_MILLIS); + defaultMaxDurationBetweenUndimsMillis); } private void onDeviceConfigChange(@NonNull Set<String> keys) { @@ -253,15 +256,16 @@ public class ScreenUndimDetector { @VisibleForTesting void readValuesFromDeviceConfig() { - mKeepScreenOnEnabled = readKeepScreenOnNotificationEnabled(); + mKeepScreenOnEnabled = readKeepScreenOnEnabled(); mKeepScreenOnForMillis = readKeepScreenOnForMillis(); mUndimsRequired = readUndimsRequired(); mMaxDurationBetweenUndimsMillis = readMaxDurationBetweenUndimsMillis(); Slog.i(TAG, "readValuesFromDeviceConfig():" + "\nmKeepScreenOnForMillis=" + mKeepScreenOnForMillis - + "\nmKeepScreenOnNotificationEnabled=" + mKeepScreenOnEnabled - + "\nmUndimsRequired=" + mUndimsRequired); + + "\nmKeepScreenOnEnabled=" + mKeepScreenOnEnabled + + "\nmUndimsRequired=" + mUndimsRequired + + "\nmMaxDurationBetweenUndimsMillis=" + mMaxDurationBetweenUndimsMillis); } diff --git a/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java index 5563f98e8842..7cd9bdbc662c 100644 --- a/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java +++ b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java @@ -374,6 +374,10 @@ public class BatteryHistoryDirectory implements BatteryStatsHistory.BatteryHisto @SuppressWarnings("unchecked") @Override public List<BatteryHistoryFragment> getFragments() { + if (!mLock.isHeldByCurrentThread()) { + throw new IllegalStateException("Reading battery history without a lock"); + } + ensureInitialized(); return (List<BatteryHistoryFragment>) (List<? extends BatteryHistoryFragment>) mHistoryFiles; @@ -443,44 +447,6 @@ public class BatteryHistoryDirectory implements BatteryStatsHistory.BatteryHisto } @Override - public BatteryHistoryFragment getNextFragment(BatteryHistoryFragment current, long startTimeMs, - long endTimeMs) { - ensureInitialized(); - - if (!mLock.isHeldByCurrentThread()) { - throw new IllegalStateException("Iterating battery history without a lock"); - } - - int nextFileIndex = 0; - int firstFileIndex = 0; - // skip the last file because its data is in history buffer. - int lastFileIndex = mHistoryFiles.size() - 2; - for (int i = lastFileIndex; i >= 0; i--) { - BatteryHistoryFragment fragment = mHistoryFiles.get(i); - if (current != null && fragment.monotonicTimeMs == current.monotonicTimeMs) { - nextFileIndex = i + 1; - } - if (fragment.monotonicTimeMs > endTimeMs) { - lastFileIndex = i - 1; - } - if (fragment.monotonicTimeMs <= startTimeMs) { - firstFileIndex = i; - break; - } - } - - if (nextFileIndex < firstFileIndex) { - nextFileIndex = firstFileIndex; - } - - if (nextFileIndex <= lastFileIndex) { - return mHistoryFiles.get(nextFileIndex); - } - - return null; - } - - @Override public boolean hasCompletedFragments() { ensureInitialized(); diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java index 21628341ea62..cb122f2080a2 100644 --- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java +++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java @@ -107,8 +107,7 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord && !ActivityManager.isLowRamDeviceStatic(); // Don't support Android Go setSnapshotEnabled(snapshotEnabled); mSnapshotPersistQueue = persistQueue; - mPersistInfoProvider = createPersistInfoProvider(service, - Environment::getDataSystemCeDirectory); + mPersistInfoProvider = createPersistInfoProvider(service); mPersister = new TaskSnapshotPersister( persistQueue, mPersistInfoProvider, @@ -117,6 +116,11 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord initialize(new ActivitySnapshotCache()); } + @VisibleForTesting + PersistInfoProvider createPersistInfoProvider(WindowManagerService service) { + return createPersistInfoProvider(service, Environment::getDataSystemCeDirectory); + } + @Override protected float initSnapshotScale() { final float config = mService.mContext.getResources().getFloat( diff --git a/services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java b/services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java index 5db02dff8351..aaa5a0074506 100644 --- a/services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java +++ b/services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java @@ -16,10 +16,20 @@ package com.android.server.wm; +import static com.android.server.wm.AbsAppSnapshotController.TAG; + import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.window.TaskSnapshot; +import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; + import java.io.File; +import java.util.UUID; class BaseAppSnapshotPersister { static final String LOW_RES_FILE_POSTFIX = "_reduced"; @@ -29,6 +39,7 @@ class BaseAppSnapshotPersister { // Shared with SnapshotPersistQueue protected final Object mLock; protected final SnapshotPersistQueue mSnapshotPersistQueue; + @VisibleForTesting protected final PersistInfoProvider mPersistInfoProvider; BaseAppSnapshotPersister(SnapshotPersistQueue persistQueue, @@ -79,6 +90,8 @@ class BaseAppSnapshotPersister { private final boolean mEnableLowResSnapshots; private final float mLowResScaleFactor; private final boolean mUse16BitFormat; + private final SparseBooleanArray mInitializedUsers = new SparseBooleanArray(); + private final SparseArray<File> mScrambleDirectories = new SparseArray<>(); PersistInfoProvider(DirectoryResolver directoryResolver, String dirName, boolean enableLowResSnapshots, float lowResScaleFactor, boolean use16BitFormat) { @@ -91,9 +104,80 @@ class BaseAppSnapshotPersister { @NonNull File getDirectory(int userId) { + if (Flags.scrambleSnapshotFileName()) { + final File directory = getOrInitScrambleDirectory(userId); + if (directory != null) { + return directory; + } + } + return getBaseDirectory(userId); + } + + @NonNull + private File getBaseDirectory(int userId) { return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), mDirName); } + @Nullable + private File getOrInitScrambleDirectory(int userId) { + synchronized (mScrambleDirectories) { + if (mInitializedUsers.get(userId)) { + return mScrambleDirectories.get(userId); + } + mInitializedUsers.put(userId, true); + final File scrambledDirectory = getScrambleDirectory(userId); + final File baseDir = getBaseDirectory(userId); + String newName = null; + // If directory exists, rename + if (scrambledDirectory.exists()) { + newName = UUID.randomUUID().toString(); + final File scrambleTo = new File(baseDir, newName); + if (!scrambledDirectory.renameTo(scrambleTo)) { + Slog.w(TAG, "SnapshotPersister rename scramble folder fail."); + return null; + } + } else { + // If directory not exists, mkDir. + if (!baseDir.exists() && !baseDir.mkdir()) { + Slog.w(TAG, "SnapshotPersister make base folder fail."); + return null; + } + if (!scrambledDirectory.mkdir()) { + Slog.e(TAG, "SnapshotPersister make scramble folder fail"); + return null; + } + // Move any existing files to this folder. + final String[] files = baseDir.list(); + if (files != null) { + for (String file : files) { + final File original = new File(baseDir, file); + if (original.isDirectory()) { + newName = file; + } else { + File to = new File(scrambledDirectory, file); + original.renameTo(to); + } + } + } + } + final File newFolder = new File(baseDir, newName); + mScrambleDirectories.put(userId, newFolder); + return newFolder; + } + } + + @NonNull + private File getScrambleDirectory(int userId) { + final File dir = getBaseDirectory(userId); + final String[] directories = dir.list( + (current, name) -> new File(current, name).isDirectory()); + if (directories != null && directories.length > 0) { + return new File(dir, directories[0]); + } else { + return new File(dir, UUID.randomUUID().toString()); + } + } + /** * Return if task snapshots are stored in 16 bit pixel format. * diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java index d91fca9e2816..dc42b32967e2 100644 --- a/services/core/java/com/android/server/wm/DesktopModeHelper.java +++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java @@ -17,7 +17,6 @@ package com.android.server.wm; import static android.app.Flags.enableConnectedDisplaysWallpaper; -import static android.window.DesktopExperienceFlags.ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE; import android.annotation.NonNull; import android.content.Context; @@ -67,7 +66,7 @@ public final class DesktopModeHelper { * Return {@code true} if the current device can hosts desktop sessions on its internal display. */ @VisibleForTesting - private static boolean canInternalDisplayHostDesktops(@NonNull Context context) { + static boolean canInternalDisplayHostDesktops(@NonNull Context context) { return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); } @@ -84,11 +83,8 @@ public final class DesktopModeHelper { if (!shouldEnforceDeviceRestrictions()) { return true; } - // If projected display is enabled, #canInternalDisplayHostDesktops is no longer a - // requirement. - final boolean desktopModeSupported = ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE.isTrue() - ? isDesktopModeSupported(context) : (isDesktopModeSupported(context) - && canInternalDisplayHostDesktops(context)); + final boolean desktopModeSupported = isDesktopModeSupported(context) + && canInternalDisplayHostDesktops(context); final boolean desktopModeSupportedByDevOptions = Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionsSupported(context); diff --git a/services/core/java/com/android/server/wm/DisplayWindowListenerController.java b/services/core/java/com/android/server/wm/DisplayWindowListenerController.java index fa7a99d55896..d90fff229cd9 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowListenerController.java +++ b/services/core/java/com/android/server/wm/DisplayWindowListenerController.java @@ -40,14 +40,14 @@ class DisplayWindowListenerController { } int[] registerListener(IDisplayWindowListener listener) { + mDisplayListeners.register(listener); + final IntArray displayIds = new IntArray(); synchronized (mService.mGlobalLock) { - mDisplayListeners.register(listener); - final IntArray displayIds = new IntArray(); mService.mAtmService.mRootWindowContainer.forAllDisplays((displayContent) -> { displayIds.add(displayContent.mDisplayId); }); - return displayIds.toArray(); } + return displayIds.toArray(); } void unregisterListener(IDisplayWindowListener listener) { diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index 95776088aad8..66d04df8095b 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -193,10 +193,6 @@ cc_defaults { "android.hardware.tv.input@1.0", "android.hardware.tv.input-V2-ndk", "android.hardware.vibrator-V3-ndk", - "android.hardware.vibrator@1.0", - "android.hardware.vibrator@1.1", - "android.hardware.vibrator@1.2", - "android.hardware.vibrator@1.3", "android.hardware.vr@1.0", "android.hidl.token@1.0-utils", "android.frameworks.schedulerservice@1.0", diff --git a/services/core/jni/com_android_server_vibrator_VibratorController.cpp b/services/core/jni/com_android_server_vibrator_VibratorController.cpp index 534dbb1f6cf1..11dbbdfb850e 100644 --- a/services/core/jni/com_android_server_vibrator_VibratorController.cpp +++ b/services/core/jni/com_android_server_vibrator_VibratorController.cpp @@ -19,7 +19,6 @@ #include <aidl/android/hardware/vibrator/IVibrator.h> #include <android/binder_parcel.h> #include <android/binder_parcel_jni.h> -#include <android/hardware/vibrator/1.3/IVibrator.h> #include <android/persistable_bundle_aidl.h> #include <android_os_vibrator.h> #include <nativehelper/JNIHelp.h> @@ -32,8 +31,6 @@ #include "core_jni_helpers.h" #include "jni.h" -namespace V1_0 = android::hardware::vibrator::V1_0; -namespace V1_3 = android::hardware::vibrator::V1_3; namespace Aidl = aidl::android::hardware::vibrator; using aidl::android::os::PersistableBundle; @@ -80,31 +77,6 @@ static struct { jfieldID timeMillis; } sPwlePointClassInfo; -static_assert(static_cast<uint8_t>(V1_0::EffectStrength::LIGHT) == - static_cast<uint8_t>(Aidl::EffectStrength::LIGHT)); -static_assert(static_cast<uint8_t>(V1_0::EffectStrength::MEDIUM) == - static_cast<uint8_t>(Aidl::EffectStrength::MEDIUM)); -static_assert(static_cast<uint8_t>(V1_0::EffectStrength::STRONG) == - static_cast<uint8_t>(Aidl::EffectStrength::STRONG)); - -static_assert(static_cast<uint8_t>(V1_3::Effect::CLICK) == - static_cast<uint8_t>(Aidl::Effect::CLICK)); -static_assert(static_cast<uint8_t>(V1_3::Effect::DOUBLE_CLICK) == - static_cast<uint8_t>(Aidl::Effect::DOUBLE_CLICK)); -static_assert(static_cast<uint8_t>(V1_3::Effect::TICK) == static_cast<uint8_t>(Aidl::Effect::TICK)); -static_assert(static_cast<uint8_t>(V1_3::Effect::THUD) == static_cast<uint8_t>(Aidl::Effect::THUD)); -static_assert(static_cast<uint8_t>(V1_3::Effect::POP) == static_cast<uint8_t>(Aidl::Effect::POP)); -static_assert(static_cast<uint8_t>(V1_3::Effect::HEAVY_CLICK) == - static_cast<uint8_t>(Aidl::Effect::HEAVY_CLICK)); -static_assert(static_cast<uint8_t>(V1_3::Effect::RINGTONE_1) == - static_cast<uint8_t>(Aidl::Effect::RINGTONE_1)); -static_assert(static_cast<uint8_t>(V1_3::Effect::RINGTONE_2) == - static_cast<uint8_t>(Aidl::Effect::RINGTONE_2)); -static_assert(static_cast<uint8_t>(V1_3::Effect::RINGTONE_15) == - static_cast<uint8_t>(Aidl::Effect::RINGTONE_15)); -static_assert(static_cast<uint8_t>(V1_3::Effect::TEXTURE_TICK) == - static_cast<uint8_t>(Aidl::Effect::TEXTURE_TICK)); - static std::shared_ptr<vibrator::HalController> findVibrator(int32_t vibratorId) { vibrator::ManagerHalController* manager = android_server_vibrator_VibratorManagerService_getManager(); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 215d6ca964eb..0ad976c38565 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -245,6 +245,7 @@ import static android.app.admin.ProvisioningException.ERROR_REMOVE_NON_REQUIRED_ import static android.app.admin.ProvisioningException.ERROR_SETTING_PROFILE_OWNER_FAILED; import static android.app.admin.ProvisioningException.ERROR_SET_DEVICE_OWNER_FAILED; import static android.app.admin.ProvisioningException.ERROR_STARTING_PROFILE_FAILED; +import static android.content.Context.RECEIVER_NOT_EXPORTED; import static android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE; import static android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; @@ -486,6 +487,7 @@ import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.telephony.data.ApnSetting; +import android.telephony.euicc.EuiccManager; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.ArrayMap; @@ -643,6 +645,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { static final String ACTION_PROFILE_OFF_DEADLINE = "com.android.server.ACTION_PROFILE_OFF_DEADLINE"; + /** Broadcast action invoked when a managed eSIM is removed while deleting work profile. */ + private static final String ACTION_ESIM_REMOVED_WITH_MANAGED_PROFILE = + "com.android.server.ACTION_ESIM_REMOVED_WITH_MANAGED_PROFILE"; + + /** Extra for the subscription ID of the managed eSIM removed while deleting work profile. */ + private static final String EXTRA_REMOVED_ESIM_SUBSCRIPTION_ID = + "com.android.server.EXTRA_ESIM_REMOVED_WITH_MANAGED_PROFILE_SUBSCRIPTION_ID"; + private static final String CALLED_FROM_PARENT = "calledFromParent"; private static final String NOT_CALLED_FROM_PARENT = "notCalledFromParent"; @@ -1266,6 +1276,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { removeCredentialManagementApp(intent.getData().getSchemeSpecificPart()); } else if (Intent.ACTION_MANAGED_PROFILE_ADDED.equals(action)) { clearWipeProfileNotification(); + } else if (Intent.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) { + removeManagedEmbeddedSubscriptionsForUser(userHandle); } else if (Intent.ACTION_DATE_CHANGED.equals(action) || Intent.ACTION_TIME_CHANGED.equals(action)) { // Update freeze period record when clock naturally progresses to the next day @@ -1298,6 +1310,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { triggerPolicyComplianceCheckIfNeeded(userHandle, suspended); } else if (LOGIN_ACCOUNTS_CHANGED_ACTION.equals(action)) { calculateHasIncompatibleAccounts(); + } else if (ACTION_ESIM_REMOVED_WITH_MANAGED_PROFILE.equals(action)) { + int removedSubscriptionId = intent.getIntExtra(EXTRA_REMOVED_ESIM_SUBSCRIPTION_ID, + -1); + Slogf.i(LOG_TAG, + "Deleted subscription with ID %d because owning managed profile was " + + "removed", + removedSubscriptionId); } } @@ -2219,9 +2238,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler); filter = new IntentFilter(); filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED); + filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED); filter.addAction(Intent.ACTION_TIME_CHANGED); filter.addAction(Intent.ACTION_DATE_CHANGED); mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler); + filter = new IntentFilter(); + filter.addAction(ACTION_ESIM_REMOVED_WITH_MANAGED_PROFILE); + mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, + mHandler, RECEIVER_NOT_EXPORTED); LocalServices.addService(DevicePolicyManagerInternal.class, mLocalService); @@ -3970,6 +3994,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { deletedUsers.remove(userInfo.id); } for (Integer userId : deletedUsers) { + removeManagedEmbeddedSubscriptionsForUser(userId); removeUserData(userId); mDevicePolicyEngine.handleUserRemoved(userId); } @@ -8099,6 +8124,45 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { mInjector.getNotificationManager().cancel(SystemMessage.NOTE_PROFILE_WIPED); } + /** + * Remove eSIM subscriptions that are managed by any of the admin packages of the given + * userHandle. + */ + private void removeManagedEmbeddedSubscriptionsForUser(int userHandle) { + if (!Flags.removeManagedEsimOnWorkProfileDeletion()) { + return; + } + + Slogf.i(LOG_TAG, + "Managed profile with ID=%d deleted: going to remove managed embedded " + + "subscriptions", userHandle); + String profileOwnerPackage = mOwners.getProfileOwnerPackage(userHandle); + if (profileOwnerPackage == null) { + Slogf.wtf(LOG_TAG, "Profile owner package for managed profile is null"); + return; + } + IntArray managedSubscriptionIds = getSubscriptionIdsInternal(profileOwnerPackage); + deleteEmbeddedSubscriptions(managedSubscriptionIds); + } + + private void deleteEmbeddedSubscriptions(IntArray subscriptionIds) { + EuiccManager euiccManager = mContext.getSystemService(EuiccManager.class); + for (int subscriptionId : subscriptionIds.toArray()) { + Slogf.i(LOG_TAG, "Deleting embedded subscription with ID %d", subscriptionId); + euiccManager.deleteSubscription(subscriptionId, + createCallbackPendingIntentForRemovingManagedSubscription( + subscriptionId)); + } + } + + private PendingIntent createCallbackPendingIntentForRemovingManagedSubscription( + Integer subscriptionId) { + Intent intent = new Intent(ACTION_ESIM_REMOVED_WITH_MANAGED_PROFILE); + intent.putExtra(EXTRA_REMOVED_ESIM_SUBSCRIPTION_ID, subscriptionId); + return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE); + } + + @Override public void setFactoryResetProtectionPolicy(ComponentName who, String callerPackageName, @Nullable FactoryResetProtectionPolicy policy) { diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java index 8e749529978e..de78271acddc 100644 --- a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java @@ -20,6 +20,7 @@ import static android.hardware.SensorManager.SENSOR_DELAY_FASTEST; import static android.hardware.devicestate.DeviceState.PROPERTY_POLICY_UNSUPPORTED_WHEN_POWER_SAVE_MODE; import static android.hardware.devicestate.DeviceState.PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL; import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; +import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.TYPE_EXTERNAL; @@ -324,6 +325,11 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, } if (newState != INVALID_DEVICE_STATE_IDENTIFIER && newState != mLastReportedState) { + if (Trace.isTagEnabled(TRACE_TAG_SYSTEM_SERVER)) { + Trace.instant(TRACE_TAG_SYSTEM_SERVER, + "[Device state changed] Last hinge sensor event timestamp: " + + mLastHingeAngleSensorEvent.timestamp); + } mLastReportedState = newState; stateToReport = newState; } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 860b6fb1dcd1..788b3b883160 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -366,6 +366,8 @@ public final class SystemServer implements Dumpable { "com.android.clockwork.time.WearTimeService"; private static final String WEAR_SETTINGS_SERVICE_CLASS = "com.android.clockwork.settings.WearSettingsService"; + private static final String WEAR_GESTURE_SERVICE_CLASS = + "com.android.clockwork.gesture.WearGestureService"; private static final String WRIST_ORIENTATION_SERVICE_CLASS = "com.android.clockwork.wristorientation.WristOrientationService"; private static final String IOT_SERVICE_CLASS = @@ -2844,6 +2846,13 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(WRIST_ORIENTATION_SERVICE_CLASS); t.traceEnd(); } + + if (android.server.Flags.wearGestureApi() + && SystemProperties.getBoolean("config.enable_gesture_api", false)) { + t.traceBegin("StartWearGestureService"); + mSystemServiceManager.startService(WEAR_GESTURE_SERVICE_CLASS); + t.traceEnd(); + } } if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_SLICES_DISABLED)) { diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig index 86ccd878de7c..7a6bd75e5893 100644 --- a/services/java/com/android/server/flags.aconfig +++ b/services/java/com/android/server/flags.aconfig @@ -65,4 +65,12 @@ flag { namespace: "package_manager_service" description: "Remove AppIntegrityManagerService" bug: "364200023" +} + +flag { + name: "wear_gesture_api" + namespace: "wear_frameworks" + description: "Whether the Wear Gesture API is available." + bug: "396154116" + is_exported: true }
\ No newline at end of file diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java index 2dd16f68dc56..80a3a8788d80 100644 --- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java +++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java @@ -109,6 +109,7 @@ import com.android.server.EventLogTags; import com.android.server.LocalServices; import com.android.server.backup.BackupAgentConnectionManager; import com.android.server.backup.BackupRestoreTask; +import com.android.server.backup.BackupRestoreTask.CancellationReason; import com.android.server.backup.BackupWakeLock; import com.android.server.backup.DataChangedJournal; import com.android.server.backup.KeyValueBackupJob; @@ -2412,7 +2413,7 @@ public class KeyValueBackupTaskTest { KeyValueBackupTask task = spy(createKeyValueBackupTask(transportMock, PACKAGE_1)); doNothing().when(task).waitCancel(); - task.handleCancel(true); + task.handleCancel(CancellationReason.EXTERNAL); InOrder inOrder = inOrder(task); inOrder.verify(task).markCancel(); @@ -2420,12 +2421,14 @@ public class KeyValueBackupTaskTest { } @Test - public void testHandleCancel_whenCancelAllFalse_throws() throws Exception { + public void testHandleCancel_timeout_throws() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); setUpAgentWithData(PACKAGE_1); KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); - expectThrows(IllegalArgumentException.class, () -> task.handleCancel(false)); + expectThrows( + IllegalArgumentException.class, + () -> task.handleCancel(CancellationReason.TIMEOUT)); } /** Do not update backup token if no data was moved. */ diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/util/IgnoreableExpect.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/util/IgnoreableExpect.kt index afb18f5be669..5c9ba401bf88 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/util/IgnoreableExpect.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/util/IgnoreableExpect.kt @@ -32,7 +32,7 @@ internal class IgnoreableExpect : TestRule { private var ignore = false - override fun apply(base: Statement?, description: Description?): Statement { + override fun apply(base: Statement, description: Description): Statement { return object : Statement() { override fun evaluate() { ignore = false diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt index 206c90d0481a..6dc7361e5366 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt @@ -47,7 +47,7 @@ class DisplayTopologyCoordinatorTest { private val mockTopology = mock<DisplayTopology>() private val mockTopologyCopy = mock<DisplayTopology>() private val mockTopologyGraph = mock<DisplayTopologyGraph>() - private val mockIsExtendedDisplayEnabled = mock<() -> Boolean>() + private val mockIsExtendedDisplayAllowed = mock<() -> Boolean>() private val mockTopologySavedCallback = mock<() -> Unit>() private val mockTopologyChangedCallback = mock<(android.util.Pair<DisplayTopology, DisplayTopologyGraph>) -> Unit>() @@ -73,10 +73,10 @@ class DisplayTopologyCoordinatorTest { ) = mockTopologyStore } - whenever(mockIsExtendedDisplayEnabled()).thenReturn(true) + whenever(mockIsExtendedDisplayAllowed()).thenReturn(true) whenever(mockTopology.copy()).thenReturn(mockTopologyCopy) whenever(mockTopologyCopy.getGraph(any())).thenReturn(mockTopologyGraph) - coordinator = DisplayTopologyCoordinator(injector, mockIsExtendedDisplayEnabled, + coordinator = DisplayTopologyCoordinator(injector, mockIsExtendedDisplayAllowed, mockTopologyChangedCallback, topologyChangeExecutor, DisplayManagerService.SyncRoot(), mockTopologySavedCallback) } @@ -195,7 +195,7 @@ class DisplayTopologyCoordinatorTest { @Test fun addDisplay_external_extendedDisplaysDisabled() { - whenever(mockIsExtendedDisplayEnabled()).thenReturn(false) + whenever(mockIsExtendedDisplayAllowed()).thenReturn(false) for (displayInfo in displayInfos) { coordinator.onDisplayAdded(displayInfo) @@ -208,7 +208,7 @@ class DisplayTopologyCoordinatorTest { @Test fun addDisplay_overlay_extendedDisplaysDisabled() { displayInfos[0].type = Display.TYPE_OVERLAY - whenever(mockIsExtendedDisplayEnabled()).thenReturn(false) + whenever(mockIsExtendedDisplayAllowed()).thenReturn(false) for (displayInfo in displayInfos) { coordinator.onDisplayAdded(displayInfo) @@ -314,7 +314,7 @@ class DisplayTopologyCoordinatorTest { @Test fun updateDisplay_external_extendedDisplaysDisabled() { - whenever(mockIsExtendedDisplayEnabled()).thenReturn(false) + whenever(mockIsExtendedDisplayAllowed()).thenReturn(false) for (displayInfo in displayInfos) { coordinator.onDisplayChanged(displayInfo) @@ -328,7 +328,7 @@ class DisplayTopologyCoordinatorTest { @Test fun updateDisplay_overlay_extendedDisplaysDisabled() { displayInfos[0].type = Display.TYPE_OVERLAY - whenever(mockIsExtendedDisplayEnabled()).thenReturn(false) + whenever(mockIsExtendedDisplayAllowed()).thenReturn(false) coordinator.onDisplayChanged(displayInfos[0]) diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java index 8aaa72339c5b..33bd95ec9f5b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java @@ -51,6 +51,7 @@ import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.runner.AndroidJUnit4; import com.android.server.LocalServices; +import com.android.server.backup.BackupRestoreTask.CancellationReason; import com.android.server.backup.internal.LifecycleOperationStorage; import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; @@ -368,9 +369,12 @@ public class BackupAgentConnectionManagerTest { mConnectionManager.agentDisconnected(TEST_PACKAGE); mTestThread.join(); - verify(mUserBackupManagerService).handleCancel(eq(123), eq(true)); - verify(mUserBackupManagerService).handleCancel(eq(456), eq(true)); - verify(mUserBackupManagerService).handleCancel(eq(789), eq(true)); + verify(mUserBackupManagerService) + .handleCancel(eq(123), eq(CancellationReason.AGENT_DISCONNECTED)); + verify(mUserBackupManagerService) + .handleCancel(eq(456), eq(CancellationReason.AGENT_DISCONNECTED)); + verify(mUserBackupManagerService) + .handleCancel(eq(789), eq(CancellationReason.AGENT_DISCONNECTED)); } @Test diff --git a/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java b/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java index 8ce05e2fa115..c9f86b04be22 100644 --- a/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java @@ -23,7 +23,6 @@ import static android.hardware.display.DisplayManagerInternal.DisplayPowerReques import static android.provider.DeviceConfig.NAMESPACE_ATTENTION_MANAGER_SERVICE; import static android.view.Display.DEFAULT_DISPLAY_GROUP; -import static com.android.server.power.ScreenUndimDetector.DEFAULT_MAX_DURATION_BETWEEN_UNDIMS_MILLIS; import static com.android.server.power.ScreenUndimDetector.KEY_KEEP_SCREEN_ON_ENABLED; import static com.android.server.power.ScreenUndimDetector.KEY_MAX_DURATION_BETWEEN_UNDIMS_MILLIS; import static com.android.server.power.ScreenUndimDetector.KEY_UNDIMS_REQUIRED; @@ -49,6 +48,7 @@ import org.junit.runners.JUnit4; import java.util.Arrays; import java.util.List; +import java.util.concurrent.TimeUnit; /** * Tests for {@link com.android.server.power.ScreenUndimDetector} @@ -61,7 +61,8 @@ public class ScreenUndimDetectorTest { POLICY_DIM, POLICY_BRIGHT); private static final int OTHER_DISPLAY_GROUP = DEFAULT_DISPLAY_GROUP + 1; - + private static final long DEFAULT_MAX_DURATION_BETWEEN_UNDIMS_MILLIS = + TimeUnit.MINUTES.toMillis(5); @ClassRule public static final TestableContext sContext = new TestableContext( InstrumentationRegistry.getInstrumentation().getTargetContext(), null); @@ -88,7 +89,8 @@ public class ScreenUndimDetectorTest { @Before public void setup() { InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - + DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE, + KEY_KEEP_SCREEN_ON_ENABLED, Boolean.TRUE.toString(), false /*makeDefault*/); DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_UNDIMS_REQUIRED, Integer.toString(1), false /*makeDefault*/); @@ -108,10 +110,10 @@ public class ScreenUndimDetectorTest { @Test public void recordScreenPolicy_disabledByFlag_noop() { + setup(); DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_KEEP_SCREEN_ON_ENABLED, Boolean.FALSE.toString(), false /*makeDefault*/); mScreenUndimDetector.readValuesFromDeviceConfig(); - mScreenUndimDetector.recordScreenPolicy(DEFAULT_DISPLAY_GROUP, POLICY_DIM); mScreenUndimDetector.recordScreenPolicy(DEFAULT_DISPLAY_GROUP, POLICY_BRIGHT); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java index 5165e34c7fcd..fc864dd230d9 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java @@ -22,6 +22,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.spy; @@ -330,31 +331,24 @@ public class BatteryStatsHistoryTest { return invocation.callRealMethod(); }).when(mHistory).readFragmentToParcel(any(), any()); - // Prepare history for iteration - mHistory.iterate(0, MonotonicClock.UNDEFINED); - - Parcel parcel = mHistory.getNextParcel(0, Long.MAX_VALUE); - assertThat(parcel).isNotNull(); - assertThat(mReadFiles).containsExactly("123.bh"); - - // Skip to the end to force reading the next parcel - parcel.setDataPosition(parcel.dataSize()); - mReadFiles.clear(); - parcel = mHistory.getNextParcel(0, Long.MAX_VALUE); - assertThat(parcel).isNotNull(); - assertThat(mReadFiles).containsExactly("1000.bh"); - - parcel.setDataPosition(parcel.dataSize()); - mReadFiles.clear(); - parcel = mHistory.getNextParcel(0, Long.MAX_VALUE); - assertThat(parcel).isNotNull(); - assertThat(mReadFiles).containsExactly("2000.bh"); + int eventsRead = 0; + BatteryStatsHistoryIterator iterator = mHistory.iterate(0, MonotonicClock.UNDEFINED); + while (iterator.hasNext()) { + HistoryItem item = iterator.next(); + if (item.eventCode == HistoryItem.EVENT_JOB_START) { + eventsRead++; + assertThat(mReadFiles).containsExactly("123.bh"); + } else if (item.eventCode == HistoryItem.EVENT_JOB_FINISH) { + eventsRead++; + assertThat(mReadFiles).containsExactly("123.bh", "1000.bh"); + } else if (item.eventCode == HistoryItem.EVENT_ALARM) { + eventsRead++; + assertThat(mReadFiles).containsExactly("123.bh", "1000.bh", "2000.bh"); + } + } - parcel.setDataPosition(parcel.dataSize()); - mReadFiles.clear(); - parcel = mHistory.getNextParcel(0, Long.MAX_VALUE); - assertThat(parcel).isNull(); - assertThat(mReadFiles).isEmpty(); + assertThat(eventsRead).isEqualTo(3); + assertThat(mReadFiles).containsExactly("123.bh", "1000.bh", "2000.bh", "3000.bh"); } @Test @@ -372,25 +366,19 @@ public class BatteryStatsHistoryTest { return invocation.callRealMethod(); }).when(mHistory).readFragmentToParcel(any(), any()); - // Prepare history for iteration - mHistory.iterate(1000, 3000); - - Parcel parcel = mHistory.getNextParcel(1000, 3000); - assertThat(parcel).isNotNull(); - assertThat(mReadFiles).containsExactly("1000.bh"); - - // Skip to the end to force reading the next parcel - parcel.setDataPosition(parcel.dataSize()); - mReadFiles.clear(); - parcel = mHistory.getNextParcel(1000, 3000); - assertThat(parcel).isNotNull(); - assertThat(mReadFiles).containsExactly("2000.bh"); + BatteryStatsHistoryIterator iterator = mHistory.iterate(1000, 3000); + while (iterator.hasNext()) { + HistoryItem item = iterator.next(); + if (item.eventCode == HistoryItem.EVENT_JOB_START) { + fail("Event outside the range"); + } else if (item.eventCode == HistoryItem.EVENT_JOB_FINISH) { + assertThat(mReadFiles).containsExactly("1000.bh"); + } else if (item.eventCode == HistoryItem.EVENT_ALARM) { + fail("Event outside the range"); + } + } - parcel.setDataPosition(parcel.dataSize()); - mReadFiles.clear(); - parcel = mHistory.getNextParcel(1000, 3000); - assertThat(parcel).isNull(); - assertThat(mReadFiles).isEmpty(); + assertThat(mReadFiles).containsExactly("1000.bh", "2000.bh"); } private void prepareMultiFileHistory() { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java index ea83825cd810..69877c372442 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java @@ -660,6 +660,59 @@ public class AutoclickControllerTest { assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isEqualTo(-1); } + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void onMotionEvent_flagOn_lazyInitAutoclickScrollPanel() { + assertThat(mController.mAutoclickScrollPanel).isNull(); + + injectFakeMouseActionHoverMoveEvent(); + + assertThat(mController.mAutoclickScrollPanel).isNotNull(); + } + + @Test + @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void onMotionEvent_flagOff_notInitAutoclickScrollPanel() { + assertThat(mController.mAutoclickScrollPanel).isNull(); + + injectFakeMouseActionHoverMoveEvent(); + + assertThat(mController.mAutoclickScrollPanel).isNull(); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void onDestroy_flagOn_hideAutoclickScrollPanel() { + injectFakeMouseActionHoverMoveEvent(); + AutoclickScrollPanel mockAutoclickScrollPanel = mock(AutoclickScrollPanel.class); + mController.mAutoclickScrollPanel = mockAutoclickScrollPanel; + + mController.onDestroy(); + + verify(mockAutoclickScrollPanel).hide(); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void changeFromScrollToOtherClickType_hidesScrollPanel() { + injectFakeMouseActionHoverMoveEvent(); + + // Set active click type to SCROLL. + mController.clickPanelController.handleAutoclickTypeChange( + AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL); + + // Show the scroll panel. + mController.mAutoclickScrollPanel.show(); + assertThat(mController.mAutoclickScrollPanel.isVisible()).isTrue(); + + // Change click type to LEFT_CLICK. + mController.clickPanelController.handleAutoclickTypeChange( + AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK); + + // Verify scroll panel is hidden. + assertThat(mController.mAutoclickScrollPanel.isVisible()).isFalse(); + } + private void injectFakeMouseActionHoverMoveEvent() { MotionEvent event = getFakeMotionHoverMoveEvent(); event.setSource(InputDevice.SOURCE_MOUSE); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java new file mode 100644 index 000000000000..f445b50c7d9c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility.autoclick; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.times; + +import android.content.Context; +import android.testing.AndroidTestingRunner; +import android.testing.TestableContext; +import android.testing.TestableLooper; +import android.view.WindowManager; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Test cases for {@link AutoclickScrollPanel}. */ +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class AutoclickScrollPanelTest { + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Rule + public TestableContext mTestableContext = + new TestableContext(getInstrumentation().getContext()); + + @Mock private WindowManager mMockWindowManager; + private AutoclickScrollPanel mScrollPanel; + + @Before + public void setUp() { + mTestableContext.addMockSystemService(Context.WINDOW_SERVICE, mMockWindowManager); + mScrollPanel = new AutoclickScrollPanel(mTestableContext, mMockWindowManager); + } + + @Test + public void show_addsViewToWindowManager() { + mScrollPanel.show(); + + // Verify view is added to window manager. + verify(mMockWindowManager).addView(any(), any(WindowManager.LayoutParams.class)); + + // Verify isVisible reflects correct state. + assertThat(mScrollPanel.isVisible()).isTrue(); + } + + @Test + public void show_alreadyVisible_doesNotAddAgain() { + // Show twice. + mScrollPanel.show(); + mScrollPanel.show(); + + // Verify addView was only called once. + verify(mMockWindowManager, times(1)).addView(any(), any()); + } + + @Test + public void hide_removesViewFromWindowManager() { + // First show the panel. + mScrollPanel.show(); + // Then hide it. + mScrollPanel.hide(); + // Verify view is removed from window manager. + verify(mMockWindowManager).removeView(any()); + // Verify scroll panel is hidden. + assertThat(mScrollPanel.isVisible()).isFalse(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java index ae973be17904..56f802b278c6 100644 --- a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java @@ -89,8 +89,7 @@ public class DiscreteAppOpXmlPersistenceTest { int attributionChainId = AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE; mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, null, opFlags, - uidState, accessTime, duration, attributionFlags, attributionChainId, - DiscreteOpsXmlRegistry.ACCESS_TYPE_FINISH_OP); + uidState, accessTime, duration, attributionFlags, attributionChainId); // Verify in-memory object is correct fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime, @@ -121,8 +120,7 @@ public class DiscreteAppOpXmlPersistenceTest { int attributionChainId = 10; mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, null, opFlags, - uidState, accessTime, duration, attributionFlags, attributionChainId, - DiscreteOpsXmlRegistry.ACCESS_TYPE_START_OP); + uidState, accessTime, duration, attributionFlags, attributionChainId); fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime, duration, uidState, opFlags, attributionFlags, attributionChainId); diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java index 8eea1c73d4f2..6c66f149baa7 100644 --- a/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java @@ -70,7 +70,7 @@ public class DiscreteOpsMigrationAndRollbackTest { opEvent.getDeviceId(), opEvent.getOpCode(), opEvent.getAttributionTag(), opEvent.getOpFlags(), opEvent.getUidState(), opEvent.getAccessTime(), opEvent.getDuration(), opEvent.getAttributionFlags(), - (int) opEvent.getChainId(), DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP); + (int) opEvent.getChainId()); } xmlRegistry.writeAndClearOldAccessHistory(); assertThat(xmlRegistry.readLargestChainIdFromDiskLocked()).isEqualTo(RECORD_COUNT); @@ -104,7 +104,7 @@ public class DiscreteOpsMigrationAndRollbackTest { opEvent.getDeviceId(), opEvent.getOpCode(), opEvent.getAttributionTag(), opEvent.getOpFlags(), opEvent.getUidState(), opEvent.getAccessTime(), opEvent.getDuration(), opEvent.getAttributionFlags(), - (int) opEvent.getChainId(), DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP); + (int) opEvent.getChainId()); } // flush records from cache to the database. sqlRegistry.shutdown(); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index 30aa8cebdff6..01bcc2584fe1 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -143,6 +143,7 @@ import android.provider.DeviceConfig; import android.provider.Settings; import android.security.KeyChain; import android.security.keystore.AttestationUtils; +import android.telephony.SubscriptionInfo; import android.telephony.TelephonyManager; import android.telephony.data.ApnSetting; import android.test.MoreAsserts; @@ -8715,6 +8716,47 @@ public class DevicePolicyManagerTest extends DpmTestBase { } } + @RequiresFlagsEnabled(Flags.FLAG_REMOVE_MANAGED_ESIM_ON_WORK_PROFILE_DELETION) + @Test + public void testManagedProfileDeleted_managedEmbeddedSubscriptionDeleted() throws Exception { + // Setup PO mode. + setupProfileOwner(); + // Mock SubscriptionManager to return a subscription managed by the profile owner package. + int managedSubscriptionId = 42; + SubscriptionInfo managedSubscription = new SubscriptionInfo.Builder().setCardId(1).setId( + managedSubscriptionId).setGroupOwner(admin1.getPackageName()).build(); + when(getServices().subscriptionManager.getAvailableSubscriptionInfoList()).thenReturn( + List.of(managedSubscription)); + + // Send a ACTION_MANAGED_PROFILE_REMOVED broadcast to emulate a managed profile being + // removed. + sendBroadcastWithUser(dpms, Intent.ACTION_MANAGED_PROFILE_REMOVED, CALLER_USER_HANDLE); + + // Verify that EuiccManager was called to delete the subscription. + verify(getServices().euiccManager).deleteSubscription(eq(managedSubscriptionId), any()); + } + + @RequiresFlagsDisabled(Flags.FLAG_REMOVE_MANAGED_ESIM_ON_WORK_PROFILE_DELETION) + @Test + public void testManagedProfileDeleted_flagDisabled_managedEmbeddedSubscriptionDeleted() + throws Exception { + // Set up PO mode. + setupProfileOwner(); + // Mock SubscriptionManager to return a subscription managed by the profile owner package. + int managedSubscriptionId = 42; + SubscriptionInfo managedSubscription = new SubscriptionInfo.Builder().setCardId(1).setId( + managedSubscriptionId).setGroupOwner(admin1.getPackageName()).build(); + when(getServices().subscriptionManager.getAvailableSubscriptionInfoList()).thenReturn( + List.of(managedSubscription)); + + // Send a ACTION_MANAGED_PROFILE_REMOVED broadcast to emulate a managed profile being + // removed. + sendBroadcastWithUser(dpms, Intent.ACTION_MANAGED_PROFILE_REMOVED, CALLER_USER_HANDLE); + + // Verify that EuiccManager was not called to delete the subscription. + verifyZeroInteractions(getServices().euiccManager); + } + private void setupVpnAuthorization(String userVpnPackage, int userVpnUid) { final AppOpsManager.PackageOps vpnOp = new AppOpsManager.PackageOps(userVpnPackage, userVpnUid, List.of(new AppOpsManager.OpEntry( diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java index 00b0c558b4e3..479af73bae3c 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java @@ -253,6 +253,8 @@ public class DpmMockContext extends MockContext { return mMockSystemServices.subscriptionManager; case Context.USB_SERVICE: return mMockSystemServices.usbManager; + case Context.EUICC_SERVICE: + return mMockSystemServices.euiccManager; } throw new UnsupportedOperationException(); } @@ -487,6 +489,14 @@ public class DpmMockContext extends MockContext { } @Override + public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user, + IntentFilter filter, String broadcastPermission, Handler scheduler, int flags) { + mMockSystemServices.registerReceiver(receiver, filter, scheduler); + return spiedContext.registerReceiverAsUser(receiver, user, filter, broadcastPermission, + scheduler, flags); + } + + @Override public void unregisterReceiver(BroadcastReceiver receiver) { mMockSystemServices.unregisterReceiver(receiver); spiedContext.unregisterReceiver(receiver); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java index 3e4448c1dafa..d01fa91e22c2 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java @@ -68,6 +68,7 @@ import android.provider.Settings; import android.security.KeyChain; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; +import android.telephony.euicc.EuiccManager; import android.test.mock.MockContentProvider; import android.test.mock.MockContentResolver; import android.util.ArrayMap; @@ -151,6 +152,7 @@ public class MockSystemServices { public final File dataDir; public final PolicyPathProvider pathProvider; public final SupervisionManagerInternal supervisionManagerInternal; + public final EuiccManager euiccManager; private final Map<String, PackageState> mTestPackageStates = new ArrayMap<>(); @@ -206,6 +208,7 @@ public class MockSystemServices { roleManagerForMock = mock(RoleManagerForMock.class); subscriptionManager = mock(SubscriptionManager.class); supervisionManagerInternal = mock(SupervisionManagerInternal.class); + euiccManager = mock(EuiccManager.class); // Package manager is huge, so we use a partial mock instead. packageManager = spy(realContext.getPackageManager()); diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java index 2da2f50447c7..e836780b3f71 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java @@ -16,6 +16,7 @@ package com.android.server.locksettings; +import static android.content.pm.UserInfo.FLAG_FOR_TESTING; import static android.content.pm.UserInfo.FLAG_FULL; import static android.content.pm.UserInfo.FLAG_MAIN; import static android.content.pm.UserInfo.FLAG_PRIMARY; @@ -44,6 +45,8 @@ import static org.mockito.Mockito.when; import android.app.PropertyInvalidatedCache; import android.app.admin.PasswordMetrics; +import android.content.ComponentName; +import android.content.pm.UserInfo; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; @@ -357,6 +360,45 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { } @Test + public void testEscrowDataRetainedWhenManagedUserVerifiesCredential() throws RemoteException { + when(mDeviceStateCache.isUserOrganizationManaged(anyInt())).thenReturn(true); + + LockscreenCredential password = newPassword("password"); + initSpAndSetCredential(PRIMARY_USER_ID, password); + + mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */); + + assertTrue("Escrow data was destroyed", mSpManager.hasEscrowData(PRIMARY_USER_ID)); + } + + @Test + public void testEscrowDataRetainedWhenUnmanagedTestUserVerifiesCredential() + throws RemoteException { + when(mDeviceStateCache.isUserOrganizationManaged(anyInt())).thenReturn(false); + UserInfo userInfo = mUserManagerInternal.getUserInfo(PRIMARY_USER_ID); + userInfo.flags |= FLAG_FOR_TESTING; + + LockscreenCredential password = newPassword("password"); + initSpAndSetCredential(PRIMARY_USER_ID, password); + + mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */); + + assertTrue("Escrow data was destroyed", mSpManager.hasEscrowData(PRIMARY_USER_ID)); + } + + @Test + public void testEscrowDataDeletedWhenUnmanagedUserVerifiesCredential() throws RemoteException { + when(mDeviceStateCache.isUserOrganizationManaged(anyInt())).thenReturn(false); + + LockscreenCredential password = newPassword("password"); + initSpAndSetCredential(PRIMARY_USER_ID, password); + + mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */); + + assertFalse("Escrow data wasn't destroyed", mSpManager.hasAnyEscrowData(PRIMARY_USER_ID)); + } + + @Test public void testTokenBasedClearPassword() throws RemoteException { LockscreenCredential password = newPassword("password"); LockscreenCredential pattern = newPattern("123654"); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java index 04335df8c454..2baf0c141c89 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java @@ -247,6 +247,7 @@ public class VibrationThreadTest { assertThat(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()).isEmpty(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_singleVibratorWaveform_runsVibrationAndChangesAmplitudes() { mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); @@ -269,7 +270,10 @@ public class VibrationThreadTest { } @Test - @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + @EnableFlags({ + Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED, + Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING, + }) public void vibrate_singleWaveformWithAdaptiveHapticsScaling_scalesAmplitudesProperly() { // No user settings scale. setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, @@ -296,7 +300,10 @@ public class VibrationThreadTest { } @Test - @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + @EnableFlags({ + Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED, + Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING, + }) public void vibrate_withVibrationParamsRequestStalling_timeoutRequestAndApplyNoScaling() { // No user settings scale. setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, @@ -357,6 +364,7 @@ public class VibrationThreadTest { } } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_singleVibratorRepeatingShortAlwaysOnWaveform_turnsVibratorOnForLonger() throws Exception { @@ -380,6 +388,7 @@ public class VibrationThreadTest { .containsExactly(expectedOneShot(5000)).inOrder(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_singleVibratorPatternWithZeroDurationSteps_skipsZeroDurationSteps() { mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); @@ -399,6 +408,7 @@ public class VibrationThreadTest { .containsExactlyElementsIn(expectedOneShots(100L, 150L)).inOrder(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_singleVibratorPatternWithZeroDurationAndAmplitude_skipsZeroDurationSteps() { mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); @@ -537,6 +547,7 @@ public class VibrationThreadTest { assertThat(fakeVibrator.getEffectSegments(vibration.id)).hasSize(10); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_singleVibratorRepeatingLongAlwaysOnWaveform_turnsVibratorOnForACycle() throws Exception { @@ -700,6 +711,7 @@ public class VibrationThreadTest { .containsExactly(expectedPrebaked(VibrationEffect.EFFECT_THUD)).inOrder(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_singleVibratorPrebakedAndUnsupportedEffectWithFallback_runsFallback() { mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); @@ -1247,6 +1259,7 @@ public class VibrationThreadTest { .containsExactly(expected).inOrder(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_multipleStereo_runsVibrationOnRightVibrators() { mockVibrators(1, 2, 3, 4); @@ -1479,6 +1492,7 @@ public class VibrationThreadTest { assertThat(mVibratorProviders.get(1).getAmplitudes()).isEmpty(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_multipleWaveforms_playsWaveformsInParallel() throws Exception { mockVibrators(1, 2, 3); @@ -1544,8 +1558,8 @@ public class VibrationThreadTest { VibrationEffect.createOneShot( expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE))); - startThreadAndDispatcher(vibration); long startTime = SystemClock.elapsedRealtime(); + startThreadAndDispatcher(vibration); vibration.waitForEnd(); long vibrationEndTime = SystemClock.elapsedRealtime(); @@ -1554,15 +1568,13 @@ public class VibrationThreadTest { long completionTime = SystemClock.elapsedRealtime(); verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); - // Vibration ends after duration, thread completed after ramp down - assertThat(vibrationEndTime - startTime).isAtLeast(expectedDuration); + // Vibration ends before ramp down, thread completed after ramp down assertThat(vibrationEndTime - startTime).isLessThan(expectedDuration + rampDownDuration); assertThat(completionTime - startTime).isAtLeast(expectedDuration + rampDownDuration); } @Test - public void vibrate_withVibratorCallbackDelayShorterThanTimeout_vibrationFinishedAfterDelay() - throws Exception { + public void vibrate_withVibratorCallbackDelayShorterThanTimeout_vibrationFinishedAfterDelay() { long expectedDuration = 10; long callbackDelay = VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT / 2; @@ -1577,10 +1589,8 @@ public class VibrationThreadTest { long startTime = SystemClock.elapsedRealtime(); startThreadAndDispatcher(vibration); - vibration.waitForEnd(); - long vibrationEndTime = SystemClock.elapsedRealtime(); - waitForCompletion(TEST_TIMEOUT_MILLIS); + long vibrationEndTime = SystemClock.elapsedRealtime(); verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); assertThat(vibrationEndTime - startTime).isAtLeast(expectedDuration + callbackDelay); @@ -1588,8 +1598,7 @@ public class VibrationThreadTest { @LargeTest @Test - public void vibrate_withVibratorCallbackDelayLongerThanTimeout_vibrationFinishedAfterTimeout() - throws Exception { + public void vibrate_withVibratorCallbackDelayLongerThanTimeout_vibrationFinishedAfterTimeout() { long expectedDuration = 10; long callbackTimeout = VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT; long callbackDelay = callbackTimeout * 2; @@ -1602,21 +1611,17 @@ public class VibrationThreadTest { VibrationEffect.createOneShot( expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE))); - startThreadAndDispatcher(vibration); long startTime = SystemClock.elapsedRealtime(); - - vibration.waitForEnd(); - long vibrationEndTime = SystemClock.elapsedRealtime(); + startThreadAndDispatcher(vibration); waitForCompletion(callbackDelay + TEST_TIMEOUT_MILLIS); - long completionTime = SystemClock.elapsedRealtime(); + long vibrationEndTime = SystemClock.elapsedRealtime(); verify(mControllerCallbacks, never()) .onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); // Vibration ends and thread completes after timeout, before the HAL callback assertThat(vibrationEndTime - startTime).isAtLeast(expectedDuration + callbackTimeout); assertThat(vibrationEndTime - startTime).isLessThan(expectedDuration + callbackDelay); - assertThat(completionTime - startTime).isLessThan(expectedDuration + callbackDelay); } @LargeTest @@ -1637,8 +1642,8 @@ public class VibrationThreadTest { Arrays.fill(amplitudes, VibrationEffect.DEFAULT_AMPLITUDE); VibrationEffect effect = VibrationEffect.createWaveform(timings, amplitudes, -1); - startThreadAndDispatcher(effect); long startTime = SystemClock.elapsedRealtime(); + startThreadAndDispatcher(effect); waitForCompletion(totalDuration + TEST_TIMEOUT_MILLIS); long delay = Math.abs(SystemClock.elapsedRealtime() - startTime - totalDuration); @@ -1806,6 +1811,7 @@ public class VibrationThreadTest { assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_waveformWithRampDown_addsRampDownAfterVibrationCompleted() { when(mVibrationConfigMock.getRampDownDurationMs()).thenReturn(15); @@ -1833,6 +1839,7 @@ public class VibrationThreadTest { } } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_waveformWithRampDown_triggersCallbackWhenOriginalVibrationEnds() throws Exception { @@ -1863,6 +1870,7 @@ public class VibrationThreadTest { verify(mManagerHooks).onVibrationThreadReleased(vibration.id); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_waveformCancelledWithRampDown_addsRampDownAfterVibrationCancelled() throws Exception { @@ -2057,6 +2065,7 @@ public class VibrationThreadTest { .containsExactly(expectedPrebaked(EFFECT_CLICK)).inOrder(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_multipleVibratorsSequentialInSession_runsInOrderWithoutDelaysAndNoOffs() { mockVibrators(1, 2, 3); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java index 948371f74a9c..ad706e879b72 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java @@ -63,11 +63,23 @@ public class ActivitySnapshotControllerTests extends TaskSnapshotPersisterTestBa super(0.8f /* highResScale */, 0.5f /* lowResScale */); } + private class TestActivitySnapshotController extends ActivitySnapshotController { + TestActivitySnapshotController(WindowManagerService service, + SnapshotPersistQueue persistQueue) { + super(service, persistQueue); + } + @Override + BaseAppSnapshotPersister.PersistInfoProvider createPersistInfoProvider( + WindowManagerService service) { + return mPersister.mPersistInfoProvider; + } + } @Override @Before public void setUp() { super.setUp(); - mActivitySnapshotController = new ActivitySnapshotController(mWm, mSnapshotPersistQueue); + mActivitySnapshotController = new TestActivitySnapshotController( + mWm, mSnapshotPersistQueue); spyOn(mActivitySnapshotController); doReturn(false).when(mActivitySnapshotController).shouldDisableSnapshots(); mActivitySnapshotController.resetTmpFields(); diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java index 43755ea3165e..1e91bedb5c18 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java @@ -181,7 +181,6 @@ public class DesktopModeHelperTest { assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue(); } - @DisableFlags(Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE) @Test public void isDeviceEligibleForDesktopMode_configDEModeOffAndIntDispHostsDesktop_returnsFalse() { doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java index 51ea498811fc..f22ecb5eb3f9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java @@ -68,11 +68,8 @@ public class TaskSnapshotLowResDisabledTest extends TaskSnapshotPersisterTestBas public void testPersistAndLoadSnapshot() { mPersister.persistSnapshot(1, mTestUserId, createSnapshot()); mSnapshotPersistQueue.waitForQueueEmpty(); - final File[] files = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.jpg")}; - final File[] nonExistsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg")}; + final File[] files = convertFilePath("1.proto", "1.jpg"); + final File[] nonExistsFiles = convertFilePath("1_reduced.proto"); assertTrueForFiles(files, File::exists, " must exist"); assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist"); final TaskSnapshot snapshot = mLoader.loadTask(1, mTestUserId, false /* isLowResolution */); @@ -92,14 +89,9 @@ public class TaskSnapshotLowResDisabledTest extends TaskSnapshotPersisterTestBas taskIds.add(1); mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId}); mSnapshotPersistQueue.waitForQueueEmpty(); - final File[] existsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.jpg")}; - final File[] nonExistsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/2.proto"), - new File(FILES_DIR.getPath() + "/snapshots/2.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/2_reduced.jpg")}; + final File[] existsFiles = convertFilePath("1.proto", "1.jpg"); + final File[] nonExistsFiles = convertFilePath("1_reduced.proto", "2.proto", "2.jpg", + "2_reduced.jpg"); assertTrueForFiles(existsFiles, File::exists, " must exist"); assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist"); } @@ -112,14 +104,8 @@ public class TaskSnapshotLowResDisabledTest extends TaskSnapshotPersisterTestBas mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId}); mPersister.persistSnapshot(2, mTestUserId, createSnapshot()); mSnapshotPersistQueue.waitForQueueEmpty(); - final File[] existsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/2.proto"), - new File(FILES_DIR.getPath() + "/snapshots/2.jpg")}; - final File[] nonExistsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/2_reduced.jpg")}; + final File[] existsFiles = convertFilePath("1.proto", "1.jpg", "2.proto", "2.jpg"); + final File[] nonExistsFiles = convertFilePath("1_reduced.jpg", "2_reduced.jpg"); assertTrueForFiles(existsFiles, File::exists, " must exist"); assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist"); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java index 4b54e4464ca7..af06c14516a1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java @@ -75,9 +75,7 @@ public class TaskSnapshotPersisterLoaderTest extends TaskSnapshotPersisterTestBa public void testPersistAndLoadSnapshot() { mPersister.persistSnapshot(1, mTestUserId, createSnapshot()); mSnapshotPersistQueue.waitForQueueEmpty(); - final File[] files = new File[]{new File(FILES_DIR.getPath() + "/snapshots/1.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg")}; + final File[] files = convertFilePath("1.proto", "1.jpg", "1_reduced.jpg"); assertTrueForFiles(files, File::exists, " must exist"); final TaskSnapshot snapshot = mLoader.loadTask(1, mTestUserId, false /* isLowResolution */); assertNotNull(snapshot); @@ -140,13 +138,8 @@ public class TaskSnapshotPersisterLoaderTest extends TaskSnapshotPersisterTestBa mSnapshotPersistQueue.waitForQueueEmpty(); // Make sure 1,2 were purged but removeObsoleteFiles wasn't. - final File[] existsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/3.proto"), - new File(FILES_DIR.getPath() + "/snapshots/4.proto")}; - final File[] nonExistsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/100.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.proto")}; + final File[] existsFiles = convertFilePath("3.proto", "4.proto"); + final File[] nonExistsFiles = convertFilePath("100.proto", "1.proto", "2.proto"); assertTrueForFiles(existsFiles, File::exists, " must exist"); assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist"); } @@ -427,14 +420,8 @@ public class TaskSnapshotPersisterLoaderTest extends TaskSnapshotPersisterTestBa taskIds.add(1); mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId}); mSnapshotPersistQueue.waitForQueueEmpty(); - final File[] existsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg")}; - final File[] nonExistsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/2.proto"), - new File(FILES_DIR.getPath() + "/snapshots/2.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/2_reduced.jpg")}; + final File[] existsFiles = convertFilePath("1.proto", "1.jpg", "1_reduced.jpg"); + final File[] nonExistsFiles = convertFilePath("2.proto", "2.jpg", "2_reduced.jpg"); assertTrueForFiles(existsFiles, File::exists, " must exist"); assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist"); } @@ -447,13 +434,8 @@ public class TaskSnapshotPersisterLoaderTest extends TaskSnapshotPersisterTestBa mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId}); mPersister.persistSnapshot(2, mTestUserId, createSnapshot()); mSnapshotPersistQueue.waitForQueueEmpty(); - final File[] existsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/2.proto"), - new File(FILES_DIR.getPath() + "/snapshots/2.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/2_reduced.jpg")}; + final File[] existsFiles = convertFilePath("1.proto", "1.jpg", "1_reduced.jpg", "2.proto", + "2.jpg", "2_reduced.jpg"); assertTrueForFiles(existsFiles, File::exists, " must exist"); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java index 1e16c97de647..b2c195e8ebaa 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import android.annotation.NonNull; import android.content.ComponentName; import android.content.ContextWrapper; import android.content.res.Resources; @@ -46,6 +47,7 @@ import android.window.TaskSnapshot; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider; +import com.android.window.flags.Flags; import org.junit.After; import org.junit.AfterClass; @@ -129,12 +131,33 @@ class TaskSnapshotPersisterTestBase extends WindowTestsBase { return; } for (File file : files) { - if (!file.isDirectory()) { - file.delete(); + if (file.isDirectory()) { + final File[] subFiles = file.listFiles(); + if (subFiles == null) { + continue; + } + for (File subFile : subFiles) { + subFile.delete(); + } } + file.delete(); } } + File[] convertFilePath(@NonNull String... fileNames) { + final File[] files = new File[fileNames.length]; + final String path; + if (Flags.scrambleSnapshotFileName()) { + path = mPersister.mPersistInfoProvider.getDirectory(mTestUserId).getPath(); + } else { + path = FILES_DIR.getPath() + "/snapshots/"; + } + for (int i = 0; i < fileNames.length; i++) { + files[i] = new File(path + fileNames[i]); + } + return files; + } + TaskSnapshot createSnapshot() { return new TaskSnapshotBuilder().setTopActivityComponent(getUniqueComponentName()).build(); } diff --git a/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt b/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt index 4995eebdd79e..fe72dae9ff34 100644 --- a/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt +++ b/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt @@ -155,35 +155,35 @@ class IntDefProcessor : AbstractProcessor() { ) { val indent = " " - writer.appendln("{") + writer.appendLine("{") val intDefTypesCount = annotationTypeToIntDefMapping.size var currentIntDefTypesCount = 0 for ((field, intDefMapping) in annotationTypeToIntDefMapping) { - writer.appendln("""$indent"$field": {""") + writer.appendLine("""$indent"$field": {""") // Start IntDef - writer.appendln("""$indent$indent"flag": ${intDefMapping.flag},""") + writer.appendLine("""$indent$indent"flag": ${intDefMapping.flag},""") - writer.appendln("""$indent$indent"values": {""") + writer.appendLine("""$indent$indent"values": {""") intDefMapping.entries.joinTo(writer, separator = ",\n") { (value, identifier) -> """$indent$indent$indent"$value": "$identifier"""" } - writer.appendln() - writer.appendln("$indent$indent}") + writer.appendLine() + writer.appendLine("$indent$indent}") // End IntDef writer.append("$indent}") if (++currentIntDefTypesCount < intDefTypesCount) { - writer.appendln(",") + writer.appendLine(",") } else { - writer.appendln("") + writer.appendLine("") } } - writer.appendln("}") + writer.appendLine("}") } } } |