diff options
213 files changed, 3966 insertions, 1828 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index f5dcf2de4c51..9ebb5068bf19 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -18112,6 +18112,7 @@ package android.graphics.drawable { method public void setThickness(@Px int); method public void setThicknessRatio(@FloatRange(from=0.0f, fromInclusive=false) float); method public void setUseLevel(boolean); + field @FlaggedApi("com.android.graphics.flags.gradient_drawable_shape_rounded_cap") public static final int ARC = 4; // 0x4 field public static final int LINE = 2; // 0x2 field public static final int LINEAR_GRADIENT = 0; // 0x0 field public static final int OVAL = 1; // 0x1 diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index 4b1afa517122..01b2953362b5 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -146,6 +146,7 @@ interface IActivityTaskManager { int getFrontActivityScreenCompatMode(); void setFrontActivityScreenCompatMode(int mode); void setFocusedTask(int taskId); + boolean setTaskIsPerceptible(int taskId, boolean isPerceptible); boolean removeTask(int taskId); void removeAllVisibleRecentTasks(); List<ActivityManager.RunningTaskInfo> getTasks(int maxNum, boolean filterOnlyVisibleRecents, diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 08719fc549f8..500f7585b673 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -14200,6 +14200,9 @@ public class DevicePolicyManager { * <li>Manifest.permission.ACTIVITY_RECOGNITION</li> * <li>Manifest.permission.BODY_SENSORS</li> * </ul> + * On devices running {@link android.os.Build.VERSION_CODES#BAKLAVA}, the + * {@link android.health.connect.HealthPermissions} are also included in the + * restricted list. * <p> * A profile owner may not grant these permissions (i.e. call this method with any of the * permissions listed above and {@code grantState} of {@code #PERMISSION_GRANT_STATE_GRANTED}), diff --git a/core/java/android/app/contextualsearch/flags.aconfig b/core/java/android/app/contextualsearch/flags.aconfig index bc1f7cea7fce..1de034b23a7a 100644 --- a/core/java/android/app/contextualsearch/flags.aconfig +++ b/core/java/android/app/contextualsearch/flags.aconfig @@ -24,7 +24,7 @@ flag { } flag { - name: "contextual_search_window_layer" + name: "contextual_search_prevent_self_capture" namespace: "sysui_integrations" description: "Identify live contextual search UI to exclude from contextual search screenshot." bug: "390176823" diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index f91b2474fdac..9e91f5944504 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -2038,7 +2038,7 @@ public abstract class PackageManager { public static final int INSTALL_SCENARIO_DEFAULT = 0; /** - * Installation scenario providing the fastest “install button to launch" experience possible. + * Installation scenario providing the fastest "install button to launch" experience possible. */ public static final int INSTALL_SCENARIO_FAST = 1; @@ -3585,7 +3585,7 @@ public abstract class PackageManager { /** * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: The device is - * compatible with Android’s security model. + * compatible with Android's security model. * * <p>See sections 2 and 9 in the * <a href="https://source.android.com/compatibility/android-cdd">Android CDD</a> for more diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index ef200c328d63..2e0999410483 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -2358,8 +2358,13 @@ public final class Configuration implements Parcelable, Comparable<Configuration * @param locales The locale list. If null, an empty LocaleList will be assigned. */ public void setLocales(@Nullable LocaleList locales) { + LocaleList oldList = mLocaleList; mLocaleList = locales == null ? LocaleList.getEmptyLocaleList() : locales; locale = mLocaleList.get(0); + if (!mLocaleList.equals(oldList)) { + Slog.v(TAG, "Updating configuration, locales updated from " + oldList + + " to " + mLocaleList); + } setLayoutDirection(locale); } diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index 96c71765d102..eec30f38b415 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -491,6 +491,9 @@ public class ResourcesImpl { } defaultLocale = adjustLanguageTag(lc.getDefaultLocale().toLanguageTag()); + Slog.v(TAG, "Updating configuration, with default locale " + + defaultLocale + " and selected locales " + + Arrays.toString(selectedLocales)); } else { String[] availableLocales; // The LocaleList has changed. We must query the AssetManager's @@ -526,6 +529,7 @@ public class ResourcesImpl { for (int i = 0; i < locales.size(); i++) { selectedLocales[i] = adjustLanguageTag(locales.get(i).toLanguageTag()); } + defaultLocale = adjustLanguageTag(lc.getDefaultLocale().toLanguageTag()); } else { selectedLocales = new String[]{ adjustLanguageTag(locales.get(0).toLanguageTag())}; diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java index 74972346bf2e..3c03bb5626c8 100644 --- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java +++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java @@ -130,7 +130,7 @@ public final class MessageQueue { MessageQueue(boolean quitAllowed) { initIsProcessAllowedToUseConcurrent(); - mUseConcurrent = sIsProcessAllowedToUseConcurrent; + mUseConcurrent = sIsProcessAllowedToUseConcurrent && !isInstrumenting(); mQuitAllowed = quitAllowed; mPtr = nativeInit(); mThread = Thread.currentThread(); @@ -202,6 +202,15 @@ public final class MessageQueue { return; } + private static boolean isInstrumenting() { + final ActivityThread activityThread = ActivityThread.currentActivityThread(); + if (activityThread == null) { + return false; + } + final Instrumentation instrumentation = activityThread.getInstrumentation(); + return instrumentation != null && instrumentation.isInstrumenting(); + } + @Override protected void finalize() throws Throwable { try { diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 4ebfe53cab58..538283e10738 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -13017,18 +13017,16 @@ public final class Settings { * false/0. * @hide */ - @Readable public static final String REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI = "redact_otp_on_wifi"; /** - * Toggle for whether to immediately redact OTP notifications, or require the device to be - * locked for 10 minutes. Defaults to false/0 + * Time (in milliseconds) that the device should need to be locked, in order for an OTP + * notification to be redacted. Default is 10 minutes (600,000 ms) * @hide */ - @Readable - public static final String REDACT_OTP_NOTIFICATION_IMMEDIATELY = - "remove_otp_redaction_delay"; + public static final String OTP_NOTIFICATION_REDACTION_LOCK_TIME = + "otp_redaction_lock_time"; /** * These entries are considered common between the personal and the managed profile, diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java index df680c054f56..fe868e1c43be 100644 --- a/core/java/android/view/NotificationHeaderView.java +++ b/core/java/android/view/NotificationHeaderView.java @@ -223,8 +223,14 @@ public class NotificationHeaderView extends RelativeLayout { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (notificationsRedesignTemplates()) { - mTopLineTranslation = measureCenterTranslation(mTopLineView); - mExpandButtonTranslation = measureCenterTranslation(mExpandButton); + // TODO: b/378660052 - These should never be null in practice, consider using + // requireViewById() in the onFinishInflate. + if (mTopLineView != null) { + mTopLineTranslation = measureCenterTranslation(mTopLineView); + } + if (mExpandButton != null) { + mExpandButtonTranslation = measureCenterTranslation(mExpandButton); + } } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 0866e0d832b1..c048d7987515 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -66,6 +66,7 @@ import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFI import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION; import static com.android.window.flags.Flags.FLAG_DELEGATE_UNHANDLED_DRAGS; import static com.android.window.flags.Flags.FLAG_SUPPORTS_DRAG_ASSISTANT_TO_MULTIWINDOW; +import static com.android.window.flags.Flags.reduceChangedExclusionRectsMsgs; import static java.lang.Math.max; @@ -247,6 +248,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; @@ -12888,15 +12890,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (rects.isEmpty() && mListenerInfo == null) return; final ListenerInfo info = getListenerInfo(); + final boolean rectsChanged = !reduceChangedExclusionRectsMsgs() + || !Objects.equals(info.mSystemGestureExclusionRects, rects); if (info.mSystemGestureExclusionRects != null) { - info.mSystemGestureExclusionRects.clear(); - info.mSystemGestureExclusionRects.addAll(rects); + if (rectsChanged) { + info.mSystemGestureExclusionRects.clear(); + info.mSystemGestureExclusionRects.addAll(rects); + } } else { info.mSystemGestureExclusionRects = new ArrayList<>(rects); } - - updatePositionUpdateListener(); - postUpdate(this::updateSystemGestureExclusionRects); + if (rectsChanged) { + updatePositionUpdateListener(); + postUpdate(this::updateSystemGestureExclusionRects); + } } private void updatePositionUpdateListener() { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index cb9832282dc4..9498407fb33b 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -136,6 +136,7 @@ import static com.android.text.flags.Flags.disableHandwritingInitiatorForIme; import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay; import static com.android.window.flags.Flags.enableWindowContextResourcesUpdateOnConfigChange; import static com.android.window.flags.Flags.predictiveBackSwipeEdgeNoneApi; +import static com.android.window.flags.Flags.reduceChangedExclusionRectsMsgs; import static com.android.window.flags.Flags.setScPropertiesInClient; import android.Manifest; @@ -6074,8 +6075,12 @@ public final class ViewRootImpl implements ViewParent, } void updateSystemGestureExclusionRectsForView(View view) { + boolean msgInQueue = reduceChangedExclusionRectsMsgs() + && mGestureExclusionTracker.isWaitingForComputeChanges(); mGestureExclusionTracker.updateRectsForView(view); - mHandler.sendEmptyMessage(MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED); + if (!msgInQueue) { + mHandler.sendEmptyMessage(MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED); + } } void systemGestureExclusionChanged() { @@ -6119,8 +6124,12 @@ public final class ViewRootImpl implements ViewParent, * the root's view hierarchy. */ public void setRootSystemGestureExclusionRects(@NonNull List<Rect> rects) { + boolean msgInQueue = reduceChangedExclusionRectsMsgs() + && mGestureExclusionTracker.isWaitingForComputeChanges(); mGestureExclusionTracker.setRootRects(rects); - mHandler.sendEmptyMessage(MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED); + if (!msgInQueue) { + mHandler.sendEmptyMessage(MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED); + } } /** diff --git a/core/java/android/view/ViewRootRectTracker.java b/core/java/android/view/ViewRootRectTracker.java index 152729b8d1d8..0bef3255f1dc 100644 --- a/core/java/android/view/ViewRootRectTracker.java +++ b/core/java/android/view/ViewRootRectTracker.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import java.lang.ref.WeakReference; @@ -32,8 +33,10 @@ import java.util.function.Function; /** * Abstract class to track a collection of rects reported by the views under the same * {@link ViewRootImpl}. + * @hide */ -class ViewRootRectTracker { +@VisibleForTesting +public class ViewRootRectTracker { private final Function<View, List<Rect>> mRectCollector; private boolean mViewsChanged = false; private boolean mRootRectsChanged = false; @@ -41,11 +44,18 @@ class ViewRootRectTracker { private List<ViewInfo> mViewInfos = new ArrayList<>(); private List<Rect> mRects = Collections.emptyList(); + // Keeps track of whether updateRectsForView has been called but there was no subsequent call + // on computeChanges yet. Since updateRectsForView is called when sending a message and + // computeChanges when it is received, this tracks whether such message is in the queue already + private boolean mWaitingForComputeChanges = false; + /** * @param rectCollector given a view returns a list of the rects of interest for this * ViewRootRectTracker + * @hide */ - ViewRootRectTracker(Function<View, List<Rect>> rectCollector) { + @VisibleForTesting + public ViewRootRectTracker(Function<View, List<Rect>> rectCollector) { mRectCollector = rectCollector; } @@ -70,6 +80,7 @@ class ViewRootRectTracker { mViewInfos.add(new ViewInfo(view)); mViewsChanged = true; } + mWaitingForComputeChanges = true; } /** @@ -92,6 +103,7 @@ class ViewRootRectTracker { * @return {@code true} if there were changes, {@code false} otherwise. */ public boolean computeChanges() { + mWaitingForComputeChanges = false; boolean changed = mRootRectsChanged; final Iterator<ViewInfo> i = mViewInfos.iterator(); final List<Rect> rects = new ArrayList<>(mRootRects); @@ -121,6 +133,10 @@ class ViewRootRectTracker { return false; } + public boolean isWaitingForComputeChanges() { + return mWaitingForComputeChanges; + } + /** * Returns a List of all Rects from all visible Views in the global (root) coordinate system. * This list is only updated when calling {@link #computeChanges()} or @@ -140,6 +156,7 @@ class ViewRootRectTracker { Preconditions.checkNotNull(rects, "rects must not be null"); mRootRects = rects; mRootRectsChanged = true; + mWaitingForComputeChanges = true; } @NonNull diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java index e3ea6b229d64..a952967f4daf 100644 --- a/core/java/android/view/WindowInsets.java +++ b/core/java/android/view/WindowInsets.java @@ -1290,7 +1290,8 @@ public final class WindowInsets { int newTop = Math.max(0, insets.top - top); int newRight = Math.max(0, insets.right - right); int newBottom = Math.max(0, insets.bottom - bottom); - if (newLeft == left && newTop == top && newRight == right && newBottom == bottom) { + if (newLeft == insets.left && newTop == insets.top + && newRight == insets.right && newBottom == insets.bottom) { return insets; } return Insets.of(newLeft, newTop, newRight, newBottom); diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index b44620f12bbb..4ba97384192f 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -105,6 +105,7 @@ public enum DesktopModeFlags { ENABLE_RESIZING_METRICS(Flags::enableResizingMetrics, true), ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE( Flags::enableRestoreToPreviousSizeFromDesktopImmersive, true), + ENABLE_TASKBAR_OVERFLOW(Flags::enableTaskbarOverflow, false), ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS(Flags::enableTaskResizingKeyboardShortcuts, true), ENABLE_TASK_STACK_OBSERVER_IN_SHELL(Flags::enableTaskStackObserverInShell, true), ENABLE_THEMED_APP_HEADERS(Flags::enableThemedAppHeaders, true), diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 3e3b8e100d6d..8265d2523d6f 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -705,6 +705,13 @@ flag { } flag { + name: "enable_activity_embedding_support_for_connected_displays" + namespace: "lse_desktop_experience" + description: "Enables activity embedding support for connected displays, including enabling AE optimization for Settings." + bug: "369438353" +} + +flag { name: "enable_full_screen_window_on_removing_split_screen_stage_bugfix" namespace: "lse_desktop_experience" description: "Enables clearing the windowing mode of a freeform window when removing the task from the split screen stage." @@ -764,3 +771,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_taskbar_overflow" + namespace: "lse_desktop_experience" + description: "Show recent apps in the taskbar overflow." + bug: "368119679" +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 60f2c811dd1f..a4d128fa3caf 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -428,6 +428,17 @@ flag { } flag { + name: "reduce_changed_exclusion_rects_msgs" + namespace: "windowing_frontend" + description: "Don't send MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED when there is no change" + bug: "388231176" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "keep_app_window_hide_while_locked" namespace: "windowing_frontend" description: "Do not let app window visible while device is locked" diff --git a/core/java/com/android/internal/os/LongArrayMultiStateCounter.java b/core/java/com/android/internal/os/LongArrayMultiStateCounter.java index 2931bd2c83dd..fe616e085488 100644 --- a/core/java/com/android/internal/os/LongArrayMultiStateCounter.java +++ b/core/java/com/android/internal/os/LongArrayMultiStateCounter.java @@ -16,6 +16,7 @@ package com.android.internal.os; +import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; import android.ravenwood.annotation.RavenwoodKeepWholeClass; @@ -155,8 +156,9 @@ public final class LongArrayMultiStateCounter implements Parcelable { /** * Adds the supplied values to the current accumulated values in the counter. + * Null `values` parameter is interpreted as an array of zeros. */ - public void incrementValues(long[] values, long timestampMs) { + public void incrementValues(@Nullable long[] values, long timestampMs) { native_incrementValues(mNativeObject, values, timestampMs); } diff --git a/core/java/com/android/internal/os/LongArrayMultiStateCounter_ravenwood.java b/core/java/com/android/internal/os/LongArrayMultiStateCounter_ravenwood.java index 7030d8e84b70..4f5f37d0640e 100644 --- a/core/java/com/android/internal/os/LongArrayMultiStateCounter_ravenwood.java +++ b/core/java/com/android/internal/os/LongArrayMultiStateCounter_ravenwood.java @@ -16,6 +16,7 @@ package com.android.internal.os; +import android.annotation.Nullable; import android.os.BadParcelableException; import android.os.Parcel; import android.ravenwood.annotation.RavenwoodKeepWholeClass; @@ -147,10 +148,12 @@ class LongArrayMultiStateCounter_ravenwood { mLastUpdateTimestampMs = timestampMs; } - public void incrementValues(long[] delta, long timestampMs) { + public void incrementValues(@Nullable long[] delta, long timestampMs) { long[] values = Arrays.copyOf(mValues, mValues.length); - for (int i = 0; i < mArrayLength; i++) { - values[i] += delta[i]; + if (delta != null) { + for (int i = 0; i < mArrayLength; i++) { + values[i] += delta[i]; + } } updateValue(values, timestampMs); } @@ -304,7 +307,8 @@ class LongArrayMultiStateCounter_ravenwood { getInstance(targetInstanceId).copyStatesFrom(getInstance(sourceInstanceId)); } - public static void native_incrementValues(long instanceId, long[] delta, long timestampMs) { + public static void native_incrementValues(long instanceId, @Nullable long[] delta, + long timestampMs) { getInstance(instanceId).incrementValues(delta, timestampMs); } diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java index b57acf3d97fd..b19967a47001 100644 --- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java +++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java @@ -261,7 +261,12 @@ public class AconfigFlags { // Default value is false for when the flag is not found. // Note: Unlike with the old storage, with AconfigPackage, we don't have a way to // know if the flag is not found or if it's found but the value is false. - value = aconfigPackage.getBooleanFlagValue(flagName, false); + try { + value = aconfigPackage.getBooleanFlagValue(flagName, false); + } catch (Exception e) { + Slog.e(LOG_TAG, "Failed to read Aconfig flag value for " + flagPackageAndName, e); + return null; + } } if (DEBUG) { Slog.v(LOG_TAG, "Aconfig flag value for " + flagPackageAndName + " = " + value); diff --git a/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java b/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java index 82d8d3431a9d..6d4a40899a65 100644 --- a/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java +++ b/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java @@ -145,11 +145,11 @@ public class ProtoLogCommandHandler extends ShellCommand { switch (cmd) { case "enable" -> { - mProtoLogConfigurationService.enableProtoLogToLogcat(processGroups()); + mProtoLogConfigurationService.enableProtoLogToLogcat(pw, processGroups()); return 0; } case "disable" -> { - mProtoLogConfigurationService.disableProtoLogToLogcat(processGroups()); + mProtoLogConfigurationService.disableProtoLogToLogcat(pw, processGroups()); return 0; } default -> { diff --git a/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java b/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java index d65aaae7deaa..a19690bbd0e4 100644 --- a/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java +++ b/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java @@ -18,6 +18,8 @@ package com.android.internal.protolog; import android.annotation.NonNull; +import java.io.PrintWriter; + public interface ProtoLogConfigurationService extends IProtoLogConfigurationService { /** * Get the list of groups clients have registered to the protolog service. @@ -37,11 +39,11 @@ public interface ProtoLogConfigurationService extends IProtoLogConfigurationServ * Enable logging target groups to logcat. * @param groups we want to enable logging them to logcat for. */ - void enableProtoLogToLogcat(@NonNull String... groups); + void enableProtoLogToLogcat(@NonNull PrintWriter pw, @NonNull String... groups); /** * Disable logging target groups to logcat. * @param groups we want to disable from being logged to logcat. */ - void disableProtoLogToLogcat(@NonNull String... groups); + void disableProtoLogToLogcat(@NonNull PrintWriter pw, @NonNull String... groups); } diff --git a/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java index f83359dddfcc..ac1022ff1e0f 100644 --- a/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java +++ b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java @@ -44,6 +44,7 @@ import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.PrintWriter; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -183,8 +184,8 @@ public class ProtoLogConfigurationServiceImpl extends IProtoLogConfigurationServ * @param groups we want to enable logging them to logcat for. */ @Override - public void enableProtoLogToLogcat(@NonNull String... groups) { - toggleProtoLogToLogcat(true, groups); + public void enableProtoLogToLogcat(@NonNull PrintWriter pw, @NonNull String... groups) { + toggleProtoLogToLogcat(pw, true, groups); } /** @@ -192,8 +193,8 @@ public class ProtoLogConfigurationServiceImpl extends IProtoLogConfigurationServ * @param groups we want to disable from being logged to logcat. */ @Override - public void disableProtoLogToLogcat(@NonNull String... groups) { - toggleProtoLogToLogcat(false, groups); + public void disableProtoLogToLogcat(@NonNull PrintWriter pw, @NonNull String... groups) { + toggleProtoLogToLogcat(pw, false, groups); } /** @@ -249,7 +250,9 @@ public class ProtoLogConfigurationServiceImpl extends IProtoLogConfigurationServ } } - private void toggleProtoLogToLogcat(boolean enabled, @NonNull String[] groups) { + private void toggleProtoLogToLogcat( + @NonNull PrintWriter pw, boolean enabled, @NonNull String[] groups + ) { final var clientToGroups = new HashMap<IProtoLogClient, Set<String>>(); for (String group : groups) { @@ -257,8 +260,10 @@ public class ProtoLogConfigurationServiceImpl extends IProtoLogConfigurationServ if (clients == null) { // No clients associated to this group - Log.w(LOG_TAG, "Attempting to toggle log to logcat for group " + group - + " with no registered clients."); + var warning = "Attempting to toggle log to logcat for group " + group + + " with no registered clients. This is a no-op."; + Log.w(LOG_TAG, warning); + pw.println("WARNING: " + warning); continue; } @@ -270,8 +275,14 @@ public class ProtoLogConfigurationServiceImpl extends IProtoLogConfigurationServ for (IProtoLogClient client : clientToGroups.keySet()) { try { - client.toggleLogcat(enabled, clientToGroups.get(client).toArray(new String[0])); + final var clientGroups = clientToGroups.get(client).toArray(new String[0]); + pw.println("Toggling logcat logging for client " + client.toString() + + " to " + enabled + " for groups: [" + + String.join(", ", clientGroups) + "]"); + client.toggleLogcat(enabled, clientGroups); + pw.println("- Done"); } catch (RemoteException e) { + pw.println("- Failed"); throw new RuntimeException( "Failed to toggle logcat status for groups on client", e); } diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java index e9d920ca3fcd..64b44df8c982 100644 --- a/core/java/com/android/internal/widget/MessagingLayout.java +++ b/core/java/com/android/internal/widget/MessagingLayout.java @@ -121,22 +121,38 @@ public class MessagingLayout extends FrameLayout setMessagingClippingDisabled(false); } - @RemotableViewMethod + @RemotableViewMethod(asyncImpl = "setAvatarReplacementAsync") public void setAvatarReplacement(Icon icon) { mAvatarReplacement = icon; } - @RemotableViewMethod + /** + * @hide + */ + public Runnable setAvatarReplacementAsync(Icon icon) { + mAvatarReplacement = icon; + return () -> {}; + } + + @RemotableViewMethod(asyncImpl = "setNameReplacementAsync") public void setNameReplacement(CharSequence nameReplacement) { mNameReplacement = nameReplacement; } /** + * @hide + */ + public Runnable setNameReplacementAsync(CharSequence nameReplacement) { + mNameReplacement = nameReplacement; + return () -> {}; + } + + /** * Set this layout to show the collapsed representation. * * @param isCollapsed is it collapsed */ - @RemotableViewMethod + @RemotableViewMethod(asyncImpl = "setIsCollapsedAsync") public void setIsCollapsed(boolean isCollapsed) { mIsCollapsed = isCollapsed; } @@ -145,7 +161,6 @@ public class MessagingLayout extends FrameLayout * setDataAsync needs to do different stuff for the collapsed vs expanded view, so store the * collapsed state early. */ - @RemotableViewMethod(asyncImpl = "setIsCollapsedAsync") public Runnable setIsCollapsedAsync(boolean isCollapsed) { mIsCollapsed = isCollapsed; return () -> {}; @@ -161,12 +176,20 @@ public class MessagingLayout extends FrameLayout * * @param conversationTitle the conversation title */ - @RemotableViewMethod + @RemotableViewMethod(asyncImpl = "setConversationTitleAsync") public void setConversationTitle(CharSequence conversationTitle) { mConversationTitle = conversationTitle; } /** + * @hide + */ + public Runnable setConversationTitleAsync(CharSequence conversationTitle) { + mConversationTitle = conversationTitle; + return ()->{}; + } + + /** * Set Messaging data * @param extras Bundle contains messaging data */ @@ -417,22 +440,44 @@ public class MessagingLayout extends FrameLayout return mPeopleHelper.createAvatarSymbol(senderName, symbol, layoutColor); } - @RemotableViewMethod + @RemotableViewMethod(asyncImpl = "setLayoutColorAsync") public void setLayoutColor(int color) { mLayoutColor = color; } - @RemotableViewMethod + /** + * @hide + */ + public Runnable setLayoutColorAsync(int color) { + mLayoutColor = color; + return () -> {}; + } + + @RemotableViewMethod(asyncImpl = "setIsOneToOneAsync") public void setIsOneToOne(boolean oneToOne) { mIsOneToOne = oneToOne; } - @RemotableViewMethod + /** + * @hide + */ + public Runnable setIsOneToOneAsync(boolean oneToOne) { + mIsOneToOne = oneToOne; + return () -> {}; + } + + @RemotableViewMethod(asyncImpl = "setSenderTextColorAsync") public void setSenderTextColor(int color) { mSenderTextColor = color; } - + /** + * @hide + */ + public Runnable setSenderTextColorAsync(int color) { + mSenderTextColor = color; + return () -> {}; + } /** * @param color the color of the notification background */ @@ -441,11 +486,19 @@ public class MessagingLayout extends FrameLayout // Nothing to do with this } - @RemotableViewMethod + @RemotableViewMethod(asyncImpl = "setMessageTextColorAsync") public void setMessageTextColor(int color) { mMessageTextColor = color; } + /** + * @hide + */ + public Runnable setMessageTextColorAsync(int color) { + mMessageTextColor = color; + return () -> {}; + } + public void setUser(Person user) { mUser = user; if (mUser.getIcon() == null) { diff --git a/core/jni/android_app_PropertyInvalidatedCache.cpp b/core/jni/android_app_PropertyInvalidatedCache.cpp index 12585d5f8137..6267522f114f 100644 --- a/core/jni/android_app_PropertyInvalidatedCache.cpp +++ b/core/jni/android_app_PropertyInvalidatedCache.cpp @@ -35,7 +35,7 @@ int NonceStore::getMaxNonce() const { return kMaxNonce; } -size_t NonceStore::getMaxByte() const { +int32_t NonceStore::getMaxByte() const { return kMaxByte; } @@ -68,13 +68,13 @@ int32_t NonceStore::getHash() const { } // Copy the byte block to the target and return the current hash. -int32_t NonceStore::getByteBlock(block_t* block, size_t len) const { +int32_t NonceStore::getByteBlock(block_t* block, int32_t len) const { memcpy(block, (void*) byteBlock(), std::min(kMaxByte, len)); return mByteHash; } // Set the byte block and the hash. -void NonceStore::setByteBlock(int hash, const block_t* block, size_t len) { +void NonceStore::setByteBlock(int hash, const block_t* block, int32_t len) { memcpy((void*) byteBlock(), block, len = std::min(kMaxByte, len)); mByteHash = hash; } diff --git a/core/jni/android_app_PropertyInvalidatedCache.h b/core/jni/android_app_PropertyInvalidatedCache.h index 54a4ac65fce2..1d75182356c6 100644 --- a/core/jni/android_app_PropertyInvalidatedCache.h +++ b/core/jni/android_app_PropertyInvalidatedCache.h @@ -27,8 +27,12 @@ namespace android::app::PropertyInvalidatedCache { * location. Fields with a variable location are found via offsets. The offsets make this * object position-independent, which is required because it is in shared memory and would be * mapped into different virtual addresses for different processes. + * + * This structure is shared between 64-bit and 32-bit processes. Therefore it is imperative + * that the structure not use any datatypes that are architecture-dependent (like size_t). + * Additionally, care must be taken to avoid unexpected padding in the structure. */ -class NonceStore { +class alignas(8) NonceStore { protected: // A convenient typedef. The jbyteArray element type is jbyte, which the compiler treats as // signed char. @@ -43,23 +47,27 @@ class NonceStore { // The value of an unset field. static constexpr int UNSET = 0; - // The size of the nonce array. + // The size of the nonce array. This and the following sizes are int32_t to + // be ABI independent. const int32_t kMaxNonce; // The size of the byte array. - const size_t kMaxByte; + const int32_t kMaxByte; // The offset to the nonce array. - const size_t mNonceOffset; + const int32_t mNonceOffset; // The offset to the byte array. - const size_t mByteOffset; + const int32_t mByteOffset; // The byte block hash. This is fixed and at a known offset, so leave it in the base class. volatile std::atomic<int32_t> mByteHash; + // A 4-byte padd that makes the size of this structure a multiple of 8 bytes. + const int32_t _pad = 0; + // The constructor is protected! It only makes sense when called from a subclass. - NonceStore(int kMaxNonce, size_t kMaxByte, volatile nonce_t* nonce, volatile block_t* block) : + NonceStore(int kMaxNonce, int kMaxByte, volatile nonce_t* nonce, volatile block_t* block) : kMaxNonce(kMaxNonce), kMaxByte(kMaxByte), mNonceOffset(offset(this, const_cast<nonce_t*>(nonce))), @@ -70,7 +78,7 @@ class NonceStore { // These provide run-time access to the sizing parameters. int getMaxNonce() const; - size_t getMaxByte() const; + int getMaxByte() const; // Fetch a nonce, returning UNSET if the index is out of range. This method specifically // does not throw or generate an error if the index is out of range; this allows the method @@ -86,10 +94,10 @@ class NonceStore { int32_t getHash() const; // Copy the byte block to the target and return the current hash. - int32_t getByteBlock(block_t* block, size_t len) const; + int32_t getByteBlock(block_t* block, int32_t len) const; // Set the byte block and the hash. - void setByteBlock(int hash, const block_t* block, size_t len); + void setByteBlock(int hash, const block_t* block, int32_t len); private: @@ -113,6 +121,12 @@ class NonceStore { } }; +// Assert that the size of the object is fixed, independent of the CPU architecture. There are +// four int32_t fields and one atomic<int32_t>, which sums to 20 bytes total. This assertion +// uses a constant instead of computing the size of the objects in the compiler, to avoid +// different answers on different architectures. +static_assert(sizeof(NonceStore) == 24); + /** * A cache nonce block contains an array of std::atomic<int64_t> and an array of bytes. The * byte array has an associated hash. This class provides methods to read and write the fields @@ -126,20 +140,22 @@ class NonceStore { * The template is parameterized by the number of nonces it supports and the number of bytes in * the string block. */ -template<int maxNonce, size_t maxByte> class CacheNonce : public NonceStore { +template<int MAX_NONCE, int MAX_BYTE> class CacheNonce : public NonceStore { // The array of nonces - volatile nonce_t mNonce[maxNonce]; + volatile nonce_t mNonce[MAX_NONCE]; // The byte array. This is not atomic but it is guarded by the mByteHash. - volatile block_t mByteBlock[maxByte]; + volatile block_t mByteBlock[MAX_BYTE]; public: + // Export the parameters for use in compiler assertions. + static constexpr int kMaxNonceCount = MAX_NONCE; + static constexpr int kMaxByteCount = MAX_BYTE; + // Construct and initialize the memory. - CacheNonce() : - NonceStore(maxNonce, maxByte, &mNonce[0], &mByteBlock[0]) - { - for (int i = 0; i < maxNonce; i++) { + CacheNonce() : NonceStore(MAX_NONCE, MAX_BYTE, &mNonce[0], &mByteBlock[0]) { + for (int i = 0; i < MAX_NONCE; i++) { mNonce[i] = UNSET; } mByteHash = UNSET; @@ -155,4 +171,10 @@ template<int maxNonce, size_t maxByte> class CacheNonce : public NonceStore { typedef CacheNonce</* max nonce */ 128, /* byte block size */ 8192> SystemCacheNonce; // LINT.ThenChange(/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java:system_nonce_config) +// Verify that there is no padding in the final class. +static_assert(sizeof(SystemCacheNonce) == + sizeof(NonceStore) + + SystemCacheNonce::kMaxNonceCount*8 + + SystemCacheNonce::kMaxByteCount); + } // namespace android.app.PropertyInvalidatedCache diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index b99b0ef7f24e..a8e51a7c1fe8 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -765,28 +765,28 @@ static void nativeSetLuts(JNIEnv* env, jclass clazz, jlong transactionObj, jlong std::vector<int32_t> dimensions; std::vector<int32_t> sizes; std::vector<int32_t> samplingKeys; - int32_t fd = -1; + base::unique_fd fd; if (jdimensionArray) { jsize numLuts = env->GetArrayLength(jdimensionArray); - ScopedIntArrayRW joffsets(env, joffsetArray); + ScopedIntArrayRO joffsets(env, joffsetArray); if (joffsets.get() == nullptr) { - jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from joffsetArray"); + jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRO from joffsetArray"); return; } - ScopedIntArrayRW jdimensions(env, jdimensionArray); + ScopedIntArrayRO jdimensions(env, jdimensionArray); if (jdimensions.get() == nullptr) { - jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jdimensionArray"); + jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRO from jdimensionArray"); return; } - ScopedIntArrayRW jsizes(env, jsizeArray); + ScopedIntArrayRO jsizes(env, jsizeArray); if (jsizes.get() == nullptr) { - jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jsizeArray"); + jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRO from jsizeArray"); return; } - ScopedIntArrayRW jsamplingKeys(env, jsamplingKeyArray); + ScopedIntArrayRO jsamplingKeys(env, jsamplingKeyArray); if (jsamplingKeys.get() == nullptr) { - jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jsamplingKeyArray"); + jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRO from jsamplingKeyArray"); return; } @@ -796,15 +796,15 @@ static void nativeSetLuts(JNIEnv* env, jclass clazz, jlong transactionObj, jlong sizes = std::vector<int32_t>(jsizes.get(), jsizes.get() + numLuts); samplingKeys = std::vector<int32_t>(jsamplingKeys.get(), jsamplingKeys.get() + numLuts); - ScopedFloatArrayRW jbuffers(env, jbufferArray); + ScopedFloatArrayRO jbuffers(env, jbufferArray); if (jbuffers.get() == nullptr) { - jniThrowRuntimeException(env, "Failed to get ScopedFloatArrayRW from jbufferArray"); + jniThrowRuntimeException(env, "Failed to get ScopedFloatArrayRO from jbufferArray"); return; } // create the shared memory and copy jbuffers size_t bufferSize = jbuffers.size() * sizeof(float); - fd = ashmem_create_region("lut_shared_mem", bufferSize); + fd.reset(ashmem_create_region("lut_shared_mem", bufferSize)); if (fd < 0) { jniThrowRuntimeException(env, "ashmem_create_region() failed"); return; @@ -820,7 +820,7 @@ static void nativeSetLuts(JNIEnv* env, jclass clazz, jlong transactionObj, jlong } } - transaction->setLuts(ctrl, base::unique_fd(fd), offsets, dimensions, sizes, samplingKeys); + transaction->setLuts(ctrl, std::move(fd), offsets, dimensions, sizes, samplingKeys); } static void nativeSetPictureProfileId(JNIEnv* env, jclass clazz, jlong transactionObj, diff --git a/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp b/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp index 7ffe0ed7c6cd..8f36ecb8b01a 100644 --- a/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp +++ b/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp @@ -97,7 +97,13 @@ static void native_updateValues(JNIEnv *env, jclass, jlong nativePtr, jlongArray static void native_incrementValues(JNIEnv *env, jclass, jlong nativePtr, jlongArray values, jlong timestamp) { auto *counter = reinterpret_cast<LongArrayMultiStateCounter *>(nativePtr); - counter->incrementValue(JavaUint64Array(env, values), timestamp); + if (values != nullptr) { + counter->incrementValue(JavaUint64Array(env, values), timestamp); + } else { + // Pass an empty Uint64Array, which is equivalent to an array of zeros. + // This is done to ensure that the timestamp is still updated in the counter. + counter->incrementValue(Uint64Array(), timestamp); + } } static void native_addCounts(JNIEnv *env, jclass, jlong nativePtr, jlongArray values) { diff --git a/core/res/res/drawable/progress_ring_watch.xml b/core/res/res/drawable/progress_ring_watch.xml index 8250ee600a8f..2e65ff13b3de 100644 --- a/core/res/res/drawable/progress_ring_watch.xml +++ b/core/res/res/drawable/progress_ring_watch.xml @@ -16,27 +16,27 @@ --> <rotate xmlns:android="http://schemas.android.com/apk/res/android" - android:fromDegrees = "270" - android:toDegrees="270" - android:pivotX="50%" - android:pivotY="50%" > + android:fromDegrees="270" + android:toDegrees="270"> <layer-list> <item> <shape - android:shape="ring" - android:innerRadiusRatio="@dimen/progressbar_inner_radius_ratio" + android:shape="arc" + android:useLevel="false" android:thickness="@dimen/progressbar_thickness" - android:useLevel="false"> - <solid android:color="@color/materialColorSurfaceContainer"/> + android:innerRadiusRatio="@dimen/progressbar_inner_radius_ratio" + android:strokeCap="round"> + <solid android:color="@*android:color/materialColorSurfaceContainer"/> </shape> </item> <item> <shape - android:shape="ring" - android:innerRadiusRatio="@dimen/progressbar_inner_radius_ratio" + android:shape="arc" + android:useLevel="true" android:thickness="@dimen/progressbar_thickness" - android:useLevel="true"> - <solid android:color="@color/materialColorPrimary"/> + android:innerRadiusRatio="@dimen/progressbar_inner_radius_ratio" + android:strokeCap="round"> + <solid android:color="@*android:color/materialColorPrimary"/> </shape> </item> </layer-list> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 66111785af4f..d2c993aecb0d 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -6934,6 +6934,8 @@ <enum name="line" value="2" /> <!-- Ring shape. --> <enum name="ring" value="3" /> + <!-- ARC shape. --> + <enum name="arc" value="4"/> </attr> <!-- Inner radius of the ring expressed as a ratio of the ring's width. For instance, if innerRadiusRatio=9, then the inner radius equals the ring's width divided by 9. @@ -6966,6 +6968,12 @@ <attr name="opticalInsetRight" /> <!-- Bottom optical inset. --> <attr name="opticalInsetBottom" /> + <!-- Attributes that customize the stroke line cap. @hide --> + <attr name="strokeCap" format="enum"> + <enum name="butt" value="0"/> + <enum name="round" value="1"/> + <enum name="square" value="2"/> + </attr> </declare-styleable> <!-- Used to specify the size of the shape for GradientDrawable. --> diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index f2ec56c69374..59ed25a1865f 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -64,6 +64,13 @@ <integer name="auto_data_switch_performance_stability_time_threshold_millis">120000</integer> <java-symbol type="integer" name="auto_data_switch_performance_stability_time_threshold_millis" /> + <!-- Define the bar for switching data back to the default SIM when both SIMs are out of service + in milliseconds. A value of 0 means an immediate switch, otherwise for a negative value, + the threshold defined by auto_data_switch_availability_stability_time_threshold_millis + will be used instead. --> + <integer name="auto_data_switch_availability_switchback_stability_time_threshold_millis">150000</integer> + <java-symbol type="integer" name="auto_data_switch_availability_switchback_stability_time_threshold_millis" /> + <!-- Define the maximum retry times when a validation for switching failed.--> <integer name="auto_data_switch_validation_max_retry">7</integer> <java-symbol type="integer" name="auto_data_switch_validation_max_retry" /> diff --git a/core/res/res/values/dimens_watch.xml b/core/res/res/values/dimens_watch.xml index 2aae98715973..7462b733b0ae 100644 --- a/core/res/res/values/dimens_watch.xml +++ b/core/res/res/values/dimens_watch.xml @@ -52,7 +52,7 @@ <dimen name="primary_content_alpha_device_default">0.38</dimen> <!-- values for wear material3 progress bar(progress indicator) --> - <item name="progressbar_inner_radius_ratio" format="float" type="dimen">2.12</item> + <item name="progressbar_inner_radius_ratio" format="float" type="dimen">2</item> <dimen name="progressbar_thickness">8dp</dimen> <dimen name="progressbar_elevation">0.1dp</dimen> diff --git a/core/tests/coretests/src/android/view/ViewRootRectTrackerTest.java b/core/tests/coretests/src/android/view/ViewRootRectTrackerTest.java new file mode 100644 index 000000000000..c66e10079bc8 --- /dev/null +++ b/core/tests/coretests/src/android/view/ViewRootRectTrackerTest.java @@ -0,0 +1,70 @@ +/* + * 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 android.view; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.testng.AssertJUnit.assertEquals; + +import android.graphics.Rect; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.google.common.collect.Lists; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Collections; +import java.util.List; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ViewRootRectTrackerTest { + private ViewRootRectTracker mTracker; + private final List<Rect> mRects = Lists.newArrayList(new Rect(0, 0, 5, 5), + new Rect(5, 5, 10, 10)); + + @Before + public void setUp() { + mTracker = new ViewRootRectTracker(v -> Collections.emptyList()); + } + + @Test + public void setRootRectsAndComputeTest() { + mTracker.setRootRects(mRects); + mTracker.computeChanges(); + assertEquals(mRects, mTracker.getLastComputedRects()); + } + + @Test + public void waitingForComputeChangesTest() { + mTracker.setRootRects(mRects); + assertTrue(mTracker.isWaitingForComputeChanges()); + mTracker.computeChangedRects(); + assertFalse(mTracker.isWaitingForComputeChanges()); + + View mockView = mock(View.class); + mTracker.updateRectsForView(mockView); + assertTrue(mTracker.isWaitingForComputeChanges()); + mTracker.computeChangedRects(); + assertFalse(mTracker.isWaitingForComputeChanges()); + } +} diff --git a/graphics/java/android/framework_graphics.aconfig b/graphics/java/android/framework_graphics.aconfig index a63cbee4d707..fdbee3ccbebe 100644 --- a/graphics/java/android/framework_graphics.aconfig +++ b/graphics/java/android/framework_graphics.aconfig @@ -42,3 +42,11 @@ flag { description: "Add DISPLAY_BT2020 ColorSpace support" bug: "344038816" } + +flag { + name: "gradient_drawable_shape_rounded_cap" + is_fixed_read_only: true + namespace: "core_graphics" + description: "Make GradientDrawable support drawing ring with rounded stroke cap." + bug: "380000245" +} diff --git a/graphics/java/android/graphics/drawable/GradientDrawable.java b/graphics/java/android/graphics/drawable/GradientDrawable.java index 29d033e64aea..ff1dc93d787b 100644 --- a/graphics/java/android/graphics/drawable/GradientDrawable.java +++ b/graphics/java/android/graphics/drawable/GradientDrawable.java @@ -16,7 +16,11 @@ package android.graphics.drawable; +import static com.android.graphics.flags.Flags.FLAG_GRADIENT_DRAWABLE_SHAPE_ROUNDED_CAP; +import static com.android.graphics.flags.Flags.gradientDrawableShapeRoundedCap; + import android.annotation.ColorInt; +import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.NonNull; @@ -125,8 +129,14 @@ public class GradientDrawable extends Drawable { */ public static final int RING = 3; + /** + * Shape is an arc. + */ + @FlaggedApi(FLAG_GRADIENT_DRAWABLE_SHAPE_ROUNDED_CAP) + public static final int ARC = 4; + /** @hide */ - @IntDef({RECTANGLE, OVAL, LINE, RING}) + @IntDef({RECTANGLE, OVAL, LINE, RING, ARC}) @Retention(RetentionPolicy.SOURCE) public @interface Shape {} @@ -167,6 +177,17 @@ public class GradientDrawable extends Drawable { @Retention(RetentionPolicy.SOURCE) public @interface RadiusType {} + private static final int BUTT = 0; + + private static final int ROUND = 1; + + private static final int SQUARE = 2; + + /** @hide */ + @IntDef({BUTT, ROUND, SQUARE}) + @Retention(RetentionPolicy.SOURCE) + public @interface StrokeCap {} + private static final float DEFAULT_INNER_RADIUS_RATIO = 3.0f; private static final float DEFAULT_THICKNESS_RATIO = 9.0f; @@ -191,6 +212,8 @@ public class GradientDrawable extends Drawable { private boolean mMutated; private Path mRingPath; private boolean mPathIsDirty = true; + private Path mArcPath; + private Path mArcOutlinePath; /** Current gradient radius, valid when {@link #mGradientIsDirty} is false. */ private float mGradientRadius; @@ -850,6 +873,55 @@ public class GradientDrawable extends Drawable { } break; } + case ARC: + if (gradientDrawableShapeRoundedCap()) { + // TODO(b/394988176): Consider applying ARC drawing logic to RING shape. + float centerX = mRect.centerX(); + float centerY = mRect.centerY(); + float thickness = + st.mThickness != -1 ? st.mThickness + : mRect.width() / st.mThicknessRatio; + float radius = st.mInnerRadius != -1 ? st.mInnerRadius + : mRect.width() / st.mInnerRadiusRatio; + radius -= thickness; + float sweep = st.mUseLevelForShape ? (360.0f * getLevel() / 10000.0f) : 360f; + mRect.set(centerX - radius, centerY - radius, centerX + radius, + centerY + radius); + + // Prepare paint. Set style to STROKE for purpose of drawing line ARC. + mFillPaint.setStyle(Paint.Style.STROKE); + mFillPaint.setStrokeWidth(thickness); + mFillPaint.setStrokeCap(getStrokeLineCapForPaint(st.mStrokeCap)); + canvas.drawArc(mRect, 0.0f, sweep, /* useCenter= */ false, mFillPaint); + + if (haveStroke) { + if (mArcPath == null) { + mArcPath = new Path(); + } else { + mArcPath.reset(); + } + if (mArcOutlinePath == null) { + mArcOutlinePath = new Path(); + } else { + mArcOutlinePath.reset(); + } + if (sweep == 360f) { + mArcPath.addOval(mRect, Path.Direction.CW); + } else { + mArcPath.arcTo(mRect, 0.0f, sweep, /* forceMoveTo= */ false); + } + + // The arc path doesn't have width. So, to get the outline of the result arc + // shape, we need to apply the paint effect to the path; then use the + // output as the result outline. + mFillPaint.getFillPath(mArcPath, mArcOutlinePath); + canvas.drawPath(mArcOutlinePath, mStrokePaint); + } + + // Restore to FILL + mFillPaint.setStyle(Paint.Style.FILL); + break; + } case RING: Path path = buildRing(st); canvas.drawPath(path, mFillPaint); @@ -993,6 +1065,36 @@ public class GradientDrawable extends Drawable { } /** + * Return current drawable's stroke line cap. Note that this is only respected when drawable is + * {@link Shape#ARC}. + * + * @return the {@link StrokeCap} of current drawable. + * @attr ref android.R.styleable#GradientDrawable_strokeCap + * @see #setStrokeCap(int) + * + * @hide + */ + @StrokeCap + public int getStrokeCap() { + return mGradientState.mStrokeCap; + } + + /** + * Configure the stroke line cap type that drawable will use while drawing. Note that this is + * only respected when drawable is {@link Shape#ARC}. + * + * @param strokeCapType the stroke line cap type that the drawable will use while drawing. + * @attr ref android.R.styleable#GradientDrawable_strokeCap + * @see #getStrokeCap + * + * @hide + */ + public void setStrokeCap(@StrokeCap int strokeCapType) { + mGradientState.mStrokeCap = strokeCapType; + invalidateSelf(); + } + + /** * Configure the padding of the gradient shape * @param left Left padding of the gradient shape * @param top Top padding of the gradient shape @@ -1066,6 +1168,15 @@ public class GradientDrawable extends Drawable { return ringPath; } + private Paint.Cap getStrokeLineCapForPaint(@StrokeCap int strokeLineCap) { + return switch (strokeLineCap) { + case BUTT -> Paint.Cap.BUTT; + case ROUND -> Paint.Cap.ROUND; + case SQUARE -> Paint.Cap.SQUARE; + default -> Paint.Cap.SQUARE; + }; + } + /** * Changes this drawable to use a single color instead of a gradient. * <p> @@ -1484,7 +1595,7 @@ public class GradientDrawable extends Drawable { state.mShape = a.getInt(R.styleable.GradientDrawable_shape, state.mShape); state.mDither = a.getBoolean(R.styleable.GradientDrawable_dither, state.mDither); - if (state.mShape == RING) { + if (state.mShape == RING || state.mShape == ARC) { state.mInnerRadius = a.getDimensionPixelSize( R.styleable.GradientDrawable_innerRadius, state.mInnerRadius); @@ -1503,6 +1614,9 @@ public class GradientDrawable extends Drawable { state.mUseLevelForShape = a.getBoolean( R.styleable.GradientDrawable_useLevel, state.mUseLevelForShape); + + state.mStrokeCap = a.getInt( + R.styleable.GradientDrawable_strokeCap, state.mStrokeCap); } final int tintMode = a.getInt(R.styleable.GradientDrawable_tintMode, -1); @@ -2045,6 +2159,9 @@ public class GradientDrawable extends Drawable { public int mInnerRadius = -1; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050218) public int mThickness = -1; + @UnsupportedAppUsage(trackingBug = 380000245) + @StrokeCap public int mStrokeCap = ROUND; + public boolean mDither = false; public Insets mOpticalInsets = Insets.NONE; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java index 5bd8d86f1144..0f1bf5e09751 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -15,6 +15,8 @@ */ package com.android.wm.shell.bubbles; +import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR; + import android.annotation.DrawableRes; import android.annotation.Nullable; import android.content.Context; @@ -35,7 +37,6 @@ import android.widget.ImageView; import androidx.constraintlayout.widget.ConstraintLayout; import com.android.launcher3.icons.DotRenderer; -import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; import com.android.wm.shell.shared.animation.Interpolators; @@ -132,7 +133,7 @@ public class BadgedImageView extends ConstraintLayout { private void getOutline(Outline outline) { final int bubbleSize = mPositioner.getBubbleSize(); - final int normalizedSize = IconNormalizer.getNormalizedCircleSize(bubbleSize); + final int normalizedSize = Math.round(ICON_VISIBLE_AREA_FACTOR * bubbleSize); final int inset = (bubbleSize - normalizedSize) / 2; outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index f6a2c8d9695e..305fcdd5fb7d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -912,7 +912,7 @@ public class BubbleController implements ConfigurationChangeListener, // TODO(b/393172431) : Utilise DragZoneFactory once it is ready final int bubbleBarDropZoneSideSize = getContext().getResources().getDimensionPixelSize( R.dimen.bubble_bar_drop_zone_side_size); - int top = t - bubbleBarDropZoneSideSize; + int top = b - bubbleBarDropZoneSideSize; result.put(BubbleBarLocation.LEFT, new Rect(l, top, l + bubbleBarDropZoneSideSize, b)); result.put(BubbleBarLocation.RIGHT, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index 221c9332711e..33f1b94bac73 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -16,6 +16,7 @@ package com.android.wm.shell.bubbles; +import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; import android.content.Context; @@ -31,7 +32,6 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.ProtoLog; -import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; @@ -557,8 +557,7 @@ public class BubblePositioner implements BubbleDropTargetBoundsProvider { public float getPointerPosition(float bubblePosition) { // TODO: I don't understand why it works but it does - why normalized in portrait // & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation? - final float normalizedSize = IconNormalizer.getNormalizedCircleSize( - getBubbleSize()); + final float normalizedSize = Math.round(ICON_VISIBLE_AREA_FACTOR * getBubbleSize()); return showBubblesVertically() ? bubblePosition + (getBubbleSize() / 2f) : bubblePosition + (normalizedSize / 2f) - mPointerWidth; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index e3b0872df593..29837dc04423 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -135,6 +135,7 @@ public class BubbleBarLayerView extends FrameLayout /** Shows the expanded view drop target at the requested {@link BubbleBarLocation location} */ public void showBubbleBarExtendedViewDropTarget(@NonNull BubbleBarLocation bubbleBarLocation) { + setVisibility(VISIBLE); mBubbleExpandedViewPinController.showDropTarget(bubbleBarLocation); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorController.kt new file mode 100644 index 000000000000..7a5bc1383ccf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorController.kt @@ -0,0 +1,124 @@ +/* + * 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.RunningTaskInfo +import android.graphics.RectF +import android.view.SurfaceControl +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.shared.annotations.ShellDesktopThread + +/** + * Controller to manage the indicators that show users the current position of the dragged window on + * the new display when performing drag move across displays. + */ +class MultiDisplayDragMoveIndicatorController( + private val displayController: DisplayController, + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + private val indicatorSurfaceFactory: MultiDisplayDragMoveIndicatorSurface.Factory, + @ShellDesktopThread private val desktopExecutor: ShellExecutor, +) { + @ShellDesktopThread + private val dragIndicators = + mutableMapOf<Int, MutableMap<Int, MultiDisplayDragMoveIndicatorSurface>>() + + /** + * Called during drag move, which started at [startDisplayId]. Updates the position and + * visibility of the drag move indicators for the [taskInfo] based on [boundsDp] on the + * destination displays ([displayIds]) as the dragged window moves. [transactionSupplier] + * provides a [SurfaceControl.Transaction] for applying changes to the indicator surfaces. + * + * It is executed on the [desktopExecutor] to prevent blocking the main thread and avoid jank, + * as creating and manipulating surfaces can be expensive. + */ + fun onDragMove( + boundsDp: RectF, + startDisplayId: Int, + taskInfo: RunningTaskInfo, + displayIds: Set<Int>, + transactionSupplier: () -> SurfaceControl.Transaction, + ) { + desktopExecutor.execute { + for (displayId in displayIds) { + if (displayId == startDisplayId) { + // No need to render indicators on the original display where the drag started. + continue + } + val displayLayout = displayController.getDisplayLayout(displayId) ?: continue + val shouldBeVisible = + RectF.intersects(RectF(boundsDp), displayLayout.globalBoundsDp()) + if ( + dragIndicators[taskInfo.taskId]?.containsKey(displayId) != true && + !shouldBeVisible + ) { + // Skip this display if: + // - It doesn't have an existing indicator that needs to be updated, AND + // - The latest dragged window bounds don't intersect with this display. + continue + } + + val boundsPx = + MultiDisplayDragMoveBoundsCalculator.convertGlobalDpToLocalPxForRect( + boundsDp, + displayLayout, + ) + + // Get or create the inner map for the current task. + val dragIndicatorsForTask = + dragIndicators.getOrPut(taskInfo.taskId) { mutableMapOf() } + dragIndicatorsForTask[displayId]?.also { existingIndicator -> + val transaction = transactionSupplier() + existingIndicator.relayout(boundsPx, transaction, shouldBeVisible) + transaction.apply() + } ?: run { + val newIndicator = + indicatorSurfaceFactory.create( + taskInfo, + displayController.getDisplay(displayId), + ) + newIndicator.show( + transactionSupplier(), + taskInfo, + rootTaskDisplayAreaOrganizer, + displayId, + boundsPx, + ) + dragIndicatorsForTask[displayId] = newIndicator + } + } + } + } + + /** + * Called when the drag ends. Disposes of the drag move indicator surfaces associated with the + * given [taskId]. [transactionSupplier] provides a [SurfaceControl.Transaction] for applying + * changes to the indicator surfaces. + * + * It is executed on the [desktopExecutor] to ensure that any pending `onDragMove` operations + * have completed before disposing of the surfaces. + */ + fun onDragEnd(taskId: Int, transactionSupplier: () -> SurfaceControl.Transaction) { + desktopExecutor.execute { + dragIndicators.remove(taskId)?.values?.takeIf { it.isNotEmpty() }?.let { indicators -> + val transaction = transactionSupplier() + indicators.forEach { indicator -> + indicator.disposeSurface(transaction) + } + transaction.apply() + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorSurface.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorSurface.kt new file mode 100644 index 000000000000..d05d3b0903d7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorSurface.kt @@ -0,0 +1,151 @@ +/* + * 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.RunningTaskInfo +import android.content.Context +import android.graphics.Color +import android.graphics.Rect +import android.os.Trace +import android.view.Display +import android.view.SurfaceControl +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.ui.graphics.toArgb +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.common.Theme + +/** + * Represents the indicator surface that visualizes the current position of a dragged window during + * a multi-display drag operation. + * + * This class manages the creation, display, and manipulation of the [SurfaceControl]s that act as a + * visual indicator, providing feedback to the user about the dragged window's location. + */ +class MultiDisplayDragMoveIndicatorSurface( + context: Context, + taskInfo: RunningTaskInfo, + display: Display, + surfaceControlBuilderFactory: Factory.SurfaceControlBuilderFactory, +) { + private var isVisible = false + + // A container surface to host the veil background + private var veilSurface: SurfaceControl? = null + + private val decorThemeUtil = DecorThemeUtil(context) + private val lightColors = dynamicLightColorScheme(context) + private val darkColors = dynamicDarkColorScheme(context) + + init { + Trace.beginSection("DragIndicatorSurface#init") + + val displayId = display.displayId + veilSurface = + surfaceControlBuilderFactory + .create("Drag indicator veil of Task=${taskInfo.taskId} Display=$displayId") + .setColorLayer() + .setCallsite("DragIndicatorSurface#init") + .setHidden(true) + .build() + + // TODO: b/383069173 - Add icon for the surface. + + Trace.endSection() + } + + /** + * Disposes the indicator surface using the provided [transaction]. + */ + fun disposeSurface(transaction: SurfaceControl.Transaction) { + veilSurface?.let { veil -> transaction.remove(veil) } + veilSurface = null + } + + /** + * Shows the indicator surface at [bounds] on the specified display ([displayId]), + * visualizing the drag of the [taskInfo]. The indicator surface is shown using [transaction], + * and the [rootTaskDisplayAreaOrganizer] is used to reparent the surfaces. + */ + fun show( + transaction: SurfaceControl.Transaction, + taskInfo: RunningTaskInfo, + rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + displayId: Int, + bounds: Rect, + ) { + val backgroundColor = + when (decorThemeUtil.getAppTheme(taskInfo)) { + Theme.LIGHT -> lightColors.surfaceContainer + Theme.DARK -> darkColors.surfaceContainer + } + val veil = veilSurface ?: return + isVisible = true + + rootTaskDisplayAreaOrganizer.reparentToDisplayArea(displayId, veil, transaction) + relayout(bounds, transaction, shouldBeVisible = true) + transaction.show(veil).setColor(veil, Color.valueOf(backgroundColor.toArgb()).components) + transaction.apply() + } + + /** + * Repositions and resizes the indicator surface based on [bounds] using [transaction]. The + * [shouldBeVisible] flag indicates whether the indicator is within the display after relayout. + */ + fun relayout(bounds: Rect, transaction: SurfaceControl.Transaction, shouldBeVisible: Boolean) { + if (!isVisible && !shouldBeVisible) { + // No need to relayout if the surface is already invisible and should not be visible. + return + } + isVisible = shouldBeVisible + val veil = veilSurface ?: return + transaction.setCrop(veil, bounds) + } + + /** + * Factory for creating [MultiDisplayDragMoveIndicatorSurface] instances with the [context]. + */ + class Factory(private val context: Context) { + private val surfaceControlBuilderFactory: SurfaceControlBuilderFactory = + object : SurfaceControlBuilderFactory {} + + /** + * Creates a new [MultiDisplayDragMoveIndicatorSurface] instance to visualize the drag + * operation of the [taskInfo] on the given [display]. + */ + fun create( + taskInfo: RunningTaskInfo, + display: Display, + ) = MultiDisplayDragMoveIndicatorSurface( + context, + taskInfo, + display, + surfaceControlBuilderFactory, + ) + + /** + * Interface for creating [SurfaceControl.Builder] instances. + * + * This provides an abstraction over [SurfaceControl.Builder] creation for testing purposes. + */ + interface SurfaceControlBuilderFactory { + fun create(name: String): SurfaceControl.Builder { + return SurfaceControl.Builder().setName(name) + } + } + } +} 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 2fd8c27d5970..5a246e7c99b9 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 @@ -68,6 +68,8 @@ 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.LaunchAdjacentController; +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController; +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorSurface; import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -82,6 +84,7 @@ import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; import com.android.wm.shell.desktopmode.DesktopDisplayEventHandler; +import com.android.wm.shell.desktopmode.DesktopDisplayModeController; import com.android.wm.shell.desktopmode.DesktopImmersiveController; import com.android.wm.shell.desktopmode.DesktopMinimizationTransitionHandler; import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler; @@ -989,7 +992,8 @@ public abstract class WMShellModule { WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, DesktopModeCompatPolicy desktopModeCompatPolicy, - DesktopTilingDecorViewModel desktopTilingDecorViewModel + DesktopTilingDecorViewModel desktopTilingDecorViewModel, + MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController ) { if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) { return Optional.empty(); @@ -1006,7 +1010,30 @@ public abstract class WMShellModule { windowDecorCaptionHandleRepository, activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy, - desktopTilingDecorViewModel)); + desktopTilingDecorViewModel, + multiDisplayDragMoveIndicatorController)); + } + + @WMSingleton + @Provides + static MultiDisplayDragMoveIndicatorController + providesMultiDisplayDragMoveIndicatorController( + DisplayController displayController, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + MultiDisplayDragMoveIndicatorSurface.Factory + multiDisplayDragMoveIndicatorSurfaceFactory, + @ShellDesktopThread ShellExecutor desktopExecutor + ) { + return new MultiDisplayDragMoveIndicatorController( + displayController, rootTaskDisplayAreaOrganizer, + multiDisplayDragMoveIndicatorSurfaceFactory, desktopExecutor); + } + + @WMSingleton + @Provides + static MultiDisplayDragMoveIndicatorSurface.Factory + providesMultiDisplayDragMoveIndicatorSurfaceFactory(Context context) { + return new MultiDisplayDragMoveIndicatorSurface.Factory(context); } @WMSingleton @@ -1230,13 +1257,10 @@ public abstract class WMShellModule { static Optional<DesktopDisplayEventHandler> provideDesktopDisplayEventHandler( Context context, ShellInit shellInit, - Transitions transitions, DisplayController displayController, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - IWindowManager windowManager, Optional<DesktopUserRepositories> desktopUserRepositories, Optional<DesktopTasksController> desktopTasksController, - ShellTaskOrganizer shellTaskOrganizer + Optional<DesktopDisplayModeController> desktopDisplayModeController ) { if (!DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.empty(); @@ -1245,13 +1269,10 @@ public abstract class WMShellModule { new DesktopDisplayEventHandler( context, shellInit, - transitions, displayController, - rootTaskDisplayAreaOrganizer, - windowManager, desktopUserRepositories.get(), desktopTasksController.get(), - shellTaskOrganizer)); + desktopDisplayModeController.get())); } @WMSingleton @@ -1381,6 +1402,29 @@ public abstract class WMShellModule { return new DesktopModeUiEventLogger(uiEventLogger, packageManager); } + @WMSingleton + @Provides + static Optional<DesktopDisplayModeController> provideDesktopDisplayModeController( + Context context, + Transitions transitions, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + IWindowManager windowManager, + ShellTaskOrganizer shellTaskOrganizer, + DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider + ) { + if (!DesktopModeStatus.canEnterDesktopMode(context)) { + return Optional.empty(); + } + return Optional.of( + new DesktopDisplayModeController( + context, + transitions, + rootTaskDisplayAreaOrganizer, + windowManager, + shellTaskOrganizer, + desktopWallpaperActivityTokenProvider)); + } + // // App zoom out // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt index c38558d7bde9..946e7952dd50 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt @@ -16,41 +16,25 @@ package com.android.wm.shell.desktopmode -import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD -import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM -import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED -import android.app.WindowConfiguration.windowingModeToString import android.content.Context -import android.provider.Settings -import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS import android.view.Display.DEFAULT_DISPLAY -import android.view.IWindowManager -import android.view.WindowManager.TRANSIT_CHANGE import android.window.DesktopExperienceFlags -import android.window.WindowContainerTransaction import com.android.internal.protolog.ProtoLog -import com.android.window.flags.Flags -import com.android.wm.shell.RootTaskDisplayAreaOrganizer -import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit -import com.android.wm.shell.transition.Transitions /** Handles display events in desktop mode */ class DesktopDisplayEventHandler( private val context: Context, shellInit: ShellInit, - private val transitions: Transitions, private val displayController: DisplayController, - private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, - private val windowManager: IWindowManager, private val desktopUserRepositories: DesktopUserRepositories, private val desktopTasksController: DesktopTasksController, - private val shellTaskOrganizer: ShellTaskOrganizer, + private val desktopDisplayModeController: DesktopDisplayModeController, ) : OnDisplaysChangedListener, OnDeskRemovedListener { private val desktopRepository: DesktopRepository @@ -70,7 +54,7 @@ class DesktopDisplayEventHandler( override fun onDisplayAdded(displayId: Int) { if (displayId != DEFAULT_DISPLAY) { - refreshDisplayWindowingMode() + desktopDisplayModeController.refreshDisplayWindowingMode() } if (!supportsDesks(displayId)) { @@ -88,7 +72,7 @@ class DesktopDisplayEventHandler( override fun onDisplayRemoved(displayId: Int) { if (displayId != DEFAULT_DISPLAY) { - refreshDisplayWindowingMode() + desktopDisplayModeController.refreshDisplayWindowingMode() } // TODO: b/362720497 - move desks in closing display to the remaining desk. @@ -102,65 +86,6 @@ class DesktopDisplayEventHandler( } } - private fun refreshDisplayWindowingMode() { - if (!Flags.enableDisplayWindowingModeSwitching()) return - // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available. - val isExtendedDisplayEnabled = - 0 != - Settings.Global.getInt( - context.contentResolver, - DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, - 0, - ) - if (!isExtendedDisplayEnabled) { - // No action needed in mirror or projected mode. - return - } - - val hasNonDefaultDisplay = - rootTaskDisplayAreaOrganizer.getDisplayIds().any { displayId -> - displayId != DEFAULT_DISPLAY - } - val targetDisplayWindowingMode = - if (hasNonDefaultDisplay) { - WINDOWING_MODE_FREEFORM - } else { - // Use the default display windowing mode when no non-default display. - windowManager.getWindowingMode(DEFAULT_DISPLAY) - } - val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY) - requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." } - val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode - if (currentDisplayWindowingMode == targetDisplayWindowingMode) { - // Already in the target mode. - return - } - - logV( - "As an external display is connected, changing default display's windowing mode from" + - " ${windowingModeToString(currentDisplayWindowingMode)}" + - " to ${windowingModeToString(targetDisplayWindowingMode)}" - ) - - val wct = WindowContainerTransaction() - wct.setWindowingMode(tdaInfo.token, targetDisplayWindowingMode) - shellTaskOrganizer - .getRunningTasks(DEFAULT_DISPLAY) - .filter { it.activityType == ACTIVITY_TYPE_STANDARD } - .forEach { - // TODO: b/391965153 - Reconsider the logic under multi-desk window hierarchy - when (it.windowingMode) { - currentDisplayWindowingMode -> { - wct.setWindowingMode(it.token, currentDisplayWindowingMode) - } - targetDisplayWindowingMode -> { - wct.setWindowingMode(it.token, WINDOWING_MODE_UNDEFINED) - } - } - } - transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) - } - // TODO: b/362720497 - connected/projected display considerations. private fun supportsDesks(displayId: Int): Boolean = DesktopModeStatus.canEnterDesktopMode(context) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt new file mode 100644 index 000000000000..c9a63ff818f5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt @@ -0,0 +1,121 @@ +/* + * 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.desktopmode + +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.app.WindowConfiguration.windowingModeToString +import android.content.Context +import android.provider.Settings +import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS +import android.view.Display.DEFAULT_DISPLAY +import android.view.IWindowManager +import android.view.WindowManager.TRANSIT_CHANGE +import android.window.WindowContainerTransaction +import com.android.internal.protolog.ProtoLog +import com.android.window.flags.Flags +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.transition.Transitions + +/** Controls the display windowing mode in desktop mode */ +class DesktopDisplayModeController( + private val context: Context, + private val transitions: Transitions, + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + private val windowManager: IWindowManager, + private val shellTaskOrganizer: ShellTaskOrganizer, + private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, +) { + + fun refreshDisplayWindowingMode() { + if (!Flags.enableDisplayWindowingModeSwitching()) return + // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available. + val isExtendedDisplayEnabled = + 0 != + Settings.Global.getInt( + context.contentResolver, + DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, + 0, + ) + if (!isExtendedDisplayEnabled) { + // No action needed in mirror or projected mode. + return + } + + val hasNonDefaultDisplay = + rootTaskDisplayAreaOrganizer.getDisplayIds().any { displayId -> + displayId != DEFAULT_DISPLAY + } + val targetDisplayWindowingMode = + if (hasNonDefaultDisplay) { + WINDOWING_MODE_FREEFORM + } else { + // Use the default display windowing mode when no non-default display. + windowManager.getWindowingMode(DEFAULT_DISPLAY) + } + val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY) + requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." } + val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode + if (currentDisplayWindowingMode == targetDisplayWindowingMode) { + // Already in the target mode. + return + } + + logV( + "As an external display is connected, changing default display's windowing mode from" + + " ${windowingModeToString(currentDisplayWindowingMode)}" + + " to ${windowingModeToString(targetDisplayWindowingMode)}" + ) + + val wct = WindowContainerTransaction() + wct.setWindowingMode(tdaInfo.token, targetDisplayWindowingMode) + shellTaskOrganizer + .getRunningTasks(DEFAULT_DISPLAY) + .filter { it.activityType == ACTIVITY_TYPE_STANDARD } + .forEach { + // TODO: b/391965153 - Reconsider the logic under multi-desk window hierarchy + when (it.windowingMode) { + currentDisplayWindowingMode -> { + wct.setWindowingMode(it.token, currentDisplayWindowingMode) + } + targetDisplayWindowingMode -> { + wct.setWindowingMode(it.token, WINDOWING_MODE_UNDEFINED) + } + } + } + // The override windowing mode of DesktopWallpaper can be UNDEFINED on fullscreen-display + // right after the first launch while its resolved windowing mode is FULLSCREEN. We here + // it has the FULLSCREEN override windowing mode. + desktopWallpaperActivityTokenProvider.getToken(DEFAULT_DISPLAY)?.let { token -> + wct.setWindowingMode(token, WINDOWING_MODE_FULLSCREEN) + } + transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) + } + + private fun logV(msg: String, vararg arguments: Any?) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + companion object { + private const val TAG = "DesktopDisplayModeController" + } +} 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 eba1be517147..031925b2997a 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 @@ -208,6 +208,7 @@ class DesktopRepository( /** Adds the given desk under the given display. */ fun addDesk(displayId: Int, deskId: Int) { + logD("addDesk for displayId=%d and deskId=%d", displayId, deskId) desktopData.createDesk(displayId, deskId) } @@ -224,6 +225,7 @@ class DesktopRepository( /** 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) desktopData.setActiveDesk(displayId = displayId, deskId = deskId) } @@ -246,6 +248,7 @@ class DesktopRepository( * TODO: b/389960283 - add explicit [deskId] argument. */ fun addTask(displayId: Int, taskId: Int, isVisible: Boolean) { + logD("addTask for displayId=%d, taskId=%d, isVisible=%b", displayId, taskId, isVisible) val activeDesk = checkNotNull(desktopData.getDefaultDesk(displayId)) { "Expected desk in display: $displayId" @@ -254,6 +257,13 @@ class DesktopRepository( } fun addTaskToDesk(displayId: Int, deskId: Int, taskId: Int, isVisible: Boolean) { + logD( + "addTaskToDesk for displayId=%d, deskId=%d, taskId=%d, isVisible=%b", + displayId, + deskId, + taskId, + isVisible, + ) addOrMoveTaskToTopOfDesk(displayId = displayId, deskId = deskId, taskId = taskId) addActiveTaskToDesk(displayId = displayId, deskId = deskId, taskId = taskId) updateTaskInDesk( @@ -265,6 +275,12 @@ class DesktopRepository( } private fun addActiveTaskToDesk(displayId: Int, deskId: Int, taskId: Int) { + logD( + "addActiveTaskToDesk for displayId=%d, deskId=%d, taskId=%d", + displayId, + deskId, + taskId, + ) val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" } // Removes task if it is active on another desk excluding this desk. @@ -279,6 +295,7 @@ class DesktopRepository( /** Removes task from active task list of desks excluding the [excludedDeskId]. */ @VisibleForTesting fun removeActiveTask(taskId: Int, excludedDeskId: Int? = null) { + logD("removeActiveTask for taskId=%d, excludedDeskId=%d", taskId, excludedDeskId) val affectedDisplays = mutableSetOf<Int>() desktopData .desksSequence() @@ -303,6 +320,7 @@ class DesktopRepository( taskId: Int, notifyListeners: Boolean = true, ): Boolean { + logD("removeActiveTaskFromDesk for deskId=%d, taskId=%d", deskId, taskId) val desk = desktopData.getDesk(deskId) ?: return false if (desk.activeTasks.remove(taskId)) { logD("Removed active task=%d from deskId=%d", taskId, desk.deskId) @@ -456,7 +474,7 @@ class DesktopRepository( /** Removes task from visible tasks of all desks except [excludedDeskId]. */ private fun removeVisibleTask(taskId: Int, excludedDeskId: Int? = null) { - desktopData.forAllDesks { displayId, desk -> + desktopData.forAllDesks { _, desk -> if (desk.deskId != excludedDeskId) { removeVisibleTaskFromDesk(deskId = desk.deskId, taskId = taskId) } @@ -718,6 +736,12 @@ class DesktopRepository( * Unminimizes the task if it is minimized. */ private fun addOrMoveTaskToTopOfDesk(displayId: Int, deskId: Int, taskId: Int) { + logD( + "addOrMoveTaskToTopOfDesk displayId=%d, deskId=%d, taskId=%d", + displayId, + deskId, + taskId, + ) val desk = desktopData.getDesk(deskId) ?: error("Could not find desk: $deskId") logD("addOrMoveTaskToTopOfDesk: display=%d deskId=%d taskId=%d", displayId, deskId, taskId) desktopData.forAllDesks { _, desk1 -> desk1.freeformTasksInZOrder.remove(taskId) } @@ -738,6 +762,7 @@ class DesktopRepository( * desk id instead of using this function and defaulting to the active one. */ fun minimizeTask(displayId: Int, taskId: Int) { + logD("minimizeTask displayId=%d, taskId=%d", displayId, taskId) if (displayId == INVALID_DISPLAY) { // When a task vanishes it doesn't have a displayId. Find the display of the task and // mark it as minimized. @@ -756,7 +781,7 @@ class DesktopRepository( /** Minimizes the task in its desk. */ @VisibleForTesting fun minimizeTaskInDesk(displayId: Int, deskId: Int, taskId: Int) { - logD("Minimize Task: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId) + logD("MinimizeTaskInDesk: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId) desktopData.getDesk(deskId)?.minimizedTasks?.add(taskId) ?: logD("Minimize task: No active desk found for task: taskId=%d", taskId) updateTaskInDesk(displayId, deskId, taskId, isVisible = false) @@ -771,12 +796,12 @@ class DesktopRepository( * TODO: b/389960283 - consider using [unminimizeTaskFromDesk] instead. */ fun unminimizeTask(displayId: Int, taskId: Int) { - logD("Unminimize Task: display=%d, task=%d", displayId, taskId) + logD("UnminimizeTask: display=%d, task=%d", displayId, taskId) desktopData.forAllDesks(displayId) { desk -> unminimizeTaskFromDesk(desk.deskId, taskId) } } private fun unminimizeTaskFromDesk(deskId: Int, taskId: Int) { - logD("Unminimize Task: deskId=%d, taskId=%d", deskId, taskId) + logD("Unminimize Task from desk: deskId=%d, taskId=%d", deskId, taskId) if (desktopData.getDesk(deskId)?.minimizedTasks?.remove(taskId) != true) { logW("Unminimize Task: deskId=%d, taskId=%d, no task data", deskId, taskId) } @@ -847,6 +872,7 @@ class DesktopRepository( /** Removes the given desk and returns the active tasks in that desk. */ fun removeDesk(deskId: Int): Set<Int> { + logD("removeDesk %d", deskId) val desk = desktopData.getDesk(deskId) ?: return emptySet<Int>().also { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt index e831d5eecdc2..6034299453fb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt @@ -19,13 +19,16 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.window.DesktopModeFlags +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.freeform.TaskChangeListener +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE /** Manages tasks handling specific to Android Desktop Mode. */ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUserRepositories) : TaskChangeListener { override fun onTaskOpening(taskInfo: RunningTaskInfo) { + logD("onTaskOpening for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId) val desktopRepository: DesktopRepository = desktopUserRepositories.getProfile(taskInfo.userId) if (!isFreeformTask(taskInfo) && desktopRepository.isActiveTask(taskInfo.taskId)) { @@ -38,6 +41,7 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser } override fun onTaskChanging(taskInfo: RunningTaskInfo) { + logD("onTaskChanging for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId) val desktopRepository: DesktopRepository = desktopUserRepositories.getProfile(taskInfo.userId) if (!desktopRepository.isActiveTask(taskInfo.taskId)) return @@ -67,9 +71,15 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser // of race conditions and possible duplications with [onTaskChanging]. override fun onNonTransitionTaskChanging(taskInfo: RunningTaskInfo) { // TODO: b/367268953 - Propagate usages from FreeformTaskListener to this method. + logD( + "onNonTransitionTaskChanging for taskId=%d, displayId=%d", + taskInfo.taskId, + taskInfo.displayId, + ) } override fun onTaskMovingToFront(taskInfo: RunningTaskInfo) { + logD("onTaskMovingToFront for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId) val desktopRepository: DesktopRepository = desktopUserRepositories.getProfile(taskInfo.userId) if (!desktopRepository.isActiveTask(taskInfo.taskId)) return @@ -80,10 +90,12 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser } override fun onTaskMovingToBack(taskInfo: RunningTaskInfo) { + logD("onTaskMovingToBack for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId) // TODO: b/367268953 - Connect this with DesktopRepository. } override fun onTaskClosing(taskInfo: RunningTaskInfo) { + logD("onTaskClosing for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId) val desktopRepository: DesktopRepository = desktopUserRepositories.getProfile(taskInfo.userId) if (!desktopRepository.isActiveTask(taskInfo.taskId)) return @@ -104,4 +116,12 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser private fun isFreeformTask(taskInfo: RunningTaskInfo): Boolean = taskInfo.windowingMode == WINDOWING_MODE_FREEFORM + + private fun logD(msg: String, vararg arguments: Any?) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + companion object { + private const val TAG = "DesktopTaskChangeListener" + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl index 59add47fc79d..5f45192569e3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl @@ -18,13 +18,11 @@ package com.android.wm.shell.desktopmode; /** * Defines the state of desks on a display whose ID is `displayId`, which is: - * - `canCreateDesks`: whether it's possible to create new desks on this display. * - `activeDeskId`: the currently active desk Id, or `-1` if none is active. * - `deskId`: the list of desk Ids of the available desks on this display. */ parcelable DisplayDeskState { int displayId; - boolean canCreateDesk; int activeDeskId; int[] deskIds; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl index 7ed1581cdfdb..cefbd8947feb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl @@ -28,7 +28,7 @@ oneway interface IDesktopTaskListener { * Called once when the listener first gets connected to initialize it with the current state of * desks in Shell. */ - void onListenerConnected(in DisplayDeskState[] displayDeskStates); + void onListenerConnected(in DisplayDeskState[] displayDeskStates, boolean canCreateDesks); /** Desktop tasks visibility has changed. Visible if at least 1 task is visible. */ void onTasksVisibilityChanged(int displayId, int visibleTasksCount); @@ -49,10 +49,10 @@ oneway interface IDesktopTaskListener { void onExitDesktopModeTransitionStarted(int transitionDuration); /** - * Called when the conditions that allow the creation of a new desk on the display whose ID is - * `displayId` changes to `canCreateDesks`. It's also called when a new display is added. + * Called when the conditions that allow the creation of a new desk changes. This is a global + * state for the entire device. */ - void onCanCreateDesksChanged(int displayId, boolean canCreateDesks); + void onCanCreateDesksChanged(boolean canCreateDesks); /** Called when a desk whose ID is `deskId` is added to the display whose ID is `displayId`. */ void onDeskAdded(int displayId, int deskId); 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 5a6ea214e561..cf139a008164 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 @@ -103,6 +103,7 @@ import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController; import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -258,6 +259,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final RecentsTransitionHandler mRecentsTransitionHandler; private final DesktopModeCompatPolicy mDesktopModeCompatPolicy; private final DesktopTilingDecorViewModel mDesktopTilingDecorViewModel; + private final MultiDisplayDragMoveIndicatorController mMultiDisplayDragMoveIndicatorController; public DesktopModeWindowDecorViewModel( Context context, @@ -296,7 +298,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, DesktopModeCompatPolicy desktopModeCompatPolicy, - DesktopTilingDecorViewModel desktopTilingDecorViewModel) { + DesktopTilingDecorViewModel desktopTilingDecorViewModel, + MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) { this( context, shellExecutor, @@ -340,7 +343,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy, - desktopTilingDecorViewModel); + desktopTilingDecorViewModel, + multiDisplayDragMoveIndicatorController); } @VisibleForTesting @@ -387,7 +391,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, DesktopModeCompatPolicy desktopModeCompatPolicy, - DesktopTilingDecorViewModel desktopTilingDecorViewModel) { + DesktopTilingDecorViewModel desktopTilingDecorViewModel, + MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; @@ -460,6 +465,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopModeCompatPolicy = desktopModeCompatPolicy; mDesktopTilingDecorViewModel = desktopTilingDecorViewModel; mDesktopTasksController.setSnapEventHandler(this); + mMultiDisplayDragMoveIndicatorController = multiDisplayDragMoveIndicatorController; shellInit.addInitCallback(this::onInit, this); } @@ -1759,7 +1765,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mTransitions, mInteractionJankMonitor, mTransactionFactory, - mMainHandler); + mMainHandler, + mMultiDisplayDragMoveIndicatorController); windowDecoration.setTaskDragResizer(taskPositioner); final DesktopModeTouchEventListener touchEventListener = @@ -2056,7 +2063,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, Transitions transitions, InteractionJankMonitor interactionJankMonitor, Supplier<SurfaceControl.Transaction> transactionFactory, - Handler handler) { + Handler handler, + MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) { final TaskPositioner taskPositioner = DesktopModeStatus.isVeiledResizeEnabled() // TODO(b/383632995): Update when the flag is launched. ? (Flags.enableConnectedDisplaysWindowDrag() @@ -2067,7 +2075,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, dragEventListener, transitions, interactionJankMonitor, - handler) + handler, + multiDisplayDragMoveIndicatorController) : new VeiledResizeTaskPositioner( taskOrganizer, windowDecoration, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt index bb20292a51d4..c6cb62d153ac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.windowdecor import android.graphics.PointF import android.graphics.Rect +import android.hardware.display.DisplayTopology import android.os.Handler import android.os.IBinder import android.os.Looper @@ -32,10 +33,10 @@ import com.android.internal.jank.InteractionJankMonitor import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.MultiDisplayDragMoveBoundsCalculator +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.transition.Transitions import java.util.concurrent.TimeUnit -import java.util.function.Supplier /** * A task positioner that also takes into account resizing a @@ -49,11 +50,12 @@ class MultiDisplayVeiledResizeTaskPositioner( private val desktopWindowDecoration: DesktopModeWindowDecoration, private val displayController: DisplayController, dragEventListener: DragPositioningCallbackUtility.DragEventListener, - private val transactionSupplier: Supplier<SurfaceControl.Transaction>, + private val transactionSupplier: () -> SurfaceControl.Transaction, private val transitions: Transitions, private val interactionJankMonitor: InteractionJankMonitor, @ShellMainThread private val handler: Handler, -) : TaskPositioner, Transitions.TransitionHandler { + private val multiDisplayDragMoveIndicatorController: MultiDisplayDragMoveIndicatorController, +) : TaskPositioner, Transitions.TransitionHandler, DisplayController.OnDisplaysChangedListener { private val dragEventListeners = mutableListOf<DragPositioningCallbackUtility.DragEventListener>() private val stableBounds = Rect() @@ -71,6 +73,7 @@ class MultiDisplayVeiledResizeTaskPositioner( private var isResizingOrAnimatingResize = false @Surface.Rotation private var rotation = 0 private var startDisplayId = 0 + private val displayIds = mutableSetOf<Int>() constructor( taskOrganizer: ShellTaskOrganizer, @@ -80,19 +83,22 @@ class MultiDisplayVeiledResizeTaskPositioner( transitions: Transitions, interactionJankMonitor: InteractionJankMonitor, @ShellMainThread handler: Handler, + multiDisplayDragMoveIndicatorController: MultiDisplayDragMoveIndicatorController, ) : this( taskOrganizer, windowDecoration, displayController, dragEventListener, - Supplier<SurfaceControl.Transaction> { SurfaceControl.Transaction() }, + { SurfaceControl.Transaction() }, transitions, interactionJankMonitor, handler, + multiDisplayDragMoveIndicatorController, ) init { dragEventListeners.add(dragEventListener) + displayController.addDisplayWindowListener(this) } override fun onDragPositioningStart(ctrlType: Int, displayId: Int, x: Float, y: Float): Rect { @@ -164,7 +170,7 @@ class MultiDisplayVeiledResizeTaskPositioner( createLongTimeoutJankConfigBuilder(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW) ) - val t = transactionSupplier.get() + val t = transactionSupplier() val startDisplayLayout = displayController.getDisplayLayout(startDisplayId) val currentDisplayLayout = displayController.getDisplayLayout(displayId) @@ -196,7 +202,13 @@ class MultiDisplayVeiledResizeTaskPositioner( ) ) - // TODO(b/383069173): Render drag indicator(s) + multiDisplayDragMoveIndicatorController.onDragMove( + boundsDp, + startDisplayId, + desktopWindowDecoration.mTaskInfo, + displayIds, + transactionSupplier, + ) t.setPosition( desktopWindowDecoration.leash, @@ -267,7 +279,10 @@ class MultiDisplayVeiledResizeTaskPositioner( ) ) - // TODO(b/383069173): Clear drag indicator(s) + multiDisplayDragMoveIndicatorController.onDragEnd( + desktopWindowDecoration.mTaskInfo.taskId, + transactionSupplier, + ) } interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW) @@ -348,6 +363,14 @@ class MultiDisplayVeiledResizeTaskPositioner( dragEventListeners.remove(dragEventListener) } + override fun onTopologyChanged(topology: DisplayTopology) { + // TODO: b/383069173 - Cancel window drag when topology changes happen during drag. + + displayIds.clear() + val displayBounds = topology.getAbsoluteBounds() + displayIds.addAll(List(displayBounds.size()) { displayBounds.keyAt(it) }) + } + companion object { // Timeout used for resize and drag CUJs, this is longer than the default timeout to avoid // timing out in the middle of a resize or drag action. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorControllerTest.kt new file mode 100644 index 000000000000..abd238847519 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorControllerTest.kt @@ -0,0 +1,168 @@ +/* + * 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.RunningTaskInfo +import android.content.res.Configuration +import android.graphics.Rect +import android.graphics.RectF +import android.testing.TestableResources +import android.view.Display +import android.view.SurfaceControl +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor +import java.util.function.Supplier +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.whenever + +/** + * Tests for [MultiDisplayDragMoveIndicatorController]. + * + * Build/Install/Run: atest WMShellUnitTests:MultiDisplayDragMoveIndicatorControllerTest + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class MultiDisplayDragMoveIndicatorControllerTest : ShellTestCase() { + private val displayController = mock<DisplayController>() + private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>() + private val indicatorSurfaceFactory = mock<MultiDisplayDragMoveIndicatorSurface.Factory>() + private val indicatorSurface0 = mock<MultiDisplayDragMoveIndicatorSurface>() + private val indicatorSurface1 = mock<MultiDisplayDragMoveIndicatorSurface>() + private val transaction = mock<SurfaceControl.Transaction>() + private val transactionSupplier = mock<Supplier<SurfaceControl.Transaction>>() + private val taskInfo = mock<RunningTaskInfo>() + private val display0 = mock<Display>() + private val display1 = mock<Display>() + + private lateinit var resources: TestableResources + private val executor = TestShellExecutor() + + private lateinit var controller: MultiDisplayDragMoveIndicatorController + + @Before + fun setUp() { + resources = mContext.getOrCreateTestableResources() + val resourceConfiguration = Configuration() + resourceConfiguration.uiMode = 0 + resources.overrideConfiguration(resourceConfiguration) + + controller = + MultiDisplayDragMoveIndicatorController( + displayController, + rootTaskDisplayAreaOrganizer, + indicatorSurfaceFactory, + executor, + ) + + val spyDisplayLayout0 = + MultiDisplayTestUtil.createSpyDisplayLayout( + MultiDisplayTestUtil.DISPLAY_GLOBAL_BOUNDS_0, + MultiDisplayTestUtil.DISPLAY_DPI_0, + resources.resources, + ) + val spyDisplayLayout1 = + MultiDisplayTestUtil.createSpyDisplayLayout( + MultiDisplayTestUtil.DISPLAY_GLOBAL_BOUNDS_1, + MultiDisplayTestUtil.DISPLAY_DPI_1, + resources.resources, + ) + + taskInfo.taskId = TASK_ID + whenever(displayController.getDisplayLayout(0)).thenReturn(spyDisplayLayout0) + whenever(displayController.getDisplayLayout(1)).thenReturn(spyDisplayLayout1) + whenever(displayController.getDisplay(0)).thenReturn(display0) + whenever(displayController.getDisplay(1)).thenReturn(display1) + whenever(indicatorSurfaceFactory.create(taskInfo, display0)).thenReturn(indicatorSurface0) + whenever(indicatorSurfaceFactory.create(taskInfo, display1)).thenReturn(indicatorSurface1) + whenever(transactionSupplier.get()).thenReturn(transaction) + } + + @Test + fun onDrag_boundsNotIntersectWithDisplay_noIndicator() { + controller.onDragMove( + RectF(2000f, 2000f, 2100f, 2200f), // not intersect with any display + startDisplayId = 0, + taskInfo, + displayIds = setOf(0, 1), + ) { transaction } + executor.flushAll() + + verify(indicatorSurfaceFactory, never()).create(any(), any()) + } + + @Test + fun onDrag_boundsIntersectWithStartDisplay_noIndicator() { + controller.onDragMove( + RectF(100f, 100f, 200f, 200f), // intersect with display 0 + startDisplayId = 0, + taskInfo, + displayIds = setOf(0, 1), + ) { transaction } + executor.flushAll() + + verify(indicatorSurfaceFactory, never()).create(any(), any()) + } + + @Test + fun onDrag_boundsIntersectWithNonStartDisplay_showAndDisposeIndicator() { + controller.onDragMove( + RectF(100f, -100f, 200f, 200f), // intersect with display 0 and 1 + startDisplayId = 0, + taskInfo, + displayIds = setOf(0, 1), + ) { transaction } + executor.flushAll() + + verify(indicatorSurfaceFactory, times(1)).create(taskInfo, display1) + verify(indicatorSurface1, times(1)) + .show(transaction, taskInfo, rootTaskDisplayAreaOrganizer, 1, Rect(0, 1800, 200, 2400)) + + controller.onDragMove( + RectF(2000f, 2000f, 2100f, 2200f), // not intersect with display 1 + startDisplayId = 0, + taskInfo, + displayIds = setOf(0, 1) + ) { transaction } + while (executor.callbacks.isNotEmpty()) { + executor.flushAll() + } + + verify(indicatorSurface1, times(1)) + .relayout(any(), eq(transaction), shouldBeVisible = eq(false)) + + controller.onDragEnd(TASK_ID, { transaction }) + while (executor.callbacks.isNotEmpty()) { + executor.flushAll() + } + + verify(indicatorSurface1, times(1)).disposeSurface(transaction) + } + + companion object { + private const val TASK_ID = 10 + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt index 0d5741fccbcc..8ad54f5a0bb4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt @@ -16,51 +16,28 @@ package com.android.wm.shell.desktopmode -import android.app.ActivityManager.RunningTaskInfo -import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM -import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN -import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED -import android.content.ContentResolver -import android.os.Binder import android.platform.test.annotations.EnableFlags -import android.provider.Settings -import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY -import android.view.IWindowManager -import android.view.WindowManager.TRANSIT_CHANGE -import android.window.DisplayAreaInfo -import android.window.WindowContainerTransaction import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.ExtendedMockito.never import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.window.flags.Flags -import com.android.wm.shell.MockToken -import com.android.wm.shell.RootTaskDisplayAreaOrganizer -import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit -import com.android.wm.shell.transition.Transitions -import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock -import org.mockito.Mockito.anyInt import org.mockito.Mockito.spy -import org.mockito.Mockito.times import org.mockito.Mockito.verify -import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.eq import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -73,27 +50,18 @@ import org.mockito.quality.Strictness @RunWith(AndroidTestingRunner::class) class DesktopDisplayEventHandlerTest : ShellTestCase() { @Mock lateinit var testExecutor: ShellExecutor - @Mock lateinit var transitions: Transitions @Mock lateinit var displayController: DisplayController - @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer - @Mock private lateinit var mockWindowManager: IWindowManager @Mock private lateinit var mockDesktopUserRepositories: DesktopUserRepositories @Mock private lateinit var mockDesktopRepository: DesktopRepository @Mock private lateinit var mockDesktopTasksController: DesktopTasksController - @Mock private lateinit var shellTaskOrganizer: ShellTaskOrganizer + @Mock private lateinit var desktopDisplayModeController: DesktopDisplayModeController private lateinit var mockitoSession: StaticMockitoSession private lateinit var shellInit: ShellInit private lateinit var handler: DesktopDisplayEventHandler private val onDisplaysChangedListenerCaptor = argumentCaptor<OnDisplaysChangedListener>() - private val runningTasks = mutableListOf<RunningTaskInfo>() private val externalDisplayId = 100 - private val freeformTask = - TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build() - private val fullscreenTask = - TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FULLSCREEN).build() - private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) @Before fun setUp() { @@ -105,24 +73,15 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { shellInit = spy(ShellInit(testExecutor)) whenever(mockDesktopUserRepositories.current).thenReturn(mockDesktopRepository) - whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } - whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) - .thenReturn(defaultTDA) handler = DesktopDisplayEventHandler( context, shellInit, - transitions, displayController, - rootTaskDisplayAreaOrganizer, - mockWindowManager, mockDesktopUserRepositories, mockDesktopTasksController, - shellTaskOrganizer, + desktopDisplayModeController, ) - whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } - runningTasks.add(freeformTask) - runningTasks.add(fullscreenTask) shellInit.init() verify(displayController) .addDisplayWindowListener(onDisplaysChangedListenerCaptor.capture()) @@ -133,65 +92,6 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { mockitoSession.finishMocking() } - private fun testDisplayWindowingModeSwitch( - defaultWindowingMode: Int, - extendedDisplayEnabled: Boolean, - expectTransition: Boolean, - ) { - defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode - whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { defaultWindowingMode } - val settingsSession = - ExtendedDisplaySettingsSession( - context.contentResolver, - if (extendedDisplayEnabled) 1 else 0, - ) - - settingsSession.use { - connectExternalDisplay() - defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - disconnectExternalDisplay() - - if (expectTransition) { - val arg = argumentCaptor<WindowContainerTransaction>() - verify(transitions, times(2)) - .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) - assertThat(arg.firstValue.changes[defaultTDA.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - assertThat(arg.secondValue.changes[defaultTDA.token.asBinder()]?.windowingMode) - .isEqualTo(defaultWindowingMode) - } else { - verify(transitions, never()).startTransition(eq(TRANSIT_CHANGE), any(), isNull()) - } - } - } - - @Test - fun displayWindowingModeSwitchOnDisplayConnected_extendedDisplayDisabled() { - testDisplayWindowingModeSwitch( - defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, - extendedDisplayEnabled = false, - expectTransition = false, - ) - } - - @Test - fun displayWindowingModeSwitchOnDisplayConnected_fullscreenDisplay() { - testDisplayWindowingModeSwitch( - defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, - extendedDisplayEnabled = true, - expectTransition = true, - ) - } - - @Test - fun displayWindowingModeSwitchOnDisplayConnected_freeformDisplay() { - testDisplayWindowingModeSwitch( - defaultWindowingMode = WINDOWING_MODE_FREEFORM, - extendedDisplayEnabled = true, - expectTransition = false, - ) - } - @Test fun testDisplayAdded_supportsDesks_createsDesk() { whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) @@ -231,70 +131,14 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { } @Test - fun displayWindowingModeSwitch_existingTasksOnConnected() { - defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { - WINDOWING_MODE_FULLSCREEN - } - - ExtendedDisplaySettingsSession(context.contentResolver, 1).use { - connectExternalDisplay() - - val arg = argumentCaptor<WindowContainerTransaction>() - verify(transitions, times(1)) - .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) - assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) - assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) - } - } - - @Test - fun displayWindowingModeSwitch_existingTasksOnDisconnected() { - defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { - WINDOWING_MODE_FULLSCREEN - } - - ExtendedDisplaySettingsSession(context.contentResolver, 1).use { - disconnectExternalDisplay() - - val arg = argumentCaptor<WindowContainerTransaction>() - verify(transitions, times(1)) - .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) - assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) - } - } - - private fun connectExternalDisplay() { - whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) - .thenReturn(intArrayOf(DEFAULT_DISPLAY, externalDisplayId)) + fun testConnectExternalDisplay() { onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(externalDisplayId) + verify(desktopDisplayModeController).refreshDisplayWindowingMode() } - private fun disconnectExternalDisplay() { - whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) - .thenReturn(intArrayOf(DEFAULT_DISPLAY)) + @Test + fun testDisconnectExternalDisplay() { onDisplaysChangedListenerCaptor.lastValue.onDisplayRemoved(externalDisplayId) - } - - private class ExtendedDisplaySettingsSession( - private val contentResolver: ContentResolver, - private val overrideValue: Int, - ) : AutoCloseable { - private val settingName = DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS - private val initialValue = Settings.Global.getInt(contentResolver, settingName, 0) - - init { - Settings.Global.putInt(contentResolver, settingName, overrideValue) - } - - override fun close() { - Settings.Global.putInt(contentResolver, settingName, initialValue) - } + verify(desktopDisplayModeController).refreshDisplayWindowingMode() } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt new file mode 100644 index 000000000000..0ff7230f6e0c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt @@ -0,0 +1,233 @@ +/* + * 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.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.content.ContentResolver +import android.os.Binder +import android.provider.Settings +import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS +import android.testing.AndroidTestingRunner +import android.view.Display.DEFAULT_DISPLAY +import android.view.IWindowManager +import android.view.WindowManager.TRANSIT_CHANGE +import android.window.DisplayAreaInfo +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.never +import com.android.wm.shell.MockToken +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider +import com.android.wm.shell.transition.Transitions +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.isNull +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** + * Test class for [DesktopDisplayModeController] + * + * Usage: atest WMShellUnitTests:DesktopDisplayModeControllerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopDisplayModeControllerTest : ShellTestCase() { + private val transitions = mock<Transitions>() + private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>() + private val mockWindowManager = mock<IWindowManager>() + private val shellTaskOrganizer = mock<ShellTaskOrganizer>() + private val desktopWallpaperActivityTokenProvider = + mock<DesktopWallpaperActivityTokenProvider>() + + private lateinit var controller: DesktopDisplayModeController + + private val runningTasks = mutableListOf<RunningTaskInfo>() + private val freeformTask = + TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build() + private val fullscreenTask = + TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FULLSCREEN).build() + private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + private val wallpaperToken = MockToken().token() + + @Before + fun setUp() { + whenever(transitions.startTransition(anyInt(), any(), isNull())).thenReturn(Binder()) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .thenReturn(defaultTDA) + controller = + DesktopDisplayModeController( + context, + transitions, + rootTaskDisplayAreaOrganizer, + mockWindowManager, + shellTaskOrganizer, + desktopWallpaperActivityTokenProvider, + ) + runningTasks.add(freeformTask) + runningTasks.add(fullscreenTask) + whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(ArrayList(runningTasks)) + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken) + } + + private fun testDisplayWindowingModeSwitch( + defaultWindowingMode: Int, + extendedDisplayEnabled: Boolean, + expectTransition: Boolean, + ) { + defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode + whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(defaultWindowingMode) + val settingsSession = + ExtendedDisplaySettingsSession( + context.contentResolver, + if (extendedDisplayEnabled) 1 else 0, + ) + + settingsSession.use { + connectExternalDisplay() + defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + disconnectExternalDisplay() + + if (expectTransition) { + val arg = argumentCaptor<WindowContainerTransaction>() + verify(transitions, times(2)) + .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) + assertThat(arg.firstValue.changes[defaultTDA.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + assertThat(arg.firstValue.changes[wallpaperToken.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + assertThat(arg.secondValue.changes[defaultTDA.token.asBinder()]?.windowingMode) + .isEqualTo(defaultWindowingMode) + assertThat(arg.secondValue.changes[wallpaperToken.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + } else { + verify(transitions, never()).startTransition(eq(TRANSIT_CHANGE), any(), isNull()) + } + } + } + + @Test + fun displayWindowingModeSwitchOnDisplayConnected_extendedDisplayDisabled() { + testDisplayWindowingModeSwitch( + defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + extendedDisplayEnabled = false, + expectTransition = false, + ) + } + + @Test + fun displayWindowingModeSwitchOnDisplayConnected_fullscreenDisplay() { + testDisplayWindowingModeSwitch( + defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + extendedDisplayEnabled = true, + expectTransition = true, + ) + } + + @Test + fun displayWindowingModeSwitchOnDisplayConnected_freeformDisplay() { + testDisplayWindowingModeSwitch( + defaultWindowingMode = WINDOWING_MODE_FREEFORM, + extendedDisplayEnabled = true, + expectTransition = false, + ) + } + + @Test + fun displayWindowingModeSwitch_existingTasksOnConnected() { + defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(WINDOWING_MODE_FULLSCREEN) + + ExtendedDisplaySettingsSession(context.contentResolver, 1).use { + connectExternalDisplay() + + val arg = argumentCaptor<WindowContainerTransaction>() + verify(transitions, times(1)) + .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) + assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + } + } + + @Test + fun displayWindowingModeSwitch_existingTasksOnDisconnected() { + defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { + WINDOWING_MODE_FULLSCREEN + } + + ExtendedDisplaySettingsSession(context.contentResolver, 1).use { + disconnectExternalDisplay() + + val arg = argumentCaptor<WindowContainerTransaction>() + verify(transitions, times(1)) + .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) + assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + } + + private fun connectExternalDisplay() { + whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, EXTERNAL_DISPLAY_ID)) + controller.refreshDisplayWindowingMode() + } + + private fun disconnectExternalDisplay() { + whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) + .thenReturn(intArrayOf(DEFAULT_DISPLAY)) + controller.refreshDisplayWindowingMode() + } + + private class ExtendedDisplaySettingsSession( + private val contentResolver: ContentResolver, + private val overrideValue: Int, + ) : AutoCloseable { + private val settingName = DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS + private val initialValue = Settings.Global.getInt(contentResolver, settingName, 0) + + init { + Settings.Global.putInt(contentResolver, settingName, overrideValue) + } + + override fun close() { + Settings.Global.putInt(contentResolver, settingName, initialValue) + } + } + + private companion object { + const val EXTERNAL_DISPLAY_ID = 100 + } +} 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 8cccdb2b6120..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 @@ -52,6 +52,7 @@ import com.android.wm.shell.common.DisplayChangeController import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayInsetsController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler @@ -138,6 +139,8 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { protected val mockFreeformTaskTransitionStarter = mock<FreeformTaskTransitionStarter>() protected val mockActivityOrientationChangeHandler = mock<DesktopActivityOrientationChangeHandler>() + protected val mockMultiDisplayDragMoveIndicatorController = + mock<MultiDisplayDragMoveIndicatorController>() protected val mockInputManager = mock<InputManager>() private val mockTaskPositionerFactory = mock<DesktopModeWindowDecorViewModel.TaskPositionerFactory>() @@ -229,6 +232,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { mockRecentsTransitionHandler, desktopModeCompatPolicy, mockTilingWindowDecoration, + mockMultiDisplayDragMoveIndicatorController, ) desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) @@ -243,6 +247,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { any(), any(), any(), + any(), any() ) ) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt index 937938df82c8..a6b077037b86 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt @@ -41,6 +41,7 @@ import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController import com.android.wm.shell.common.MultiDisplayTestUtil import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionFinishCallback @@ -62,8 +63,8 @@ import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.`when` +import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations /** @@ -93,7 +94,8 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { @Mock private lateinit var mockTransitions: Transitions @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor @Mock private lateinit var mockSurfaceControl: SurfaceControl - + @Mock private lateinit var mockMultiDisplayDragMoveIndicatorController: + MultiDisplayDragMoveIndicatorController private lateinit var resources: TestableResources private lateinit var spyDisplayLayout0: DisplayLayout private lateinit var spyDisplayLayout1: DisplayLayout @@ -170,10 +172,11 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { mockDesktopWindowDecoration, mockDisplayController, mockDragEventListener, - mockTransactionFactory, + { mockTransaction }, mockTransitions, mockInteractionJankMonitor, mainHandler, + mockMultiDisplayDragMoveIndicatorController, ) } diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp index 4fe0b80f3951..275972495206 100644 --- a/native/android/surface_control.cpp +++ b/native/android/surface_control.cpp @@ -715,47 +715,45 @@ void ASurfaceTransaction_setLuts(ASurfaceTransaction* aSurfaceTransaction, sp<SurfaceControl> surfaceControl = ASurfaceControl_to_SurfaceControl(aSurfaceControl); Transaction* transaction = ASurfaceTransaction_to_Transaction(aSurfaceTransaction); - int fd = -1; + base::unique_fd fd; std::vector<int32_t> offsets; std::vector<int32_t> dimensions; std::vector<int32_t> sizes; std::vector<int32_t> samplingKeys; if (luts) { - std::vector<float> buffer(luts->totalBufferSize); int32_t count = luts->offsets.size(); offsets = luts->offsets; dimensions.reserve(count); sizes.reserve(count); samplingKeys.reserve(count); - for (int32_t i = 0; i < count; i++) { - dimensions.emplace_back(luts->entries[i]->properties.dimension); - sizes.emplace_back(luts->entries[i]->properties.size); - samplingKeys.emplace_back(luts->entries[i]->properties.samplingKey); - std::copy(luts->entries[i]->buffer.data.begin(), luts->entries[i]->buffer.data.end(), - buffer.begin() + offsets[i]); - } // mmap - fd = ashmem_create_region("lut_shared_mem", luts->totalBufferSize * sizeof(float)); + fd.reset(ashmem_create_region("lut_shared_mem", luts->totalBufferSize * sizeof(float))); if (fd < 0) { LOG_ALWAYS_FATAL("setLuts, ashmem_create_region() failed"); return; } - void* ptr = mmap(nullptr, luts->totalBufferSize * sizeof(float), PROT_READ | PROT_WRITE, - MAP_SHARED, fd, 0); + float* ptr = (float*)mmap(nullptr, luts->totalBufferSize * sizeof(float), + PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (ptr == MAP_FAILED) { LOG_ALWAYS_FATAL("setLuts, Failed to map the shared memory"); return; } - memcpy(ptr, buffer.data(), luts->totalBufferSize * sizeof(float)); + for (int32_t i = 0; i < count; i++) { + dimensions.emplace_back(luts->entries[i]->properties.dimension); + sizes.emplace_back(luts->entries[i]->properties.size); + samplingKeys.emplace_back(luts->entries[i]->properties.samplingKey); + std::copy(luts->entries[i]->buffer.data.begin(), luts->entries[i]->buffer.data.end(), + ptr + offsets[i]); + } + munmap(ptr, luts->totalBufferSize * sizeof(float)); } - transaction->setLuts(surfaceControl, base::unique_fd(fd), offsets, dimensions, sizes, - samplingKeys); + transaction->setLuts(surfaceControl, std::move(fd), offsets, dimensions, sizes, samplingKeys); } void ASurfaceTransaction_setColor(ASurfaceTransaction* aSurfaceTransaction, diff --git a/packages/CtsShim/build/Android.bp b/packages/CtsShim/build/Android.bp index bd892637a5eb..853d1ad0dc94 100644 --- a/packages/CtsShim/build/Android.bp +++ b/packages/CtsShim/build/Android.bp @@ -152,34 +152,6 @@ android_app { } //########################################################## -// Variant: System app upgrade - -android_app { - name: "CtsShimUpgrade", - - sdk_version: "current", - optimize: { - enabled: false, - }, - dex_preopt: { - enabled: false, - }, - - manifest: "shim/AndroidManifestUpgrade.xml", - min_sdk_version: "24", -} - -java_genrule { - name: "generate_shim_manifest", - srcs: [ - "shim/AndroidManifest.xml", - ":CtsShimUpgrade", - ], - out: ["AndroidManifest.xml"], - cmd: "sed -e s/__HASH__/`sha512sum -b $(location :CtsShimUpgrade) | cut -d' ' -f1`/ $(location shim/AndroidManifest.xml) > $(out)", -} - -//########################################################## // Variant: System app android_app { @@ -193,7 +165,7 @@ android_app { enabled: false, }, - manifest: ":generate_shim_manifest", + manifest: "shim/AndroidManifest.xml", apex_available: [ "//apex_available:platform", "com.android.apex.cts.shim.v1", diff --git a/packages/CtsShim/build/shim/AndroidManifest.xml b/packages/CtsShim/build/shim/AndroidManifest.xml index 3b8276b66bac..1ffe56c0c76a 100644 --- a/packages/CtsShim/build/shim/AndroidManifest.xml +++ b/packages/CtsShim/build/shim/AndroidManifest.xml @@ -17,15 +17,13 @@ <!-- Manifest for the system CTS shim --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" - package="com.android.cts.ctsshim" - android:sharedUserId="com.android.cts.ctsshim" > + package="com.android.cts.ctsshim" > - <uses-sdk - android:minSdkVersion="24" + <uses-sdk android:minSdkVersion="24" android:targetSdkVersion="28" /> <restrict-update - android:hash="__HASH__" /> + android:hash="__CAN_NOT_BE_UPDATED__" /> <application android:hasCode="false" diff --git a/packages/CtsShim/build/shim/AndroidManifestUpgrade.xml b/packages/CtsShim/build/shim/AndroidManifestUpgrade.xml deleted file mode 100644 index 7f3644a18ad6..000000000000 --- a/packages/CtsShim/build/shim/AndroidManifestUpgrade.xml +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2016 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 for the system CTS shim --> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - package="com.android.cts.ctsshim" > - - <uses-sdk - android:minSdkVersion="24" - android:targetSdkVersion="28" /> - - <application - android:hasCode="false" - tools:ignore="AllowBackup,MissingApplicationIcon" /> -</manifest> - diff --git a/packages/SettingsLib/IllustrationPreference/AndroidManifest.xml b/packages/SettingsLib/IllustrationPreference/AndroidManifest.xml index a0d10c383445..56b05159a30f 100644 --- a/packages/SettingsLib/IllustrationPreference/AndroidManifest.xml +++ b/packages/SettingsLib/IllustrationPreference/AndroidManifest.xml @@ -18,6 +18,6 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.settingslib.widget.preference.illustration"> - <uses-sdk android:minSdkVersion="28" /> + <uses-sdk android:minSdkVersion="30" /> </manifest> diff --git a/packages/SettingsLib/IllustrationPreference/res/values/strings.xml b/packages/SettingsLib/IllustrationPreference/res/values/strings.xml new file mode 100644 index 000000000000..3a8aaf8b5092 --- /dev/null +++ b/packages/SettingsLib/IllustrationPreference/res/values/strings.xml @@ -0,0 +1,27 @@ +<?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. + --> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Label for an accessibility action that starts an animation [CHAR LIMIT=30] --> + <string name="settingslib_action_label_resume">resume</string> + <!-- Label for an accessibility action that stops an animation [CHAR LIMIT=30] --> + <string name="settingslib_action_label_pause">pause</string> + <!-- Label for an accessibility action that stops an animation [CHAR LIMIT=50] --> + <string name="settingslib_state_animation_playing">Animation playing</string> + <!-- Label for an accessibility action that stops an animation [CHAR LIMIT=50] --> + <string name="settingslib_state_animation_paused">Animation paused</string> +</resources> diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java index af40c647e805..e818a603c5b4 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java @@ -32,6 +32,8 @@ import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.widget.FrameLayout; import android.widget.ImageView; @@ -73,6 +75,7 @@ public class IllustrationPreference extends Preference implements GroupSectionDi private boolean mLottieDynamicColor; private CharSequence mContentDescription; private boolean mIsTablet; + private boolean mIsAnimatable; private boolean mIsAnimationPaused; /** @@ -81,6 +84,7 @@ public class IllustrationPreference extends Preference implements GroupSectionDi public interface OnBindListener { /** * Called when when {@link #onBindViewHolder(PreferenceViewHolder)} occurs. + * * @param animationView the animation view for this preference. */ void onBind(LottieAnimationView animationView); @@ -144,16 +148,6 @@ public class IllustrationPreference extends Preference implements GroupSectionDi (FrameLayout) holder.findViewById(R.id.middleground_layout); final LottieAnimationView illustrationView = (LottieAnimationView) holder.findViewById(R.id.lottie_view); - // Pause and resume animation - illustrationFrame.setOnClickListener(v -> { - mIsAnimationPaused = !mIsAnimationPaused; - if (mIsAnimationPaused) { - illustrationView.pauseAnimation(); - } else { - illustrationView.resumeAnimation(); - } - }); - if (illustrationView != null && !TextUtils.isEmpty(mContentDescription)) { illustrationView.setContentDescription(mContentDescription); illustrationView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); @@ -171,12 +165,13 @@ public class IllustrationPreference extends Preference implements GroupSectionDi illustrationView.setCacheComposition(mCacheComposition); handleImageWithAnimation(illustrationView, illustrationFrame); + handleAnimationControl(illustrationView, illustrationFrame); handleImageFrameMaxHeight(backgroundView, illustrationView); if (mIsAutoScale) { illustrationView.setScaleType(mIsAutoScale - ? ImageView.ScaleType.CENTER_CROP - : ImageView.ScaleType.CENTER_INSIDE); + ? ImageView.ScaleType.CENTER_CROP + : ImageView.ScaleType.CENTER_INSIDE); } handleMiddleGroundView(middleGroundLayout); @@ -377,6 +372,7 @@ public class IllustrationPreference extends Preference implements GroupSectionDi final Drawable drawable = illustrationView.getDrawable(); if (drawable != null) { startAnimation(drawable); + mIsAnimatable = false; } } @@ -386,10 +382,12 @@ public class IllustrationPreference extends Preference implements GroupSectionDi final Drawable drawable = illustrationView.getDrawable(); if (drawable != null) { startAnimation(drawable); + mIsAnimatable = false; } else { // The lottie image from the raw folder also returns null because the ImageView // couldn't handle it now. startLottieAnimationWith(illustrationView, mImageUri); + mIsAnimatable = true; } } @@ -418,10 +416,12 @@ public class IllustrationPreference extends Preference implements GroupSectionDi final Drawable drawable = illustrationView.getDrawable(); if (drawable != null) { startAnimation(drawable); + mIsAnimatable = false; } else { // The lottie image from the raw folder also returns null because the ImageView // couldn't handle it now. startLottieAnimationWith(illustrationView, mImageResId); + mIsAnimatable = true; } } } @@ -459,6 +459,60 @@ public class IllustrationPreference extends Preference implements GroupSectionDi ((Animatable) drawable).start(); } + private void handleAnimationControl(LottieAnimationView illustrationView, + ViewGroup container) { + if (mIsAnimatable) { + // TODO(b/397340540): list out pages having illustration without a content description. + if (TextUtils.isEmpty(mContentDescription)) { + Log.w(TAG, "Illustration should have a content description. preference key = " + + getKey()); + } + // Enable pause and resume abilities to animation only + container.setOnClickListener(v -> { + mIsAnimationPaused = !mIsAnimationPaused; + if (mIsAnimationPaused) { + illustrationView.pauseAnimation(); + } else { + illustrationView.resumeAnimation(); + } + updateAccessibilityAction(container); + }); + + updateAccessibilityAction(container); + } + } + + private void updateAccessibilityAction(ViewGroup container) { + // Setting the state of animation + container.setStateDescription(getStateDescriptionForAnimation()); + container.setAccessibilityDelegate(new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + final AccessibilityAction clickAction = new AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLICK, + getActionLabelForAnimation()); + info.addAction(clickAction); + } + }); + } + + private String getActionLabelForAnimation() { + if (mIsAnimationPaused) { + return getContext().getString(R.string.settingslib_action_label_resume); + } else { + return getContext().getString(R.string.settingslib_action_label_pause); + } + } + + private String getStateDescriptionForAnimation() { + if (mIsAnimationPaused) { + return getContext().getString(R.string.settingslib_state_animation_paused); + } else { + return getContext().getString(R.string.settingslib_state_animation_playing); + } + } + private static void startLottieAnimationWith(LottieAnimationView illustrationView, Uri imageUri) { final InputStream inputStream = @@ -514,15 +568,20 @@ public class IllustrationPreference extends Preference implements GroupSectionDi mIsAutoScale = false; if (attrs != null) { TypedArray a = context.obtainStyledAttributes(attrs, - com.airbnb.lottie.R.styleable.LottieAnimationView, 0 /*defStyleAttr*/, 0 /*defStyleRes*/); - mImageResId = a.getResourceId(com.airbnb.lottie.R.styleable.LottieAnimationView_lottie_rawRes, 0); + com.airbnb.lottie.R.styleable.LottieAnimationView, /* defStyleAttr= */ 0, + /* defStyleRes= */ 0); + mImageResId = a.getResourceId( + com.airbnb.lottie.R.styleable.LottieAnimationView_lottie_rawRes, + /* defValue= */ 0); mCacheComposition = a.getBoolean( - com.airbnb.lottie.R.styleable.LottieAnimationView_lottie_cacheComposition, true); + com.airbnb.lottie.R.styleable.LottieAnimationView_lottie_cacheComposition, + /* defValue= */ true); a = context.obtainStyledAttributes(attrs, - R.styleable.IllustrationPreference, 0 /*defStyleAttr*/, 0 /*defStyleRes*/); + R.styleable.IllustrationPreference, /* defStyleAttr= */ 0, + /* defStyleRes= */ 0); mLottieDynamicColor = a.getBoolean(R.styleable.IllustrationPreference_dynamicColor, - false); + /* defValue= */ false); a.recycle(); } diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/themes.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/themes.xml index dc5c9b297181..b6e80c784f10 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v35/themes.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v35/themes.xml @@ -18,7 +18,7 @@ <resources> <style name="Theme.SettingsBase_v35" parent="Theme.SettingsBase_v33" > <item name="android:colorAccent">@color/settingslib_materialColorPrimary</item> - <item name="android:colorBackground">@color/settingslib_materialColorSurfaceContainerLowest</item> + <item name="android:colorBackground">@color/settingslib_materialColorSurfaceContainer</item> <item name="android:textColorPrimary">@color/settingslib_materialColorOnSurface</item> <item name="android:textColorSecondary">@color/settingslib_text_color_secondary</item> <item name="android:textColorTertiary">@color/settingslib_materialColorOutline</item> diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt index 6a0632073de9..a04fce7eeb86 100644 --- a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt +++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt @@ -47,7 +47,7 @@ open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) : private val mHandler = Handler(Looper.getMainLooper()) - private val syncRunnable = Runnable { updatePreferences() } + private val syncRunnable = Runnable { updatePreferencesList() } init { val context = preferenceGroup.context @@ -64,7 +64,7 @@ open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) : true, /* resolveRefs */ ) mLegacyBackgroundRes = outValue.resourceId - updatePreferences() + updatePreferencesList() } @SuppressLint("RestrictedApi") @@ -82,7 +82,7 @@ open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) : updateBackground(holder, position) } - private fun updatePreferences() { + private fun updatePreferencesList() { val oldList = ArrayList(mRoundCornerMappingList) mRoundCornerMappingList = ArrayList() mappingPreferenceGroup(mRoundCornerMappingList, mPreferenceGroup) diff --git a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml index 6015be8c894f..aa5e9d28cabe 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml @@ -23,10 +23,10 @@ <clip-path android:pathData=" M0,0 - V13.5,0 - H13.5,20 - V0,20 - H0,0 + H16 + V12 + H0 + Z M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" /> <path android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" diff --git a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml index fb73b6b253e1..f7bf8518a55c 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml @@ -8,10 +8,10 @@ <clip-path android:pathData=" M0,0 - V13.5,0 - H13.5,20 - V0,20 - H0,0 + H16 + V12 + H0 + Z M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" /> <path android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" diff --git a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml index 7c4c1c6b1126..016c614846e9 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml @@ -23,10 +23,10 @@ <clip-path android:pathData=" M0,0 - V13.5,0 - H13.5,20 - V0,20 - H0,0 + H16 + V12 + H0 + Z M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" /> <path android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" diff --git a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml index d23680d17b9c..b8304151eefe 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml @@ -23,10 +23,10 @@ <clip-path android:pathData=" M0,0 - V13.5,0 - H13.5,20 - V0,20 - H0,0 + H16 + V12 + H0 + Z M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" /> <path android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index 3017d79836e8..367e38ed779d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -63,6 +63,7 @@ import com.google.common.collect.ImmutableList; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -70,7 +71,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -108,6 +108,10 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { private static final String SYSUI_PKG = "com.android.systemui"; private static final String TAG = "LocalBluetoothLeBroadcast"; private static final boolean DEBUG = BluetoothUtils.D; + private static final String VALID_PASSWORD_CHARACTERS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:," + + ".<>?/"; + private static final int PASSWORD_LENGTH = 16; static final String NAME = "LE_AUDIO_BROADCAST"; private static final String UNDERLINE = "_"; @@ -1088,11 +1092,16 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { mBroadcastId = UNKNOWN_VALUE_PLACEHOLDER; } - private String generateRandomPassword() { - String randomUUID = UUID.randomUUID().toString(); - // first 16 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx - return randomUUID.substring(0, 8) + randomUUID.substring(9, 13) + randomUUID.substring(14, - 18); + private static String generateRandomPassword() { + SecureRandom random = new SecureRandom(); + StringBuilder stringBuilder = new StringBuilder(PASSWORD_LENGTH); + + for (int i = 0; i < PASSWORD_LENGTH; i++) { + int randomIndex = random.nextInt(VALID_PASSWORD_CHARACTERS.length()); + stringBuilder.append(VALID_PASSWORD_CHARACTERS.charAt(randomIndex)); + } + + return stringBuilder.toString(); } private void registerContentObserver() { diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 8e3aa65fa5c7..bc281eea39d8 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -1503,7 +1503,7 @@ public class SettingsProvider extends ContentProvider { if (DEBUG) { Slog.v(LOG_TAG, "insertGlobalSetting(" + name + ", " + value + ", " + ", " + tag + ", " + makeDefault + ", " + requestingUserId - + ", " + forceNotify + ")"); + + ", " + forceNotify + ", " + overrideableByRestore + ")"); } return mutateGlobalSetting(name, value, tag, makeDefault, requestingUserId, MUTATION_OPERATION_INSERT, forceNotify, 0, overrideableByRestore); @@ -1785,7 +1785,7 @@ public class SettingsProvider extends ContentProvider { if (DEBUG) { Slog.v(LOG_TAG, "insertSecureSetting(" + name + ", " + value + ", " + ", " + tag + ", " + makeDefault + ", " + requestingUserId - + ", " + forceNotify + ")"); + + ", " + forceNotify + ", " + overrideableByRestore + ")"); } return mutateSecureSetting(name, value, tag, makeDefault, requestingUserId, MUTATION_OPERATION_INSERT, forceNotify, 0, overrideableByRestore); @@ -1946,7 +1946,7 @@ public class SettingsProvider extends ContentProvider { boolean overrideableByRestore) { if (DEBUG) { Slog.v(LOG_TAG, "insertSystemSetting(" + name + ", " + value + ", " - + requestingUserId + ")"); + + requestingUserId + ", " + overrideableByRestore + ")"); } return mutateSystemSetting(name, value, /* tag= */ null, requestingUserId, diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java index 17ebf6fc3235..0484defeb4d7 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java @@ -30,6 +30,7 @@ import android.os.ShellCommand; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.util.Slog; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -94,6 +95,8 @@ final public class SettingsService extends Binder { } final static class MyShellCommand extends ShellCommand { + private static final String LOG_TAG = "SettingsShellCmd"; + final SettingsProvider mProvider; final boolean mDumping; @@ -115,6 +118,7 @@ final public class SettingsService extends Binder { String mTag = null; int mResetMode = -1; boolean mMakeDefault; + boolean mOverrideableByRestore; MyShellCommand(SettingsProvider provider, boolean dumping) { mProvider = provider; @@ -209,6 +213,7 @@ final public class SettingsService extends Binder { return -1; } break; + // At this point, mVerb == PUT } else if (mKey == null) { mKey = arg; // keep going; there's another PUT arg @@ -217,36 +222,8 @@ final public class SettingsService extends Binder { // what we have so far is a valid command valid = true; // keep going; there may be another PUT arg - } else if (mTag == null) { - mTag = arg; - if ("default".equalsIgnoreCase(mTag)) { - mTag = null; - mMakeDefault = true; - if (peekNextArg() == null) { - valid = true; - } else { - perr.println("Too many arguments"); - return -1; - } - break; - } - if (peekNextArg() == null) { - valid = true; - break; - } - } else { // PUT, final arg - if (!"default".equalsIgnoreCase(arg)) { - perr.println("Argument expected to be 'default'"); - return -1; - } - mMakeDefault = true; - if (peekNextArg() == null) { - valid = true; - } else { - perr.println("Too many arguments"); - return -1; - } - break; + } else { + valid = parseOptionalPutArgument(arg); } } while ((arg = getNextArg()) != null); @@ -275,7 +252,8 @@ final public class SettingsService extends Binder { pout.println(getForUser(iprovider, mUser, mTable, mKey)); break; case PUT: - putForUser(iprovider, mUser, mTable, mKey, mValue, mTag, mMakeDefault); + putForUser(iprovider, mUser, mTable, mKey, mValue, mTag, mMakeDefault, + mOverrideableByRestore); break; case DELETE: pout.println("Deleted " @@ -297,6 +275,41 @@ final public class SettingsService extends Binder { return 0; } + private boolean parseOptionalPutArgument(String arg) { + boolean valid = true; + // Given that the order is TAG default overrideableByRestore, we need to parse from the + // opposite direction + switch (arg) { + case "overrideableByRestore": + if (mOverrideableByRestore) { + valid = false; + } else { + mOverrideableByRestore = true; + } + break; + case "default": + if (mMakeDefault || mOverrideableByRestore) { + valid = false; + } else { + mMakeDefault = true; + } + break; + default: // tag + if (mMakeDefault || mOverrideableByRestore || mTag != null) { + valid = false; + } else { + mTag = arg; + } + break; + } + if (!valid) { + Slog.e(LOG_TAG, "parseOptionalPutArgument(" + arg + "): invalid state (" + + "mTag=" + mTag + ", mMakeDefault=" + mMakeDefault + + ", mOverrideableByRestore=" + mOverrideableByRestore + ")"); + } + return valid; + } + List<String> listForUser(IContentProvider provider, int userHandle, String table) { final String callListCommand; if ("system".equals(table)) callListCommand = Settings.CALL_METHOD_LIST_SYSTEM; @@ -351,7 +364,11 @@ final public class SettingsService extends Binder { } void putForUser(IContentProvider provider, int userHandle, final String table, - final String key, final String value, String tag, boolean makeDefault) { + final String key, final String value, String tag, boolean makeDefault, + boolean overrideableByRestore) { + Slog.v(LOG_TAG, "putForUser(userId=" + userHandle + ", table=" + table + ", key=" + key + + ", value=" + value + ", tag=" + tag + ", makeDefault=" + makeDefault + + ", overrideableByRestore=" + overrideableByRestore + ")"); final String callPutCommand; if ("system".equals(table)) { callPutCommand = Settings.CALL_METHOD_PUT_SYSTEM; @@ -377,6 +394,9 @@ final public class SettingsService extends Binder { if (makeDefault) { arg.putBoolean(Settings.CALL_METHOD_MAKE_DEFAULT_KEY, true); } + if (overrideableByRestore) { + arg.putBoolean(Settings.CALL_METHOD_OVERRIDEABLE_BY_RESTORE_KEY, true); + } final AttributionSource attributionSource = new AttributionSource( Binder.getCallingUid(), resolveCallingPackage(), /*attributionTag*/ null); provider.call(attributionSource, Settings.AUTHORITY, @@ -474,10 +494,11 @@ final public class SettingsService extends Binder { pw.println(" Print this help text."); pw.println(" get [--user <USER_ID> | current] NAMESPACE KEY"); pw.println(" Retrieve the current value of KEY."); - pw.println(" put [--user <USER_ID> | current] NAMESPACE KEY VALUE [TAG] [default]"); + pw.println(" put [--user <USER_ID> | current] NAMESPACE KEY VALUE [TAG] [default] [overrideableByRestore]"); pw.println(" Change the contents of KEY to VALUE."); - pw.println(" TAG to associate with the setting."); + pw.println(" TAG to associate with the setting (cannot be default or overrideableByRestore)."); pw.println(" {default} to set as the default, case-insensitive only for global/secure namespace"); + pw.println(" {overrideableByRestore} to let the value be overridden by BackupManager on restore operations"); pw.println(" delete [--user <USER_ID> | current] NAMESPACE KEY"); pw.println(" Delete the entry for KEY."); pw.println(" reset [--user <USER_ID> | current] NAMESPACE {PACKAGE_NAME | RESET_MODE}"); diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 85617bad1a91..70c042cb8eba 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -811,7 +811,7 @@ public class SettingsBackupTest { Settings.Secure.V_TO_U_RESTORE_ALLOWLIST, Settings.Secure.V_TO_U_RESTORE_DENYLIST, Settings.Secure.REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI, - Settings.Secure.REDACT_OTP_NOTIFICATION_IMMEDIATELY); + Settings.Secure.OTP_NOTIFICATION_REDACTION_LOCK_TIME); @Test public void systemSettingsBackedUpOrDenied() { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt index 02de78bc84ce..db9035b1635b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt @@ -25,8 +25,8 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer @@ -43,10 +43,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope 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.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer @@ -67,6 +65,7 @@ import com.android.compose.animation.scene.ValueKey import com.android.compose.animation.scene.animateElementFloatAsState import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.modifiers.thenIf +import com.android.compose.theme.colorAttr import com.android.settingslib.Utils import com.android.systemui.battery.BatteryMeterView import com.android.systemui.battery.BatteryMeterViewController @@ -77,21 +76,18 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.chipBackground -import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.chipHighlighted import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.onScrimDim +import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.ChipPaddingHorizontal +import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.ChipPaddingVertical import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.CollapsedHeight import com.android.systemui.shade.ui.composable.ShadeHeader.Values.ClockScale import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder -import com.android.systemui.statusbar.phone.NotificationIconContainer import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.phone.StatusIconContainer import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel import com.android.systemui.statusbar.policy.Clock -import kotlinx.coroutines.launch object ShadeHeader { object Elements { @@ -110,6 +106,8 @@ object ShadeHeader { object Dimensions { val CollapsedHeight = 48.dp val ExpandedHeight = 120.dp + val ChipPaddingHorizontal = 6.dp + val ChipPaddingVertical = 4.dp } object Colors { @@ -118,12 +116,6 @@ object ShadeHeader { val ColorScheme.onScrimDim: Color get() = Color.DarkGray - - val ColorScheme.chipBackground: Color - get() = Color.DarkGray - - val ColorScheme.chipHighlighted: Color - get() = Color.LightGray } object TestTags { @@ -149,9 +141,6 @@ fun ContentScope.CollapsedShadeHeader( } } - val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle() - val shorterDateText by viewModel.shorterDateText.collectAsStateWithLifecycle() - val isShadeLayoutWide = viewModel.isShadeLayoutWide val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() @@ -167,9 +156,9 @@ fun ContentScope.CollapsedShadeHeader( ) { Clock(scale = 1f, onClick = viewModel::onClockClicked) VariableDayDate( - longerDateText = longerDateText, - shorterDateText = shorterDateText, - chipHighlight = viewModel.notificationsChipHighlight, + longerDateText = viewModel.longerDateText, + shorterDateText = viewModel.shorterDateText, + textColor = colorAttr(android.R.attr.textColorPrimary), modifier = Modifier.element(ShadeHeader.Elements.CollapsedContentStart), ) } @@ -229,8 +218,6 @@ fun ContentScope.ExpandedShadeHeader( derivedStateOf { shouldUseExpandedFormat(layoutState.transitionState) } } - val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle() - val shorterDateText by viewModel.shorterDateText.collectAsStateWithLifecycle() val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() Box(modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root)) { @@ -269,9 +256,9 @@ fun ContentScope.ExpandedShadeHeader( modifier = Modifier.element(ShadeHeader.Elements.ExpandedContent), ) { VariableDayDate( - longerDateText = longerDateText, - shorterDateText = shorterDateText, - chipHighlight = viewModel.notificationsChipHighlight, + longerDateText = viewModel.longerDateText, + shorterDateText = viewModel.shorterDateText, + textColor = colorAttr(android.R.attr.textColorPrimary), modifier = Modifier.widthIn(max = 90.dp), ) Spacer(modifier = Modifier.weight(1f)) @@ -316,6 +303,7 @@ fun ContentScope.OverlayShadeHeader( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = horizontalPadding), ) { + val chipHighlight = viewModel.notificationsChipHighlight if (isShadeLayoutWide) { Clock( scale = 1f, @@ -324,28 +312,15 @@ fun ContentScope.OverlayShadeHeader( ) Spacer(modifier = Modifier.width(5.dp)) } - val chipHighlight = viewModel.notificationsChipHighlight - NotificationIconChip( - chipHighlight = chipHighlight, + NotificationsChip( onClick = viewModel::onNotificationIconChipClicked, + backgroundColor = chipHighlight.backgroundColor(MaterialTheme.colorScheme), ) { - if (isShadeLayoutWide) { - NotificationIcons( - chipHighlight = chipHighlight, - notificationIconContainerStatusBarViewBinder = - viewModel.notificationIconContainerStatusBarViewBinder, - modifier = Modifier.width(IntrinsicSize.Min).height(20.dp), - ) - } else { - val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle() - val shorterDateText by - viewModel.shorterDateText.collectAsStateWithLifecycle() - VariableDayDate( - longerDateText = longerDateText, - shorterDateText = shorterDateText, - chipHighlight = viewModel.notificationsChipHighlight, - ) - } + VariableDayDate( + longerDateText = viewModel.longerDateText, + shorterDateText = viewModel.shorterDateText, + textColor = chipHighlight.foregroundColor(MaterialTheme.colorScheme), + ) } } }, @@ -357,14 +332,13 @@ fun ContentScope.OverlayShadeHeader( ) { val chipHighlight = viewModel.quickSettingsChipHighlight SystemIconChip( - chipHighlight = chipHighlight, + backgroundColor = chipHighlight.backgroundColor(MaterialTheme.colorScheme), onClick = viewModel::onSystemIconChipClicked, ) { StatusIcons( viewModel = viewModel, useExpandedFormat = false, modifier = Modifier.padding(end = 6.dp).weight(1f, fill = false), - chipHighlight = chipHighlight, ) BatteryIcon( createBatteryMeterViewController = @@ -534,6 +508,7 @@ private fun BatteryIcon( batteryIcon.setPercentShowMode( if (useExpandedFormat) BatteryMeterView.MODE_ESTIMATE else BatteryMeterView.MODE_ON ) + // TODO(b/397223606): Get the actual spec for this. if (chipHighlight is HeaderChipHighlight.Strong) { batteryIcon.updateColors(primaryColor, inverseColor, inverseColor) } else if (chipHighlight is HeaderChipHighlight.Weak) { @@ -546,11 +521,8 @@ private fun BatteryIcon( @Composable private fun ShadeCarrierGroup(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) { - Row(modifier = modifier) { - val subIds by viewModel.mobileSubIds.collectAsStateWithLifecycle() - - for (subId in subIds) { - Spacer(modifier = Modifier.width(5.dp)) + Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(5.dp)) { + for (subId in viewModel.mobileSubIds) { AndroidView( factory = { context -> ModernShadeCarrierGroupMobileView.constructAndBind( @@ -571,36 +543,10 @@ private fun ShadeCarrierGroup(viewModel: ShadeHeaderViewModel, modifier: Modifie } @Composable -private fun NotificationIcons( - chipHighlight: HeaderChipHighlight, - notificationIconContainerStatusBarViewBinder: NotificationIconContainerStatusBarViewBinder, - modifier: Modifier = Modifier, -) { - val scope = rememberCoroutineScope() - - AndroidView( - factory = { context -> - NotificationIconContainer(context, null).also { view -> - view.setOverrideIconColor(true) - scope.launch { - notificationIconContainerStatusBarViewBinder.bindWhileAttached( - view = view, - displayId = context.displayId, - ) - } - } - }, - update = { it.setUseInverseOverrideIconColor(chipHighlight is HeaderChipHighlight.Strong) }, - modifier = modifier, - ) -} - -@Composable private fun ContentScope.StatusIcons( viewModel: ShadeHeaderViewModel, useExpandedFormat: Boolean, modifier: Modifier = Modifier, - chipHighlight: HeaderChipHighlight = HeaderChipHighlight.None, ) { val localContext = LocalContext.current val themedContext = @@ -628,6 +574,8 @@ private fun ContentScope.StatusIcons( viewModel.createTintedIconManager(iconContainer, StatusBarLocation.QS) } + val chipHighlight = viewModel.quickSettingsChipHighlight + AndroidView( factory = { context -> iconManager.setTint(primaryColor, inverseColor) @@ -664,6 +612,7 @@ private fun ContentScope.StatusIcons( iconContainer.removeIgnoredSlot(locationSlot) } + // TODO(b/397223606): Get the actual spec for this. if (chipHighlight is HeaderChipHighlight.Strong) { iconManager.setTint(inverseColor, primaryColor) } else if (chipHighlight is HeaderChipHighlight.Weak) { @@ -675,62 +624,49 @@ private fun ContentScope.StatusIcons( } @Composable -private fun NotificationIconChip( - chipHighlight: HeaderChipHighlight, +private fun NotificationsChip( onClick: () -> Unit, modifier: Modifier = Modifier, - content: @Composable RowScope.() -> Unit, + backgroundColor: Color = Color.Unspecified, + content: @Composable BoxScope.() -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } - Box(modifier = modifier) { - Row( - modifier = - Modifier.align(Alignment.CenterStart) - .clickable( - interactionSource = interactionSource, - indication = null, - onClick = { onClick() }, - ) - .clip(RoundedCornerShape(25.dp)) - .background( - if (chipHighlight is HeaderChipHighlight.Strong) - MaterialTheme.colorScheme.chipHighlighted - else MaterialTheme.colorScheme.chipBackground - ) - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { - content() - } + Box( + modifier = + modifier + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = onClick, + ) + .background(backgroundColor, RoundedCornerShape(25.dp)) + .padding(horizontal = ChipPaddingHorizontal, vertical = ChipPaddingVertical) + ) { + content() } } @Composable private fun SystemIconChip( modifier: Modifier = Modifier, - chipHighlight: HeaderChipHighlight = HeaderChipHighlight.None, + backgroundColor: Color = Color.Unspecified, onClick: (() -> Unit)? = null, content: @Composable RowScope.() -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() val hoverModifier = - Modifier.clip(RoundedCornerShape(CollapsedHeight / 4)) - .background(MaterialTheme.colorScheme.onScrimDim) - val backgroundColor = - if (chipHighlight is HeaderChipHighlight.Strong) MaterialTheme.colorScheme.chipHighlighted - else MaterialTheme.colorScheme.chipBackground + with(MaterialTheme.colorScheme) { + Modifier.background(onScrimDim, RoundedCornerShape(CollapsedHeight / 4)) + } Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier - .thenIf(chipHighlight !is HeaderChipHighlight.None) { - Modifier.graphicsLayer { - shape = RoundedCornerShape(25.dp) - clip = true - } - .background(backgroundColor) - .padding(horizontal = 8.dp, vertical = 4.dp) + .thenIf(backgroundColor != Color.Unspecified) { + Modifier.background(backgroundColor, RoundedCornerShape(25.dp)) + .padding(horizontal = ChipPaddingHorizontal, vertical = ChipPaddingVertical) } .thenIf(onClick != null) { Modifier.clickable( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt index 64aada52626b..8fbd0519cbdf 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt @@ -4,22 +4,16 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.Layout -import com.android.compose.theme.colorAttr -import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight @Composable fun VariableDayDate( longerDateText: String, shorterDateText: String, - chipHighlight: HeaderChipHighlight, + textColor: Color, modifier: Modifier = Modifier, ) { - val textColor = - if (chipHighlight is HeaderChipHighlight.Strong) - colorAttr(android.R.attr.textColorPrimaryInverse) - else colorAttr(android.R.attr.textColorPrimary) - Layout( contents = listOf( 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 8317aa39ef2b..54be4d81ea06 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 @@ -215,12 +215,7 @@ open class SimpleDigitalClockTextView( ) } - setInterpolatedViewBounds( - getInterpolatedTextBounds(), - widthMeasureSpec, - heightMeasureSpec, - force = true, - ) + setInterpolatedViewBounds(getInterpolatedTextBounds(), widthMeasureSpec, heightMeasureSpec) } override fun onDraw(canvas: Canvas) { @@ -388,7 +383,6 @@ open class SimpleDigitalClockTextView( interpBounds: Rect, widthMeasureSpec: Int = measuredWidthAndState, heightMeasureSpec: Int = measuredHeightAndState, - force: Boolean = false, ) { val heightMode = MeasureSpec.getMode(heightMeasureSpec) val widthMode = MeasureSpec.getMode(widthMeasureSpec) @@ -415,10 +409,7 @@ open class SimpleDigitalClockTextView( ) } - if (force || widthSpec != measuredWidthAndState || heightSpec != measuredHeightAndState) { - setMeasuredDimension(widthSpec, heightSpec) - parent?.requestLayout() - } + setMeasuredDimension(widthSpec, heightSpec) } private fun updateXTranslation(inPoint: Point, interpolatedTextBounds: Rect): Point { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index 50762edc1179..88c9e74551fd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -21,6 +21,7 @@ import android.content.res.Configuration import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricConstants import android.hardware.biometrics.BiometricManager +import android.hardware.biometrics.BiometricPrompt import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.PromptVerticalListContentView @@ -290,7 +291,7 @@ open class AuthContainerViewTest : SysuiTestCase() { verify(callback) .onDismissed( - eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED), + eq(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED), eq<ByteArray?>(null), /* credentialAttestation */ eq(authContainer?.requestId ?: 0L), ) @@ -310,7 +311,7 @@ open class AuthContainerViewTest : SysuiTestCase() { ) verify(callback) .onDismissed( - eq(AuthDialogCallback.DISMISSED_USER_CANCELED), + eq(BiometricPrompt.DISMISSED_REASON_USER_CANCEL), eq<ByteArray?>(null), /* credentialAttestation */ eq(authContainer?.requestId ?: 0L), ) @@ -325,7 +326,7 @@ open class AuthContainerViewTest : SysuiTestCase() { verify(callback) .onDismissed( - eq(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE), + eq(BiometricPrompt.DISMISSED_REASON_NEGATIVE), eq<ByteArray?>(null), /* credentialAttestation */ eq(authContainer?.requestId ?: 0L), ) @@ -352,7 +353,7 @@ open class AuthContainerViewTest : SysuiTestCase() { verify(callback) .onDismissed( - eq(AuthDialogCallback.DISMISSED_ERROR), + eq(BiometricPrompt.DISMISSED_REASON_ERROR), eq<ByteArray?>(null), /* credentialAttestation */ eq(authContainer?.requestId ?: 0L), ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java index acc97a9f8642..a1a2aa70d869 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -138,9 +138,9 @@ public class AuthControllerTest extends SysuiTestCase { @Mock private IBiometricContextListener mContextListener; @Mock - private AuthDialog mDialog1; + private AuthContainerView mDialog1; @Mock - private AuthDialog mDialog2; + private AuthContainerView mDialog2; @Mock private CommandQueue mCommandQueue; @Mock @@ -382,7 +382,7 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testSendsReasonUserCanceled_whenDismissedByUserCancel() throws Exception { showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null, /* credentialAttestation */ mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( @@ -393,7 +393,7 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testSendsReasonNegative_whenDismissedByButtonNegative() throws Exception { showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE, null, /* credentialAttestation */ mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( @@ -404,7 +404,7 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testSendsReasonConfirmed_whenDismissedByButtonPositive() throws Exception { showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_POSITIVE, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED, null, /* credentialAttestation */ mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( @@ -415,7 +415,7 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testSendsReasonConfirmNotRequired_whenDismissedByAuthenticated() throws Exception { showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED, null, /* credentialAttestation */ mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( @@ -426,7 +426,7 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testSendsReasonError_whenDismissedByError() throws Exception { showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_ERROR, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_ERROR, null, /* credentialAttestation */ mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( @@ -437,7 +437,7 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testSendsReasonServerRequested_whenDismissedByServer() throws Exception { showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED, null, /* credentialAttestation */ mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( @@ -452,7 +452,7 @@ public class AuthControllerTest extends SysuiTestCase { final byte[] credentialAttestation = generateRandomHAT(); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED, credentialAttestation, mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( eq(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED), @@ -462,7 +462,7 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testSendsReasonContentViewMoreOptions_whenButtonPressed() throws Exception { showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS, null, /* credentialAttestation */ mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( @@ -696,7 +696,7 @@ public class AuthControllerTest extends SysuiTestCase { final byte[] credentialAttestation = generateRandomHAT(); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED, credentialAttestation, mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( eq(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED), @@ -755,7 +755,7 @@ public class AuthControllerTest extends SysuiTestCase { public void testDoesNotCrash_whenTryAgainPressedAfterDismissal() { showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); final long requestID = mAuthController.mCurrentDialog.getRequestId(); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null, /* credentialAttestation */requestID); mAuthController.onTryAgainPressed(requestID); } @@ -764,7 +764,7 @@ public class AuthControllerTest extends SysuiTestCase { public void testDoesNotCrash_whenDeviceCredentialPressedAfterDismissal() { showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); final long requestID = mAuthController.mCurrentDialog.getRequestId(); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */, requestID); mAuthController.onDeviceCredentialPressed(requestID); } @@ -818,7 +818,7 @@ public class AuthControllerTest extends SysuiTestCase { // WHEN dialog is shown and then dismissed showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */, mAuthController.mCurrentDialog.getRequestId()); @@ -1218,14 +1218,14 @@ public class AuthControllerTest extends SysuiTestCase { } @Override - protected AuthDialog buildDialog(DelayableExecutor bgExecutor, PromptInfo promptInfo, + protected AuthContainerView buildDialog(DelayableExecutor bgExecutor, PromptInfo promptInfo, boolean requireConfirmation, int userId, int[] sensorIds, String opPackageName, boolean skipIntro, long operationId, long requestId, WakefulnessLifecycle wakefulnessLifecycle, UserManager userManager, LockPatternUtils lockPatternUtils, PromptViewModel viewModel) { - AuthDialog dialog; + AuthContainerView dialog; if (mBuildCount == 0) { dialog = mDialog1; } else if (mBuildCount == 1) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt index 98486a22854a..af6c65ec6d6d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt @@ -538,7 +538,6 @@ object TestShortcuts { simpleShortcutCategory(System, "System apps", "Open settings"), simpleShortcutCategory(System, "System controls", "Lock screen"), simpleShortcutCategory(System, "System controls", "View notifications"), - simpleShortcutCategory(System, "System apps", "Take a note"), simpleShortcutCategory(System, "System controls", "Take screenshot"), simpleShortcutCategory(System, "System controls", "Go back"), simpleShortcutCategory(MultiTasking, "Split screen", "Use full screen"), @@ -570,7 +569,6 @@ object TestShortcuts { simpleInputGestureData( keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL ), - simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES), simpleInputGestureData( keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT ), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt new file mode 100644 index 000000000000..052dfd52887f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt @@ -0,0 +1,54 @@ +/* + * 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.keyguard.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.kosmos.collectValues +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DozingToDreamingTransitionViewModelTest : SysuiTestCase() { + val kosmos = testKosmos() + + val underTest by lazy { kosmos.dozingToDreamingTransitionViewModel } + + @Test + fun notificationShadeAlpha() = + kosmos.runTest { + val values by collectValues(underTest.notificationAlpha) + assertThat(values).isEmpty() + + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.DOZING, + to = KeyguardState.DREAMING, + testScope, + ) + + assertThat(values).isNotEmpty() + values.forEach { assertThat(it).isEqualTo(0) } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt index 4e14fec8408f..943ada9346e7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt @@ -18,6 +18,9 @@ package com.android.systemui.media.controls.ui.binder import android.animation.Animator import android.animation.ObjectAnimator +import android.icu.text.MeasureFormat +import android.icu.util.Measure +import android.icu.util.MeasureUnit import android.testing.TestableLooper import android.view.View import android.widget.SeekBar @@ -30,6 +33,7 @@ import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel import com.android.systemui.res.R import com.google.common.truth.Truth.assertThat +import java.util.Locale import org.junit.Before import org.junit.Rule import org.junit.Test @@ -61,11 +65,11 @@ class SeekBarObserverTest : SysuiTestCase() { fun setUp() { context.orCreateTestableResources.addOverride( R.dimen.qs_media_enabled_seekbar_height, - enabledHeight + enabledHeight, ) context.orCreateTestableResources.addOverride( R.dimen.qs_media_disabled_seekbar_height, - disabledHeight + disabledHeight, ) seekBarView = SeekBar(context) @@ -110,14 +114,31 @@ class SeekBarObserverTest : SysuiTestCase() { @Test fun seekBarProgress() { + val elapsedTime = 3000 + val duration = (1.5 * 60 * 60 * 1000).toInt() // WHEN part of the track has been played - val data = SeekBarViewModel.Progress(true, true, true, false, 3000, 120000, true) + val data = SeekBarViewModel.Progress(true, true, true, false, elapsedTime, duration, true) observer.onChanged(data) // THEN seek bar shows the progress - assertThat(seekBarView.progress).isEqualTo(3000) - assertThat(seekBarView.max).isEqualTo(120000) - - val desc = context.getString(R.string.controls_media_seekbar_description, "00:03", "02:00") + assertThat(seekBarView.progress).isEqualTo(elapsedTime) + assertThat(seekBarView.max).isEqualTo(duration) + + val expectedProgress = + MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) + .formatMeasures(Measure(3, MeasureUnit.SECOND)) + val expectedDuration = + MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) + .formatMeasures( + Measure(1, MeasureUnit.HOUR), + Measure(30, MeasureUnit.MINUTE), + Measure(0, MeasureUnit.SECOND), + ) + val desc = + context.getString( + R.string.controls_media_seekbar_description, + expectedProgress, + expectedDuration, + ) assertThat(seekBarView.contentDescription).isEqualTo(desc) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorTest.kt index 84fc93008f49..a3dd67f85150 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorTest.kt @@ -22,6 +22,9 @@ import android.provider.AlarmClock import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.activityStarter import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback @@ -31,22 +34,32 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argThat import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat +import java.util.Date +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatcher -import org.mockito.Mockito.times import org.mockito.Mockito.verify +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class ShadeHeaderClockInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() private val testScope = kosmos.testScope private val activityStarter = kosmos.activityStarter private val nextAlarmController = kosmos.nextAlarmController - val underTest = kosmos.shadeHeaderClockInteractor + private val underTest = kosmos.shadeHeaderClockInteractor @Test fun launchClockActivity_default() = @@ -55,7 +68,7 @@ class ShadeHeaderClockInteractorTest : SysuiTestCase() { verify(activityStarter) .postStartActivityDismissingKeyguard( argThat(IntentMatcherAction(AlarmClock.ACTION_SHOW_ALARMS)), - any() + any(), ) } @@ -71,6 +84,75 @@ class ShadeHeaderClockInteractorTest : SysuiTestCase() { underTest.launchClockActivity() verify(activityStarter).postStartActivityDismissingKeyguard(any()) } + + @Test + fun onTimezoneOrLocaleChanged_localeAndTimezoneChanged_emitsForEach() = + testScope.runTest { + val timeZoneOrLocaleChanges by collectValues(underTest.onTimezoneOrLocaleChanged) + + sendIntentActionBroadcast(Intent.ACTION_TIMEZONE_CHANGED) + sendIntentActionBroadcast(Intent.ACTION_LOCALE_CHANGED) + sendIntentActionBroadcast(Intent.ACTION_LOCALE_CHANGED) + sendIntentActionBroadcast(Intent.ACTION_TIMEZONE_CHANGED) + + assertThat(timeZoneOrLocaleChanges).hasSize(4) + } + + @Test + fun onTimezoneOrLocaleChanged_timeChanged_doesNotEmit() = + testScope.runTest { + val timeZoneOrLocaleChanges by collectValues(underTest.onTimezoneOrLocaleChanged) + assertThat(timeZoneOrLocaleChanges).hasSize(1) + + sendIntentActionBroadcast(Intent.ACTION_TIME_CHANGED) + sendIntentActionBroadcast(Intent.ACTION_TIME_TICK) + + // Expect only 1 event to have been emitted onStart, but no more. + assertThat(timeZoneOrLocaleChanges).hasSize(1) + } + + @Test + fun currentTime_timeChanged() = + testScope.runTest { + val currentTime by collectLastValue(underTest.currentTime) + + sendIntentActionBroadcast(Intent.ACTION_TIME_CHANGED) + val earlierTime = checkNotNull(currentTime) + + advanceTimeBy(3.seconds) + runCurrent() + + sendIntentActionBroadcast(Intent.ACTION_TIME_CHANGED) + val laterTime = checkNotNull(currentTime) + + assertThat(differenceBetween(laterTime, earlierTime)).isEqualTo(3.seconds) + } + + @Test + fun currentTime_timeTicked() = + testScope.runTest { + val currentTime by collectLastValue(underTest.currentTime) + + sendIntentActionBroadcast(Intent.ACTION_TIME_TICK) + val earlierTime = checkNotNull(currentTime) + + advanceTimeBy(7.seconds) + runCurrent() + + sendIntentActionBroadcast(Intent.ACTION_TIME_TICK) + val laterTime = checkNotNull(currentTime) + + assertThat(differenceBetween(laterTime, earlierTime)).isEqualTo(7.seconds) + } + + private fun differenceBetween(date1: Date, date2: Date): Duration { + return (date1.time - date2.time).milliseconds + } + + private fun TestScope.sendIntentActionBroadcast(intentAction: String) { + kosmos.broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, Intent(intentAction)) + runCurrent() + } } private class IntentMatcherAction(private val action: String) : ArgumentMatcher<Intent> { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt index 061e04ef29f7..37b4688f753d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt @@ -25,12 +25,15 @@ import com.android.systemui.shade.domain.interactor.disableDualShade import com.android.systemui.shade.domain.interactor.enableDualShade import com.android.systemui.shade.domain.interactor.enableSingleShade import com.android.systemui.shade.domain.interactor.enableSplitShade +import com.android.systemui.shade.domain.interactor.shadeMode +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor import com.android.systemui.testKosmos import com.android.systemui.util.mockito.argThat 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.runCurrent @@ -43,6 +46,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @EnableSceneContainer @@ -64,14 +68,15 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun mobileSubIds_update() = testScope.runTest { - val mobileSubIds by collectLastValue(underTest.mobileSubIds) mobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1) + runCurrent() - assertThat(mobileSubIds).isEqualTo(listOf(1)) + assertThat(underTest.mobileSubIds).isEqualTo(listOf(1)) mobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + runCurrent() - assertThat(mobileSubIds).isEqualTo(listOf(1, 2)) + assertThat(underTest.mobileSubIds).isEqualTo(listOf(1, 2)) } @Test @@ -116,13 +121,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onSystemIconChipClicked_lockedOnQsShade_collapsesShadeToLockscreen() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(false) - setScene(Scenes.Lockscreen) - setOverlay(Overlays.QuickSettingsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onSystemIconChipClicked() runCurrent() @@ -134,13 +135,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onSystemIconChipClicked_lockedOnNotifShade_expandsQsShade() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(false) - setScene(Scenes.Lockscreen) - setOverlay(Overlays.NotificationsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onSystemIconChipClicked() runCurrent() @@ -166,13 +163,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onSystemIconChipClicked_unlockedOnQsShade_collapsesShadeToGone() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.QuickSettingsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(true) - setScene(Scenes.Gone) - setOverlay(Overlays.QuickSettingsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onSystemIconChipClicked() runCurrent() @@ -184,13 +177,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onSystemIconChipClicked_unlockedOnNotifShade_expandsQsShade() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.NotificationsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(true) - setScene(Scenes.Gone) - setOverlay(Overlays.NotificationsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onSystemIconChipClicked() runCurrent() @@ -203,13 +192,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onNotificationIconChipClicked_lockedOnNotifShade_collapsesShadeToLockscreen() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(false) - setScene(Scenes.Lockscreen) - setOverlay(Overlays.NotificationsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onNotificationIconChipClicked() runCurrent() @@ -221,13 +206,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onNotificationIconChipClicked_lockedOnQsShade_expandsNotifShade() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(false) - setScene(Scenes.Lockscreen) - setOverlay(Overlays.QuickSettingsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onNotificationIconChipClicked() runCurrent() @@ -240,13 +221,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onNotificationIconChipClicked_unlockedOnNotifShade_collapsesShadeToGone() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.NotificationsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(true) - setScene(Scenes.Gone) - setOverlay(Overlays.NotificationsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onNotificationIconChipClicked() runCurrent() @@ -258,13 +235,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onNotificationIconChipClicked_unlockedOnQsShade_expandsNotifShade() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.QuickSettingsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(true) - setScene(Scenes.Gone) - setOverlay(Overlays.QuickSettingsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onNotificationIconChipClicked() runCurrent() @@ -319,22 +292,13 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun highlightChips_notifsOpenInDualShade_notifsStrongQuickSettingsWeak() = testScope.runTest { - kosmos.enableDualShade() - val currentScene by collectLastValue(sceneInteractor.currentScene) - val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - // Test the lockscreen scenario. - setScene(Scenes.Lockscreen) - setOverlay(Overlays.NotificationsShade) + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade) assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) // Test the unlocked scenario. - setDeviceEntered(true) - setScene(Scenes.Gone) - setOverlay(Overlays.NotificationsShade) - assertThat(currentScene).isEqualTo(Scenes.Gone) - assertThat(currentOverlays).isNotEmpty() + setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.NotificationsShade) assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) } @@ -342,22 +306,13 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun highlightChips_quickSettingsOpenInDualShade_notifsWeakQuickSettingsStrong() = testScope.runTest { - kosmos.enableDualShade() - val currentScene by collectLastValue(sceneInteractor.currentScene) - val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - // Test the lockscreen scenario. - setScene(Scenes.Lockscreen) - setOverlay(Overlays.QuickSettingsShade) + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade) assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) // Test the unlocked scenario. - setDeviceEntered(true) - setScene(Scenes.Gone) - setOverlay(Overlays.QuickSettingsShade) - assertThat(currentScene).isEqualTo(Scenes.Gone) - assertThat(currentOverlays).isNotEmpty() + setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.QuickSettingsShade) assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) } @@ -365,21 +320,13 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun highlightChips_noOverlaysInDualShade_bothNone() = testScope.runTest { - kosmos.enableDualShade() - val currentScene by collectLastValue(sceneInteractor.currentScene) - val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - // Test the lockscreen scenario. - setScene(Scenes.Lockscreen) - assertThat(currentOverlays).isEmpty() + setupDualShadeState(scene = Scenes.Lockscreen) assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None) assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None) // Test the unlocked scenario. - setDeviceEntered(true) - setScene(Scenes.Gone) - assertThat(currentScene).isEqualTo(Scenes.Gone) - assertThat(currentOverlays).isEmpty() + setupDualShadeState(scene = Scenes.Gone) assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None) assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None) } @@ -401,21 +348,43 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { ) } - private fun setScene(key: SceneKey) { - sceneInteractor.changeScene(key, "test") + private fun TestScope.setupDualShadeState(scene: SceneKey, overlay: OverlayKey? = null) { + kosmos.enableDualShade() + val shadeMode by collectLastValue(kosmos.shadeMode) + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + if (scene == Scenes.Gone) { + // Unlock the device, marking the device has been entered. + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + } + runCurrent() + assertThat(shadeMode).isEqualTo(ShadeMode.Dual) + + sceneInteractor.changeScene(scene, "test") + checkNotNull(currentOverlays).forEach { sceneInteractor.instantlyHideOverlay(it, "test") } + runCurrent() + overlay?.let { sceneInteractor.showOverlay(it, "test") } sceneInteractor.setTransitionState( - MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(scene, setOfNotNull(overlay)) + ) ) - testScope.runCurrent() + runCurrent() + + assertThat(currentScene).isEqualTo(scene) + if (overlay == null) { + assertThat(currentOverlays).isEmpty() + } else { + assertThat(currentOverlays).containsExactly(overlay) + } } - private fun setOverlay(key: OverlayKey) { - val currentOverlays = sceneInteractor.currentOverlays.value + key - sceneInteractor.showOverlay(key, "test") + private fun setScene(key: SceneKey) { + sceneInteractor.changeScene(key, "test") sceneInteractor.setTransitionState( - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(sceneInteractor.currentScene.value, currentOverlays) - ) + MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) ) testScope.runCurrent() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java index c6801f1ad9d5..3d8da6140ff7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -23,8 +23,8 @@ import static android.service.quickaccesswallet.Flags.FLAG_LAUNCH_WALLET_VIA_SYS import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -35,14 +35,12 @@ import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback; import android.os.Bundle; -import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.view.KeyEvent; import android.view.WindowInsets; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsController.Appearance; import android.view.WindowInsetsController.Behavior; -import android.view.accessibility.Flags; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -385,30 +383,7 @@ public class CommandQueueTest extends SysuiTestCase { } @Test - @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) - public void addQsTile_withA11yQsShortcutFlagOff() { - ComponentName c = new ComponentName("testpkg", "testcls"); - - mCommandQueue.addQsTile(c); - waitForIdleSync(); - - verify(mCallbacks).addQsTile(eq(c)); - } - - @Test - @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) - public void addQsTileToFrontOrEnd_withA11yQsShortcutFlagOff_doNothing() { - ComponentName c = new ComponentName("testpkg", "testcls"); - - mCommandQueue.addQsTileToFrontOrEnd(c, true); - waitForIdleSync(); - - verifyNoMoreInteractions(mCallbacks); - } - - @Test - @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) - public void addQsTile_withA11yQsShortcutFlagOn() { + public void addQsTile() { ComponentName c = new ComponentName("testpkg", "testcls"); mCommandQueue.addQsTile(c); @@ -418,8 +393,7 @@ public class CommandQueueTest extends SysuiTestCase { } @Test - @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) - public void addQsTileAtTheEnd_withA11yQsShortcutFlagOn() { + public void addQsTileAtTheEnd() { ComponentName c = new ComponentName("testpkg", "testcls"); mCommandQueue.addQsTileToFrontOrEnd(c, true); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt index 426af264da07..83e26c4220b1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.collection +import android.app.Notification +import android.graphics.Color import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import android.testing.TestableLooper.RunWithLooper @@ -72,4 +74,35 @@ class BundleEntryTest : SysuiTestCase() { fun getKey_adapter() { assertThat(underTest.entryAdapter.key).isEqualTo("key") } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isClearable_adapter() { + assertThat(underTest.entryAdapter.isClearable).isTrue() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getSummarization_adapter() { + assertThat(underTest.entryAdapter.summarization).isNull() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getContrastedColor_adapter() { + assertThat(underTest.entryAdapter.getContrastedColor(context, false, Color.WHITE)) + .isEqualTo(Notification.COLOR_DEFAULT) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun canPeek_adapter() { + assertThat(underTest.entryAdapter.canPeek()).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getWhen_adapter() { + assertThat(underTest.entryAdapter.`when`).isEqualTo(0) + } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java index 19d1224a9bf3..1f5c6722f38e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java @@ -163,9 +163,10 @@ public class NotificationEntryTest extends SysuiTestCase { @Test public void testIsExemptFromDndVisualSuppression_media() { + MediaSession session = new MediaSession(mContext, "test"); Notification.Builder n = new Notification.Builder(mContext, "") .setStyle(new Notification.MediaStyle() - .setMediaSession(mock(MediaSession.Token.class))) + .setMediaSession(session.getSessionToken())) .setSmallIcon(R.drawable.ic_person) .setContentTitle("Title") .setContentText("Text"); @@ -593,6 +594,76 @@ public class NotificationEntryTest extends SysuiTestCase { assertThat(entry.getEntryAdapter().getGroupRoot()).isEqualTo(parent.getEntryAdapter()); } + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void isClearable_adapter() { + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .build(); + entry.setRow(row); + + assertThat(entry.getEntryAdapter().isClearable()).isEqualTo(entry.isClearable()); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getSummarization_adapter() { + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .build(); + Ranking ranking = new RankingBuilder(entry.getRanking()) + .setSummarization("hello") + .build(); + entry.setRanking(ranking); + entry.setRow(row); + + assertThat(entry.getEntryAdapter().getSummarization()).isEqualTo("hello"); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getIcons_adapter() { + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .build(); + entry.setRow(row); + + assertThat(entry.getEntryAdapter().getIcons()).isEqualTo(entry.getIcons()); + } + private Notification.Action createContextualAction(String title) { return new Notification.Action.Builder( Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt index 7781df1ad91f..43cb9575b609 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt @@ -16,8 +16,11 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.app.Notification +import android.app.Notification.MediaStyle +import android.media.session.MediaSession import android.platform.test.flag.junit.FlagsParameterization import android.provider.Settings +import android.service.notification.StatusBarNotification import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.dumpManager @@ -36,6 +39,7 @@ import com.android.systemui.scene.data.repository.Idle import com.android.systemui.scene.data.repository.setTransition import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.SbnBuilder import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder @@ -217,6 +221,16 @@ class OriginalUnseenKeyguardCoordinatorTest(flags: FlagsParameterization) : Sysu mock<ExpandableNotificationRow>().apply { whenever(isMediaRow).thenReturn(true) } + sbn = SbnBuilder().setNotification( + Notification.Builder(context, "channel").setStyle( + MediaStyle().setMediaSession( + MediaSession( + context, + "tag" + ).sessionToken + ) + ).build() + ).build() } collectionListener.onEntryAdded(fakeEntry) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt index 4c1f4f17e00c..1b8d64d5483c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt @@ -80,9 +80,6 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.testKosmos import com.android.systemui.util.kotlin.JavaAdapter import com.android.systemui.wmshell.BubblesManager -import java.util.Optional -import kotlin.test.assertNotNull -import kotlin.test.fail import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -110,6 +107,9 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +import java.util.Optional +import kotlin.test.assertNotNull +import kotlin.test.fail /** Tests for [NotificationGutsManager]. */ @SmallTest @@ -509,7 +509,6 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( .setImportance(NotificationManager.IMPORTANCE_HIGH) .build() - whenever(row.isNonblockable).thenReturn(false) whenever(highPriorityProvider.isHighPriority(entry)).thenReturn(true) val statusBarNotification = entry.sbn gutsManager.initializeNotificationInfo(row, notificationInfoView) @@ -546,7 +545,6 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( NotificationEntryHelper.modifyRanking(row.entry) .setUserSentiment(NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE) .build() - whenever(row.isNonblockable).thenReturn(false) val statusBarNotification = row.entry.sbn val entry = row.entry @@ -586,7 +584,6 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( NotificationEntryHelper.modifyRanking(row.entry) .setUserSentiment(NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE) .build() - whenever(row.isNonblockable).thenReturn(false) val statusBarNotification = row.entry.sbn val entry = row.entry @@ -641,7 +638,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( ): NotificationMenuRowPlugin.MenuItem { val menuRow: NotificationMenuRowPlugin = NotificationMenuRow(mContext, peopleNotificationIdentifier) - menuRow.createMenu(row, row.entry.sbn) + menuRow.createMenu(row) val menuItem = menuRow.getLongpressMenuItem(mContext) assertNotNull(menuItem) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java index 027e899e20df..9fdfca14a5b2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java @@ -72,7 +72,7 @@ public class NotificationMenuRowTest extends LeakCheckedTest { public void testAttachDetach() { NotificationMenuRowPlugin row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); - row.createMenu(mRow, null); + row.createMenu(mRow); ViewUtils.attachView(row.getMenuView()); TestableLooper.get(this).processAllMessages(); ViewUtils.detachView(row.getMenuView()); @@ -83,9 +83,9 @@ public class NotificationMenuRowTest extends LeakCheckedTest { public void testRecreateMenu() { NotificationMenuRowPlugin row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); - row.createMenu(mRow, null); + row.createMenu(mRow); assertTrue(row.getMenuView() != null); - row.createMenu(mRow, null); + row.createMenu(mRow); assertTrue(row.getMenuView() != null); } @@ -103,7 +103,7 @@ public class NotificationMenuRowTest extends LeakCheckedTest { Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0); NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); - row.createMenu(mRow, null); + row.createMenu(mRow); ViewGroup container = (ViewGroup) row.getMenuView(); // noti blocking @@ -116,7 +116,7 @@ public class NotificationMenuRowTest extends LeakCheckedTest { Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0); NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); - row.createMenu(mRow, null); + row.createMenu(mRow); ViewGroup container = (ViewGroup) row.getMenuView(); // just for noti blocking @@ -129,7 +129,7 @@ public class NotificationMenuRowTest extends LeakCheckedTest { Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0); NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); - row.createMenu(mRow, null); + row.createMenu(mRow); ViewGroup container = (ViewGroup) row.getMenuView(); // one for snooze and one for noti blocking @@ -142,7 +142,7 @@ public class NotificationMenuRowTest extends LeakCheckedTest { Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 1); NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); - row.createMenu(mRow, null); + row.createMenu(mRow); ViewGroup container = (ViewGroup) row.getMenuView(); // Clear menu @@ -417,7 +417,7 @@ public class NotificationMenuRowTest extends LeakCheckedTest { public void testOnTouchMove() { NotificationMenuRow row = Mockito.spy( new NotificationMenuRow(mContext, mPeopleNotificationIdentifier)); - row.createMenu(mRow, null); + row.createMenu(mRow); doReturn(50f).when(row).getDismissThreshold(); doReturn(true).when(row).canBeDismissed(); doReturn(mView).when(row).getMenuView(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt index e5cb0fbc9e4b..885e71e7a7fe 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.testKosmos import com.google.android.msdl.data.model.MSDLToken import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -49,7 +50,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { private val sectionsManager = mock<NotificationSectionsManager>() private val msdlPlayer = kosmos.fakeMSDLPlayer private var canRowBeDismissed = true - private var magneticAnimationsCancelled = false + private var magneticAnimationsCancelled = MutableList(childrenNumber) { false } private val underTest = kosmos.magneticNotificationRowManagerImpl @@ -64,8 +65,10 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { NotificationTestHelper(mContext, mDependency, kosmos.testableLooper, featureFlags) children = notificationTestHelper.createGroup(childrenNumber).childrenContainer swipedRow = children.attachedChildren[childrenNumber / 2] - configureMagneticRowListener(swipedRow) - magneticAnimationsCancelled = false + children.attachedChildren.forEachIndexed { index, row -> + row.magneticRowListener = row.magneticRowListener.asTestableListener(index) + } + magneticAnimationsCancelled.replaceAll { false } } @Test @@ -259,14 +262,14 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { underTest.onMagneticInteractionEnd(swipedRow, velocity = null) // THEN magnetic animations are cancelled - assertThat(magneticAnimationsCancelled).isTrue() + assertThat(magneticAnimationsCancelled[childrenNumber / 2]).isTrue() } @Test fun onMagneticInteractionEnd_forMagneticNeighbor_cancelsMagneticAnimations() = kosmos.testScope.runTest { - val neighborRow = children.attachedChildren[childrenNumber / 2 - 1] - configureMagneticRowListener(neighborRow) + val neighborIndex = childrenNumber / 2 - 1 + val neighborRow = children.attachedChildren[neighborIndex] // GIVEN that targets are set setTargets() @@ -275,9 +278,15 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { underTest.onMagneticInteractionEnd(neighborRow, null) // THEN magnetic animations are cancelled - assertThat(magneticAnimationsCancelled).isTrue() + assertThat(magneticAnimationsCancelled[neighborIndex]).isTrue() } + @After + fun tearDown() { + // We reset the manager so that all MagneticRowListener can cancel all animations + underTest.reset() + } + private fun setDetachedState() { val threshold = 100f underTest.setSwipeThresholdPx(threshold) @@ -302,27 +311,33 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { originalTranslation * MagneticNotificationRowManagerImpl.MAGNETIC_REDUCTION - private fun configureMagneticRowListener(row: ExpandableNotificationRow) { - val listener = - object : MagneticRowListener { - override fun setMagneticTranslation(translation: Float) { - row.translation = translation - } + private fun MagneticRowListener.asTestableListener(rowIndex: Int): MagneticRowListener { + val delegate = this + return object : MagneticRowListener { + override fun setMagneticTranslation(translation: Float) { + delegate.setMagneticTranslation(translation) + } - override fun triggerMagneticForce( - endTranslation: Float, - springForce: SpringForce, - startVelocity: Float, - ) {} + override fun triggerMagneticForce( + endTranslation: Float, + springForce: SpringForce, + startVelocity: Float, + ) { + delegate.triggerMagneticForce(endTranslation, springForce, startVelocity) + } - override fun cancelMagneticAnimations() { - magneticAnimationsCancelled = true - } + override fun cancelMagneticAnimations() { + magneticAnimationsCancelled[rowIndex] = true + delegate.cancelMagneticAnimations() + } - override fun cancelTranslationAnimations() {} + override fun cancelTranslationAnimations() { + delegate.cancelTranslationAnimations() + } - override fun canRowBeDismissed(): Boolean = canRowBeDismissed + override fun canRowBeDismissed(): Boolean { + return canRowBeDismissed } - row.magneticRowListener = listener + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index 01ba4df3a314..7603eecd27ff 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -146,6 +146,20 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @EnableSceneContainer + fun resetViewStates_defaultHun_hasShadow() { + val headsUpTop = 200f + ambientState.headsUpTop = headsUpTop + + whenever(notificationRow.isPinned).thenReturn(true) + whenever(notificationRow.isHeadsUp).thenReturn(true) + + stackScrollAlgorithm.resetViewStates(ambientState, 0) + + assertThat(notificationRow.viewState.zTranslation).isGreaterThan(baseZ) + } + + @Test @DisableSceneContainer fun resetViewStates_defaultHunWhenShadeIsOpening_yTranslationIsInset() { whenever(notificationRow.isPinned).thenReturn(true) diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java index 94fdbae83253..9b961d2535ae 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java @@ -122,7 +122,7 @@ public interface NotificationMenuRowPlugin extends Plugin { public void setAppName(String appName); - public void createMenu(ViewGroup parent, StatusBarNotification sbn); + public void createMenu(ViewGroup parent); public void resetMenu(); @@ -215,9 +215,8 @@ public interface NotificationMenuRowPlugin extends Plugin { /** * Callback used to signal the menu that its parent notification has been updated. - * @param sbn */ - public void onNotificationUpdated(StatusBarNotification sbn); + public void onNotificationUpdated(); /** * Callback used to signal the menu that a user is moving the parent notification. diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 3fc46ed6c9d1..359bd2bcb37c 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3170,8 +3170,8 @@ <string name="controls_media_settings_button">Settings</string> <!-- Description for media control's playing media item, including information for the media's title, the artist, and source app [CHAR LIMIT=NONE]--> <string name="controls_media_playing_item_description"><xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> by <xliff:g id="artist_name" example="Various artists">%2$s</xliff:g> is playing from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string> - <!-- Content description for media cotnrols progress bar [CHAR_LIMIT=NONE] --> - <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1:30">%1$s</xliff:g> of <xliff:g id="total_time" example="3:00">%2$s</xliff:g></string> + <!-- Content description for media controls progress bar [CHAR_LIMIT=NONE] --> + <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1 hour 2 minutes 30 seconds">%1$s</xliff:g> of <xliff:g id="total_time" example="4 hours 5 seconds">%2$s</xliff:g></string> <!-- Placeholder title to inform user that an app has posted media controls [CHAR_LIMIT=NONE] --> <string name="controls_media_empty_title"><xliff:g id="app_name" example="Foo Music App">%1$s</xliff:g> is running</string> diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp index 0f1da509468a..ae3a76e2d2ca 100644 --- a/packages/SystemUI/shared/Android.bp +++ b/packages/SystemUI/shared/Android.bp @@ -71,6 +71,7 @@ android_library { "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", "//frameworks/libs/systemui:msdl", "//frameworks/libs/systemui:view_capture", + "am_flags_lib", ], resource_dirs: [ "res", diff --git a/packages/SystemUI/shared/res/values/bools.xml b/packages/SystemUI/shared/res/values/bools.xml index f22dac4b9fb4..98e5cea0e78f 100644 --- a/packages/SystemUI/shared/res/values/bools.xml +++ b/packages/SystemUI/shared/res/values/bools.xml @@ -22,4 +22,7 @@ <resources> <!-- Whether to add padding at the bottom of the complication clock --> <bool name="dream_overlay_complication_clock_bottom_padding">false</bool> -</resources>
\ No newline at end of file + + <!-- Whether to mark tasks that are present in the UI as perceptible tasks. --> + <bool name="config_usePerceptibleTasks">false</bool> +</resources> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java index ed9ba7aa455b..26d78bbee8e5 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java @@ -46,6 +46,8 @@ import android.view.Display; import android.window.TaskSnapshot; import com.android.internal.app.IVoiceInteractionManagerService; +import com.android.server.am.Flags; +import com.android.systemui.shared.R; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -227,6 +229,17 @@ public class ActivityManagerWrapper { } /** + * Sets whether or not the specified task is perceptible. + */ + public boolean setTaskIsPerceptible(int taskId, boolean isPerceptible) { + try { + return getService().setTaskIsPerceptible(taskId, isPerceptible); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Removes a task by id. */ public void removeTask(final int taskId) { @@ -311,6 +324,14 @@ public class ActivityManagerWrapper { } /** + * Returns true if tasks with a presence in the UI should be marked as perceptible tasks. + */ + public static boolean usePerceptibleTasks(Context context) { + return Flags.perceptibleTasks() + && context.getResources().getBoolean(R.bool.config_usePerceptibleTasks); + } + + /** * Returns true if the running task represents the home task */ public static boolean isHomeTask(RunningTaskInfo info) { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayout.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayout.java index 7c141c1b561e..5247acc94e03 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayout.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayout.java @@ -58,11 +58,22 @@ public class AmbientVolumeLayout extends LinearLayout implements AmbientVolumeUi private final BiMap<Integer, AmbientVolumeSlider> mSideToSliderMap = HashBiMap.create(); private int mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT; + private HearingDevicesUiEventLogger mUiEventLogger; + private int mLaunchSourceId; + private final AmbientVolumeSlider.OnChangeListener mSliderOnChangeListener = (slider, value) -> { - if (mListener != null) { - final int side = mSideToSliderMap.inverse().get(slider); - mListener.onSliderValueChange(side, value); + final Integer side = mSideToSliderMap.inverse().get(slider); + if (side != null) { + if (mUiEventLogger != null) { + HearingDevicesUiEvent uiEvent = side == SIDE_UNIFIED + ? HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_CHANGE_UNIFIED + : HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_CHANGE_SEPARATED; + mUiEventLogger.log(uiEvent, mLaunchSourceId); + } + if (mListener != null) { + mListener.onSliderValueChange(side, value); + } } }; @@ -94,6 +105,12 @@ public class AmbientVolumeLayout extends LinearLayout implements AmbientVolumeUi return; } setMuted(!mMuted); + if (mUiEventLogger != null) { + HearingDevicesUiEvent uiEvent = mMuted + ? HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_MUTE + : HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_UNMUTE; + mUiEventLogger.log(uiEvent, mLaunchSourceId); + } if (mListener != null) { mListener.onAmbientVolumeIconClick(); } @@ -103,6 +120,12 @@ public class AmbientVolumeLayout extends LinearLayout implements AmbientVolumeUi mExpandIcon = requireViewById(R.id.ambient_expand_icon); mExpandIcon.setOnClickListener(v -> { setExpanded(!mExpanded); + if (mUiEventLogger != null) { + HearingDevicesUiEvent uiEvent = mExpanded + ? HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_EXPAND_CONTROLS + : HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_COLLAPSE_CONTROLS; + mUiEventLogger.log(uiEvent, mLaunchSourceId); + } if (mListener != null) { mListener.onExpandIconClick(); } @@ -243,6 +266,11 @@ public class AmbientVolumeLayout extends LinearLayout implements AmbientVolumeUi updateVolumeLevel(); } + void setUiEventLogger(HearingDevicesUiEventLogger uiEventLogger, int launchSourceId) { + mUiEventLogger = uiEventLogger; + mLaunchSourceId = launchSourceId; + } + private void updateVolumeLevel() { int leftLevel, rightLevel; if (mExpanded) { 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 22ecb0af8c18..786d27af3994 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -382,6 +382,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, private void setupAmbientControls(CachedBluetoothDevice activeHearingDevice) { final AmbientVolumeLayout ambientLayout = mDialog.requireViewById(R.id.ambient_layout); + ambientLayout.setUiEventLogger(mUiEventLogger, mLaunchSourceId); mAmbientController = new AmbientVolumeUiController( mDialog.getContext(), mLocalBluetoothManager, ambientLayout); mAmbientController.setShowUiWhenLocalDataExist(false); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.kt b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.kt index 9e77b02be495..fe1d5040c6f5 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.kt @@ -29,7 +29,17 @@ enum class HearingDevicesUiEvent(private val id: Int) : UiEventLogger.UiEventEnu @UiEvent(doc = "Click on the device gear to enter device detail page") HEARING_DEVICES_GEAR_CLICK(1853), @UiEvent(doc = "Select a preset from preset spinner") HEARING_DEVICES_PRESET_SELECT(1854), - @UiEvent(doc = "Click on related tool") HEARING_DEVICES_RELATED_TOOL_CLICK(1856); + @UiEvent(doc = "Click on related tool") HEARING_DEVICES_RELATED_TOOL_CLICK(1856), + @UiEvent(doc = "Change the ambient volume with unified control") + HEARING_DEVICES_AMBIENT_CHANGE_UNIFIED(2149), + @UiEvent(doc = "Change the ambient volume with separated control") + HEARING_DEVICES_AMBIENT_CHANGE_SEPARATED(2150), + @UiEvent(doc = "Mute the ambient volume") HEARING_DEVICES_AMBIENT_MUTE(2151), + @UiEvent(doc = "Unmute the ambient volume") HEARING_DEVICES_AMBIENT_UNMUTE(2152), + @UiEvent(doc = "Expand the ambient volume controls") + HEARING_DEVICES_AMBIENT_EXPAND_CONTROLS(2153), + @UiEvent(doc = "Collapse the ambient volume controls") + HEARING_DEVICES_AMBIENT_COLLAPSE_CONTROLS(2154); override fun getId(): Int = this.id } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index b6537118324e..4c8a8f1c13d7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -33,6 +33,7 @@ import android.graphics.PixelFormat; import android.hardware.biometrics.BiometricAuthenticator.Modality; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricManager.Authenticators; +import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.PromptInfo; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; @@ -100,7 +101,7 @@ import javax.inject.Provider; */ @Deprecated public class AuthContainerView extends LinearLayout - implements AuthDialog, WakefulnessLifecycle.Observer, CredentialView.Host { + implements WakefulnessLifecycle.Observer, CredentialView.Host { private static final String TAG = "AuthContainerView"; @@ -158,12 +159,11 @@ public class AuthContainerView extends LinearLayout private final Set<Integer> mFailedModalities = new HashSet<Integer>(); private final OnBackInvokedCallback mBackCallback = this::onBackInvoked; - private final @Background DelayableExecutor mBackgroundExecutor; private final MSDLPlayer mMSDLPlayer; // Non-null only if the dialog is in the act of dismissing and has not sent the reason yet. - @Nullable @AuthDialogCallback.DismissedReason private Integer mPendingCallbackReason; + @Nullable @BiometricPrompt.DismissedReason private Integer mPendingCallbackReason; // HAT received from LockSettingsService when credential is verified. @Nullable private byte[] mCredentialAttestation; @@ -188,18 +188,18 @@ public class AuthContainerView extends LinearLayout final class BiometricCallback implements Spaghetti.Callback { @Override public void onAuthenticated() { - animateAway(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED); + animateAway(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED); } @Override public void onUserCanceled() { sendEarlyUserCanceled(); - animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); + animateAway(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); } @Override public void onButtonNegative() { - animateAway(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE); + animateAway(BiometricPrompt.DISMISSED_REASON_NEGATIVE); } @Override @@ -210,12 +210,12 @@ public class AuthContainerView extends LinearLayout @Override public void onContentViewMoreOptionsButtonPressed() { - animateAway(AuthDialogCallback.DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS); + animateAway(BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS); } @Override public void onError() { - animateAway(AuthDialogCallback.DISMISSED_ERROR); + animateAway(BiometricPrompt.DISMISSED_REASON_ERROR); } @Override @@ -234,20 +234,20 @@ public class AuthContainerView extends LinearLayout @Override public void onAuthenticatedAndConfirmed() { - animateAway(AuthDialogCallback.DISMISSED_BUTTON_POSITIVE); + animateAway(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED); } } @Override public void onCredentialMatched(@NonNull byte[] attestation) { mCredentialAttestation = attestation; - animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED); + animateAway(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED); } @Override public void onCredentialAborted() { sendEarlyUserCanceled(); - animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); + animateAway(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); } @Override @@ -277,7 +277,7 @@ public class AuthContainerView extends LinearLayout com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss, null /* OnClickListener */) .setOnDismissListener( - dialog -> animateAway(AuthDialogCallback.DISMISSED_ERROR)) + dialog -> animateAway(BiometricPrompt.DISMISSED_REASON_ERROR)) .create(); alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); alertDialog.show(); @@ -349,7 +349,6 @@ public class AuthContainerView extends LinearLayout mPanelView = mLayout.findViewById(R.id.panel); mPanelController = new AuthPanelController(mContext, mPanelView); - mBackgroundExecutor = bgExecutor; mInteractionJankMonitor = jankMonitor; mCredentialViewModelProvider = credentialViewModelProvider; @@ -394,7 +393,7 @@ public class AuthContainerView extends LinearLayout @VisibleForTesting public void onBackInvoked() { sendEarlyUserCanceled(); - animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); + animateAway(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); } void sendEarlyUserCanceled() { @@ -402,7 +401,6 @@ public class AuthContainerView extends LinearLayout BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL, getRequestId()); } - @Override public boolean isAllowDeviceCredentials() { return Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo); } @@ -450,7 +448,6 @@ public class AuthContainerView extends LinearLayout mPanelController.setContainerDimensions(getMeasuredWidth(), getMeasuredHeight()); } - @Override public void onOrientationChanged() { } @@ -538,10 +535,9 @@ public class AuthContainerView extends LinearLayout @Override public void onStartedGoingToSleep() { - animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); + animateAway(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); } - @Override public void show(WindowManager wm) { wm.addView(this, getLayoutParams(mWindowToken, mConfig.mPromptInfo.getTitle())); } @@ -559,7 +555,6 @@ public class AuthContainerView extends LinearLayout } } - @Override public void dismissWithoutCallback(boolean animate) { if (animate) { animateAway(false /* sendReason */, 0 /* reason */); @@ -569,12 +564,10 @@ public class AuthContainerView extends LinearLayout } } - @Override public void dismissFromSystemServer() { animateAway(false /* sendReason */, 0 /* reason */); } - @Override public void onAuthenticationSucceeded(@Modality int modality) { if (mBiometricView != null) { mBiometricView.onAuthenticationSucceeded(modality); @@ -583,7 +576,6 @@ public class AuthContainerView extends LinearLayout } } - @Override public void onAuthenticationFailed(@Modality int modality, String failureReason) { if (mBiometricView != null) { mFailedModalities.add(modality); @@ -593,7 +585,6 @@ public class AuthContainerView extends LinearLayout } } - @Override public void onHelp(@Modality int modality, String help) { if (mBiometricView != null) { mBiometricView.onHelp(modality, help); @@ -602,7 +593,6 @@ public class AuthContainerView extends LinearLayout } } - @Override public void onError(@Modality int modality, String error) { if (mBiometricView != null) { mBiometricView.onError(modality, error); @@ -611,7 +601,6 @@ public class AuthContainerView extends LinearLayout } } - @Override public void onPointerDown() { if (mBiometricView != null) { if (mFailedModalities.contains(TYPE_FACE)) { @@ -624,22 +613,18 @@ public class AuthContainerView extends LinearLayout } } - @Override public String getOpPackageName() { return mConfig.mOpPackageName; } - @Override public String getClassNameIfItIsConfirmDeviceCredentialActivity() { return mConfig.mPromptInfo.getClassNameIfItIsConfirmDeviceCredentialActivity(); } - @Override public long getRequestId() { return mConfig.mRequestId; } - @Override public void animateToCredentialUI(boolean isError) { if (mBiometricView != null) { mBiometricView.startTransitionToCredentialUI(isError); @@ -648,11 +633,11 @@ public class AuthContainerView extends LinearLayout } } - void animateAway(@AuthDialogCallback.DismissedReason int reason) { + void animateAway(@BiometricPrompt.DismissedReason int reason) { animateAway(true /* sendReason */, reason); } - private void animateAway(boolean sendReason, @AuthDialogCallback.DismissedReason int reason) { + private void animateAway(boolean sendReason, @BiometricPrompt.DismissedReason int reason) { if (mContainerState == STATE_ANIMATING_IN) { Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn"); mContainerState = STATE_PENDING_DISMISS; @@ -732,7 +717,7 @@ public class AuthContainerView extends LinearLayout private void onDialogAnimatedIn() { if (mContainerState == STATE_PENDING_DISMISS) { Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now"); - animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); + animateAway(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); return; } if (mContainerState == STATE_ANIMATING_OUT || mContainerState == STATE_GONE) { @@ -748,7 +733,6 @@ public class AuthContainerView extends LinearLayout } } - @Override public PromptViewModel getViewModel() { return mPromptViewModel; } @@ -776,7 +760,6 @@ public class AuthContainerView extends LinearLayout return lp; } - @Override public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println(" isAttachedToWindow=" + isAttachedToWindow()); pw.println(" containerState=" + mContainerState); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index eee5f9e34317..68a282018ba4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -163,7 +163,7 @@ public class AuthController implements // TODO: These should just be saved from onSaveState private SomeArgs mCurrentDialogArgs; @VisibleForTesting - AuthDialog mCurrentDialog; + AuthContainerView mCurrentDialog; @NonNull private final WindowManager mWindowManager; @NonNull private final DisplayManager mDisplayManager; @@ -222,7 +222,7 @@ public class AuthController implements closeDialog(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, reasonString); } - private void closeDialog(@DismissedReason int reason, String reasonString) { + private void closeDialog(@BiometricPrompt.DismissedReason int reason, String reasonString) { if (isShowing()) { Log.i(TAG, "Close BP, reason :" + reasonString); mCurrentDialog.dismissWithoutCallback(true /* animate */); @@ -511,60 +511,14 @@ public class AuthController implements } @Override - public void onDismissed(@DismissedReason int reason, - @Nullable byte[] credentialAttestation, long requestId) { - + public void onDismissed(@BiometricPrompt.DismissedReason int reason, + @Nullable byte[] credentialAttestation, long requestId) { if (mCurrentDialog != null && requestId != mCurrentDialog.getRequestId()) { Log.w(TAG, "requestId doesn't match, skip onDismissed"); return; } - switch (reason) { - case AuthDialogCallback.DISMISSED_USER_CANCELED: - sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, - credentialAttestation); - break; - - case AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE: - sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_NEGATIVE, - credentialAttestation); - break; - - case AuthDialogCallback.DISMISSED_BUTTON_POSITIVE: - sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED, - credentialAttestation); - break; - - case AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED: - sendResultAndCleanUp( - BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED, - credentialAttestation); - break; - - case AuthDialogCallback.DISMISSED_ERROR: - sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_ERROR, - credentialAttestation); - break; - - case AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER: - sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED, - credentialAttestation); - break; - - case AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED: - sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED, - credentialAttestation); - break; - - case AuthDialogCallback.DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS: - sendResultAndCleanUp( - BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS, - credentialAttestation); - break; - default: - Log.e(TAG, "Unhandled reason: " + reason); - break; - } + sendResultAndCleanUp(reason, credentialAttestation); } @Override @@ -699,7 +653,7 @@ public class AuthController implements mUdfpsController.onAodInterrupt(screenX, screenY, major, minor); } - private void sendResultAndCleanUp(@DismissedReason int reason, + private void sendResultAndCleanUp(@BiometricPrompt.DismissedReason int reason, @Nullable byte[] credentialAttestation) { if (mReceiver == null) { Log.e(TAG, "sendResultAndCleanUp: Receiver is null"); @@ -1244,7 +1198,7 @@ public class AuthController implements final long requestId = args.argl2; // Create a new dialog but do not replace the current one yet. - final AuthDialog newDialog = buildDialog( + final AuthContainerView newDialog = buildDialog( mBackgroundExecutor, promptInfo, requireConfirmation, @@ -1327,7 +1281,7 @@ public class AuthController implements return mContext.createDisplayContext(display).getSystemService(WindowManager.class); } - private void onDialogDismissed(@DismissedReason int reason) { + private void onDialogDismissed(@BiometricPrompt.DismissedReason int reason) { if (DEBUG) Log.d(TAG, "onDialogDismissed: " + reason); if (mCurrentDialog == null) { Log.w(TAG, "Dialog already dismissed"); @@ -1361,7 +1315,7 @@ public class AuthController implements } } - protected AuthDialog buildDialog(@Background DelayableExecutor bgExecutor, + protected AuthContainerView buildDialog(@Background DelayableExecutor bgExecutor, PromptInfo promptInfo, boolean requireConfirmation, int userId, int[] sensorIds, String opPackageName, boolean skipIntro, long operationId, long requestId, @NonNull WakefulnessLifecycle wakefulnessLifecycle, @@ -1389,7 +1343,7 @@ public class AuthController implements @Override public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { - final AuthDialog dialog = mCurrentDialog; + final AuthContainerView dialog = mCurrentDialog; pw.println(" mCachedDisplayInfo=" + mCachedDisplayInfo); pw.println(" mScaleFactor=" + mScaleFactor); pw.println(" fingerprintSensorLocationInNaturalOrientation=" diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java deleted file mode 100644 index 861191671ba9..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics; - -import android.hardware.biometrics.BiometricAuthenticator.Modality; -import android.view.WindowManager; - -import com.android.systemui.Dumpable; -import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel; - -/** - * Interface for the biometric dialog UI. - * - * TODO(b/287311775): remove along with legacy controller once flag is removed - */ -@Deprecated -public interface AuthDialog extends Dumpable { - - /** - * Parameters used when laying out {@link AuthBiometricView}, its subclasses, and - * {@link AuthPanelController}. - */ - class LayoutParams { - public final int mMediumHeight; - public final int mMediumWidth; - - public LayoutParams(int mediumWidth, int mediumHeight) { - mMediumWidth = mediumWidth; - mMediumHeight = mediumHeight; - } - } - - /** - * Show the dialog. - * @param wm - */ - void show(WindowManager wm); - - /** - * Dismiss the dialog without sending a callback. - */ - void dismissWithoutCallback(boolean animate); - - /** - * Dismiss the dialog. Animate away. - */ - void dismissFromSystemServer(); - - /** - * Biometric authenticated. May be pending user confirmation, or completed. - */ - void onAuthenticationSucceeded(@Modality int modality); - - /** - * Authentication failed (reject, timeout). Dialog stays showing. - * @param modality sensor modality that triggered the error - * @param failureReason message - */ - void onAuthenticationFailed(@Modality int modality, String failureReason); - - /** - * Authentication rejected, or help message received. - * @param modality sensor modality that triggered the help message - * @param help message - */ - void onHelp(@Modality int modality, String help); - - /** - * Authentication failed. Dialog going away. - * @param modality sensor modality that triggered the error - * @param error message - */ - void onError(@Modality int modality, String error); - - /** UDFPS pointer down event. */ - void onPointerDown(); - - /** - * Get the client's package name - */ - String getOpPackageName(); - - /** - * Get the class name of ConfirmDeviceCredentialActivity. Returns null if the direct caller is - * not ConfirmDeviceCredentialActivity. - */ - String getClassNameIfItIsConfirmDeviceCredentialActivity(); - - /** The requestId of the underlying operation within the framework. */ - long getRequestId(); - - /** - * Animate to credential UI. Typically called after biometric is locked out. - */ - void animateToCredentialUI(boolean isError); - - /** - * @return true if device credential is allowed. - */ - boolean isAllowDeviceCredentials(); - - /** - * Called when the device's orientation changed and the dialog may need to do another - * layout. This is most relevant to UDFPS since configuration changes are not sent by - * the framework in equivalent cases (landscape to reverse landscape) but the dialog - * must remain fixed on the physical sensor location. - */ - void onOrientationChanged(); - - PromptViewModel getViewModel(); -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java index 024c6eaa75bb..31c63d3c57e0 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java @@ -16,40 +16,20 @@ package com.android.systemui.biometrics; -import android.annotation.IntDef; import android.annotation.Nullable; +import android.hardware.biometrics.BiometricPrompt; /** * Callback interface for dialog views. These should be implemented by the controller (e.g. * FingerprintDialogImpl) and passed into their views (e.g. FingerprintDialogView). */ public interface AuthDialogCallback { - - int DISMISSED_USER_CANCELED = 1; - int DISMISSED_BUTTON_NEGATIVE = 2; - int DISMISSED_BUTTON_POSITIVE = 3; - int DISMISSED_BIOMETRIC_AUTHENTICATED = 4; - int DISMISSED_ERROR = 5; - int DISMISSED_BY_SYSTEM_SERVER = 6; - int DISMISSED_CREDENTIAL_AUTHENTICATED = 7; - int DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS = 8; - - @IntDef({DISMISSED_USER_CANCELED, - DISMISSED_BUTTON_NEGATIVE, - DISMISSED_BUTTON_POSITIVE, - DISMISSED_BIOMETRIC_AUTHENTICATED, - DISMISSED_ERROR, - DISMISSED_BY_SYSTEM_SERVER, - DISMISSED_CREDENTIAL_AUTHENTICATED, - DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS}) - @interface DismissedReason {} - /** * Invoked when the dialog is dismissed - * @param reason + * @param reason - the {@link BiometricPrompt.DismissedReason} for dismissing * @param credentialAttestation the HAT received from LockSettingsService upon verification */ - void onDismissed(@DismissedReason int reason, + void onDismissed(@BiometricPrompt.DismissedReason int reason, @Nullable byte[] credentialAttestation, long requestId); /** 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 cce1ae1a2947..6473b1c30586 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 @@ -237,7 +237,15 @@ constructor( with(mediaHost) { expansion = MediaHostState.EXPANDED expandedMatchesParentHeight = true - showsOnlyActiveMedia = false + if (v2FlagEnabled()) { + // Only show active media to match lock screen, not resumable media, which can + // persist + // for up to 2 days. + showsOnlyActiveMedia = true + } else { + // Maintain old behavior on tablet until V2 flag rolls out. + showsOnlyActiveMedia = false + } falsingProtectionNeeded = false disablePagination = true init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt index 0908e3b5bf85..a779c4c998a7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt @@ -31,7 +31,6 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION -import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER @@ -67,7 +66,6 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER to System, KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL to System, KEY_GESTURE_TYPE_LOCK_SCREEN to System, - KEY_GESTURE_TYPE_OPEN_NOTES to System, KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to System, KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to System, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to System, @@ -113,7 +111,6 @@ class InputGestureMaps @Inject constructor(private val context: Context) { R.string.shortcut_helper_category_system_controls, KEY_GESTURE_TYPE_LOCK_SCREEN to R.string.shortcut_helper_category_system_controls, KEY_GESTURE_TYPE_ALL_APPS to R.string.shortcut_helper_category_system_controls, - KEY_GESTURE_TYPE_OPEN_NOTES to R.string.shortcut_helper_category_system_apps, KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to R.string.shortcut_helper_category_system_apps, KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.shortcut_helper_category_system_apps, @@ -173,7 +170,6 @@ class InputGestureMaps @Inject constructor(private val context: Context) { R.string.group_system_access_notification_shade, KEY_GESTURE_TYPE_LOCK_SCREEN to R.string.group_system_lock_screen, KEY_GESTURE_TYPE_ALL_APPS to R.string.group_system_access_all_apps_search, - KEY_GESTURE_TYPE_OPEN_NOTES to R.string.group_system_quick_memo, KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to R.string.group_system_access_system_settings, KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.group_system_access_google_assistant, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt index 8bed8537b6c5..a7375f7e7efc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt @@ -178,11 +178,6 @@ constructor(@Main private val resources: Resources, private val inputManager: In private fun systemAppsShortcuts() = listOf( - // Pull up Notes app for quick memo: - // - Meta + Ctrl + N - shortcutInfo(resources.getString(R.string.group_system_quick_memo)) { - command(META_META_ON or META_CTRL_ON, KEYCODE_N) - }, // Access system settings: // - Meta + I shortcutInfo(resources.getString(R.string.group_system_access_system_settings)) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index 6f5f662d6fa3..0700ec639153 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -159,6 +159,7 @@ constructor( val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value val isKeyguardGoingAway = keyguardInteractor.isKeyguardGoingAway.value + val canStartDreaming = dreamManager.canStartDreaming(false) if (!deviceEntryInteractor.isLockscreenEnabled()) { if (!SceneContainerFlag.isEnabled) { @@ -191,6 +192,13 @@ constructor( if (!SceneContainerFlag.isEnabled) { transitionToGlanceableHub() } + } else if (canStartDreaming) { + // If we're waking up to dream, transition directly to dreaming without + // showing the lockscreen. + startTransitionTo( + KeyguardState.DREAMING, + ownerReason = "moving from doze to dream", + ) } else { startTransitionTo(KeyguardState.LOCKSCREEN) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt index e6a85c6860c5..9018c58a7e36 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt @@ -39,4 +39,6 @@ constructor(animationFlow: KeyguardTransitionAnimationFlow) { ) val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) + // Notifications should not be shown while transitioning to dream. + val notificationAlpha = transitionAnimation.immediatelyTransitionTo(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt index 6d796d96ea71..3f538203aee9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt @@ -49,9 +49,10 @@ import com.android.systemui.media.controls.ui.util.MediaArtworkHelper import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.media.controls.ui.viewmodel.MediaActionViewModel import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel -import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_CENTER_ALPHA import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_END_ALPHA +import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_START_ALPHA +import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.SEMANTIC_ACTIONS_ALL import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.SEMANTIC_ACTIONS_COMPACT import com.android.systemui.media.controls.ui.viewmodel.MediaOutputSwitcherViewModel @@ -537,18 +538,24 @@ object MediaControlViewBinder { height: Int, ): LayerDrawable { val albumArt = MediaArtworkHelper.getScaledBackground(context, artworkIcon, width, height) - val alpha = + val startAlpha = if (Flags.mediaControlsA11yColors()) { - MEDIA_PLAYER_SCRIM_CENTER_ALPHA - } else { MEDIA_PLAYER_SCRIM_START_ALPHA + } else { + MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY + } + val endAlpha = + if (Flags.mediaControlsA11yColors()) { + MEDIA_PLAYER_SCRIM_END_ALPHA + } else { + MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY } return MediaArtworkHelper.setUpGradientColorOnDrawable( albumArt, context.getDrawable(R.drawable.qs_media_scrim)?.mutate() as GradientDrawable, mutableColorScheme, - alpha, - MEDIA_PLAYER_SCRIM_END_ALPHA, + startAlpha, + endAlpha, ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt index 34f7c4dcaec0..c9716be52408 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt @@ -18,6 +18,9 @@ package com.android.systemui.media.controls.ui.binder import android.animation.Animator import android.animation.ObjectAnimator +import android.icu.text.MeasureFormat +import android.icu.util.Measure +import android.icu.util.MeasureUnit import android.text.format.DateUtils import androidx.annotation.UiThread import androidx.lifecycle.Observer @@ -28,8 +31,11 @@ import com.android.systemui.media.controls.ui.drawable.SquigglyProgress import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel import com.android.systemui.res.R +import java.util.Locale private const val TAG = "SeekBarObserver" +private const val MIN_IN_SEC = 60 +private const val HOUR_IN_SEC = MIN_IN_SEC * 60 /** * Observer for changes from SeekBarViewModel. @@ -127,10 +133,9 @@ open class SeekBarObserver(private val holder: MediaViewHolder) : } holder.seekBar.setMax(data.duration) - val totalTimeString = - DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS) + val totalTimeDescription = formatTimeContentDescription(data.duration) if (data.scrubbing) { - holder.scrubbingTotalTimeView.text = totalTimeString + holder.scrubbingTotalTimeView.text = formatTimeLabel(data.duration) } data.elapsedTime?.let { @@ -148,20 +153,62 @@ open class SeekBarObserver(private val holder: MediaViewHolder) : } } - val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS) + val elapsedTimeDescription = formatTimeContentDescription(it) if (data.scrubbing) { - holder.scrubbingElapsedTimeView.text = elapsedTimeString + holder.scrubbingElapsedTimeView.text = formatTimeLabel(it) } holder.seekBar.contentDescription = holder.seekBar.context.getString( R.string.controls_media_seekbar_description, - elapsedTimeString, - totalTimeString + elapsedTimeDescription, + totalTimeDescription, ) } } + /** Returns a time string suitable for display, e.g. "12:34" */ + private fun formatTimeLabel(milliseconds: Int): CharSequence { + return DateUtils.formatElapsedTime(milliseconds / DateUtils.SECOND_IN_MILLIS) + } + + /** + * Returns a time string suitable for content description, e.g. "12 minutes 34 seconds" + * + * Follows same logic as Chronometer#formatDuration + */ + private fun formatTimeContentDescription(milliseconds: Int): CharSequence { + var seconds = milliseconds / DateUtils.SECOND_IN_MILLIS + + val hours = + if (seconds >= HOUR_IN_SEC) { + seconds / HOUR_IN_SEC + } else { + 0 + } + seconds -= hours * HOUR_IN_SEC + + val minutes = + if (seconds >= MIN_IN_SEC) { + seconds / MIN_IN_SEC + } else { + 0 + } + seconds -= minutes * MIN_IN_SEC + + val measures = arrayListOf<Measure>() + if (hours > 0) { + measures.add(Measure(hours, MeasureUnit.HOUR)) + } + if (minutes > 0) { + measures.add(Measure(minutes, MeasureUnit.MINUTE)) + } + measures.add(Measure(seconds, MeasureUnit.SECOND)) + + return MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) + .formatMeasures(*measures.toTypedArray()) + } + @VisibleForTesting open fun buildResetAnimator(targetTime: Int): Animator { val animator = @@ -169,7 +216,7 @@ open class SeekBarObserver(private val holder: MediaViewHolder) : holder.seekBar, "progress", holder.seekBar.progress, - targetTime + RESET_ANIMATION_DURATION_MS + targetTime + RESET_ANIMATION_DURATION_MS, ) animator.setAutoCancel(true) animator.duration = RESET_ANIMATION_DURATION_MS.toLong() diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java index 39c08daf53d6..694a4c7e493d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java @@ -23,7 +23,10 @@ import static com.android.systemui.Flags.communalHub; import static com.android.systemui.Flags.mediaLockscreenLaunchAnimation; import static com.android.systemui.media.controls.domain.pipeline.MediaActionsKt.getNotificationActions; import static com.android.systemui.media.controls.shared.model.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS; -import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_CENTER_ALPHA; +import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_END_ALPHA; +import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY; +import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_START_ALPHA; +import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY; import android.animation.Animator; import android.animation.AnimatorInflater; @@ -176,9 +179,7 @@ public class MediaControlPanel { protected static final int SMARTSPACE_CARD_DISMISS_EVENT = 761; private static final float REC_MEDIA_COVER_SCALE_FACTOR = 1.25f; - private static final float MEDIA_SCRIM_START_ALPHA = 0.25f; private static final float MEDIA_REC_SCRIM_START_ALPHA = 0.15f; - private static final float MEDIA_PLAYER_SCRIM_END_ALPHA = 1.0f; private static final float MEDIA_REC_SCRIM_END_ALPHA = 1.0f; private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS); @@ -1093,11 +1094,12 @@ public class MediaControlPanel { Drawable albumArt = getScaledBackground(artworkIcon, width, height); GradientDrawable gradient = (GradientDrawable) mContext.getDrawable( R.drawable.qs_media_scrim).mutate(); - float startAlpha = (Flags.mediaControlsA11yColors()) - ? MEDIA_PLAYER_SCRIM_CENTER_ALPHA - : MEDIA_SCRIM_START_ALPHA; + if (Flags.mediaControlsA11yColors()) { + return setupGradientColorOnDrawable(albumArt, gradient, mutableColorScheme, + MEDIA_PLAYER_SCRIM_START_ALPHA, MEDIA_PLAYER_SCRIM_END_ALPHA); + } return setupGradientColorOnDrawable(albumArt, gradient, mutableColorScheme, - startAlpha, MEDIA_PLAYER_SCRIM_END_ALPHA); + MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY, MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY); } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt index 198155b3f297..b687dce20b06 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt @@ -1137,6 +1137,7 @@ constructor( ) { gutsViewHolder.gutsText.setTypeface(menuTF) gutsViewHolder.dismissText.setTypeface(menuTF) + gutsViewHolder.cancelText.setTypeface(menuTF) titleText.setTypeface(titleTF) artistText.setTypeface(artistTF) seamlessText.setTypeface(menuTF) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt index 9153e17393d2..bcda485272c2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt @@ -419,8 +419,10 @@ class MediaControlViewModel( const val TURBULENCE_NOISE_PLAY_MS_DURATION = 7500L @Deprecated("Remove with media_controls_a11y_colors flag") - const val MEDIA_PLAYER_SCRIM_START_ALPHA = 0.25f - const val MEDIA_PLAYER_SCRIM_CENTER_ALPHA = 0.75f - const val MEDIA_PLAYER_SCRIM_END_ALPHA = 1.0f + const val MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY = 0.25f + @Deprecated("Remove with media_controls_a11y_colors flag") + const val MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY = 1.0f + const val MEDIA_PLAYER_SCRIM_START_ALPHA = 0.65f + const val MEDIA_PLAYER_SCRIM_END_ALPHA = 0.75f } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSEvents.kt b/packages/SystemUI/src/com/android/systemui/qs/QSEvents.kt index 24bb16a11fe7..3a81102699f7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSEvents.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/QSEvents.kt @@ -27,9 +27,7 @@ object QSEvents { private set fun setLoggerForTesting(): UiEventLoggerFake { - return UiEventLoggerFake().also { - qsUiEventsLogger = it - } + return UiEventLoggerFake().also { qsUiEventsLogger = it } } fun resetLogger() { @@ -40,32 +38,28 @@ object QSEvents { enum class QSEvent(private val _id: Int) : UiEventLogger.UiEventEnum { @UiEvent(doc = "Tile clicked. It has an instance id and a spec (or packageName)") QS_ACTION_CLICK(387), - - @UiEvent(doc = "Tile secondary button clicked. " + - "It has an instance id and a spec (or packageName)") + @UiEvent( + doc = + "Tile secondary button clicked. " + "It has an instance id and a spec (or packageName)" + ) QS_ACTION_SECONDARY_CLICK(388), - @UiEvent(doc = "Tile long clicked. It has an instance id and a spec (or packageName)") QS_ACTION_LONG_PRESS(389), - - @UiEvent(doc = "Quick Settings panel expanded") - QS_PANEL_EXPANDED(390), - - @UiEvent(doc = "Quick Settings panel collapsed") - QS_PANEL_COLLAPSED(391), - - @UiEvent(doc = "Tile visible in Quick Settings panel. The tile may be in a different page. " + - "It has an instance id and a spec (or packageName)") + @UiEvent(doc = "Quick Settings panel expanded") QS_PANEL_EXPANDED(390), + @UiEvent(doc = "Quick Settings panel collapsed") QS_PANEL_COLLAPSED(391), + @UiEvent( + doc = + "Tile visible in Quick Settings panel. The tile may be in a different page. " + + "It has an instance id and a spec (or packageName)" + ) QS_TILE_VISIBLE(392), - - @UiEvent(doc = "Quick Quick Settings panel expanded") - QQS_PANEL_EXPANDED(393), - - @UiEvent(doc = "Quick Quick Settings panel collapsed") - QQS_PANEL_COLLAPSED(394), - - @UiEvent(doc = "Tile visible in Quick Quick Settings panel. " + - "It has an instance id and a spec (or packageName)") + @UiEvent(doc = "Quick Quick Settings panel expanded") QQS_PANEL_EXPANDED(393), + @UiEvent(doc = "Quick Quick Settings panel collapsed") QQS_PANEL_COLLAPSED(394), + @UiEvent( + doc = + "Tile visible in Quick Quick Settings panel. " + + "It has an instance id and a spec (or packageName)" + ) QQS_TILE_VISIBLE(395); override fun getId() = _id @@ -73,47 +67,32 @@ enum class QSEvent(private val _id: Int) : UiEventLogger.UiEventEnum { enum class QSEditEvent(private val _id: Int) : UiEventLogger.UiEventEnum { - @UiEvent(doc = "Tile removed from current tiles") - QS_EDIT_REMOVE(210), - - @UiEvent(doc = "Tile added to current tiles") - QS_EDIT_ADD(211), - - @UiEvent(doc = "Tile moved") - QS_EDIT_MOVE(212), - - @UiEvent(doc = "QS customizer open") - QS_EDIT_OPEN(213), - - @UiEvent(doc = "QS customizer closed") - QS_EDIT_CLOSED(214), - - @UiEvent(doc = "QS tiles reset") - QS_EDIT_RESET(215); + @UiEvent(doc = "Tile removed from current tiles") QS_EDIT_REMOVE(210), + @UiEvent(doc = "Tile added to current tiles") QS_EDIT_ADD(211), + @UiEvent(doc = "Tile moved") QS_EDIT_MOVE(212), + @UiEvent(doc = "QS customizer open") QS_EDIT_OPEN(213), + @UiEvent(doc = "QS customizer closed") QS_EDIT_CLOSED(214), + @UiEvent(doc = "QS tiles reset") QS_EDIT_RESET(215), + @UiEvent(doc = "QS edit mode resize tile to large") QS_EDIT_RESIZE_LARGE(2122), + @UiEvent(doc = "QS edit mode resize tile to small") QS_EDIT_RESIZE_SMALL(2123); override fun getId() = _id } /** - * Events from the QS DND tile dialog. {@see QSZenModeDialogMetricsLogger} - * Other names for DND (Do Not Disturb) include "Zen" and "Priority mode". + * Events from the QS DND tile dialog. {@see QSZenModeDialogMetricsLogger} Other names for DND (Do + * Not Disturb) include "Zen" and "Priority mode". */ enum class QSDndEvent(private val _id: Int) : UiEventLogger.UiEventEnum { - @UiEvent(doc = "User selected an option on the DND dialog") - QS_DND_CONDITION_SELECT(420), - + @UiEvent(doc = "User selected an option on the DND dialog") QS_DND_CONDITION_SELECT(420), @UiEvent(doc = "User increased countdown duration of DND from the DND dialog") QS_DND_TIME_UP(422), - @UiEvent(doc = "User decreased countdown duration of DND from the DND dialog") QS_DND_TIME_DOWN(423), - @UiEvent(doc = "User enabled DND from the QS DND dialog to last until manually turned off") QS_DND_DIALOG_ENABLE_FOREVER(946), - @UiEvent(doc = "User enabled DND from the QS DND dialog to last until the next alarm goes off") QS_DND_DIALOG_ENABLE_UNTIL_ALARM(947), - @UiEvent(doc = "User enabled DND from the QS DND dialog to last until countdown is done") QS_DND_DIALOG_ENABLE_UNTIL_COUNTDOWN(948); @@ -121,29 +100,17 @@ enum class QSDndEvent(private val _id: Int) : UiEventLogger.UiEventEnum { } enum class QSUserSwitcherEvent(private val _id: Int) : UiEventLogger.UiEventEnum { - @UiEvent(doc = "The current user has been switched in the detail panel") - QS_USER_SWITCH(424), - - @UiEvent(doc = "User switcher QS dialog open") - QS_USER_DETAIL_OPEN(425), - - @UiEvent(doc = "User switcher QS dialog closed") - QS_USER_DETAIL_CLOSE(426), - - @UiEvent(doc = "User switcher QS dialog more settings pressed") - QS_USER_MORE_SETTINGS(427), - - @UiEvent(doc = "The user has added a guest in the detail panel") - QS_USER_GUEST_ADD(754), - + @UiEvent(doc = "The current user has been switched in the detail panel") QS_USER_SWITCH(424), + @UiEvent(doc = "User switcher QS dialog open") QS_USER_DETAIL_OPEN(425), + @UiEvent(doc = "User switcher QS dialog closed") QS_USER_DETAIL_CLOSE(426), + @UiEvent(doc = "User switcher QS dialog more settings pressed") QS_USER_MORE_SETTINGS(427), + @UiEvent(doc = "The user has added a guest in the detail panel") QS_USER_GUEST_ADD(754), @UiEvent(doc = "The user selected 'Start over' after switching to the existing Guest user") QS_USER_GUEST_WIPE(755), - @UiEvent(doc = "The user selected 'Yes, continue' after switching to the existing Guest user") QS_USER_GUEST_CONTINUE(756), - @UiEvent(doc = "The user has pressed 'Remove guest' in the detail panel") QS_USER_GUEST_REMOVE(757); override fun getId() = _id -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt index 482cd4014acf..3f279b0f7a74 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt @@ -16,15 +16,18 @@ package com.android.systemui.qs.panels.domain.interactor +import com.android.internal.logging.UiEventLogger import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel +import com.android.systemui.qs.QSEditEvent import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository import com.android.systemui.qs.panels.data.repository.LargeTileSpanRepository import com.android.systemui.qs.panels.shared.model.PanelsLog import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.metricSpec import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted @@ -40,6 +43,7 @@ constructor( private val repo: DefaultLargeTilesRepository, private val currentTilesInteractor: CurrentTilesInteractor, private val preferencesInteractor: QSPreferencesInteractor, + private val uiEventLogger: UiEventLogger, largeTilesSpanRepo: LargeTileSpanRepository, @PanelsLog private val logBuffer: LogBuffer, @Application private val applicationScope: CoroutineScope, @@ -70,8 +74,18 @@ constructor( val isIcon = !largeTilesSpecs.value.contains(spec) if (toIcon && !isIcon) { preferencesInteractor.setLargeTilesSpecs(largeTilesSpecs.value - spec) + uiEventLogger.log( + /* event= */ QSEditEvent.QS_EDIT_RESIZE_SMALL, + /* uid= */ 0, + /* packageName= */ spec.metricSpec, + ) } else if (!toIcon && isIcon) { preferencesInteractor.setLargeTilesSpecs(largeTilesSpecs.value + spec) + uiEventLogger.log( + /* event= */ QSEditEvent.QS_EDIT_RESIZE_LARGE, + /* uid= */ 0, + /* packageName= */ spec.metricSpec, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractor.kt index 186bfcbbc8e2..a4de1d675a15 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractor.kt @@ -17,11 +17,19 @@ package com.android.systemui.shade.domain.interactor import android.content.Intent +import android.content.IntentFilter +import android.os.UserHandle import android.provider.AlarmClock +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.ActivityStarter import com.android.systemui.shade.data.repository.ShadeHeaderClockRepository +import com.android.systemui.util.kotlin.emitOnStart +import com.android.systemui.util.time.SystemClock +import java.util.Date import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map @SysUISingleton class ShadeHeaderClockInteractor @@ -29,7 +37,20 @@ class ShadeHeaderClockInteractor constructor( private val repository: ShadeHeaderClockRepository, private val activityStarter: ActivityStarter, + private val broadcastDispatcher: BroadcastDispatcher, + private val systemClock: SystemClock, ) { + /** [Flow] that emits `Unit` whenever the timezone or locale has changed. */ + val onTimezoneOrLocaleChanged: Flow<Unit> = + broadcastFlowForActions(Intent.ACTION_TIMEZONE_CHANGED, Intent.ACTION_LOCALE_CHANGED) + .emitOnStart() + + /** [Flow] that emits the current `Date` every minute, or when the system time has changed. */ + val currentTime: Flow<Date> = + broadcastFlowForActions(Intent.ACTION_TIME_TICK, Intent.ACTION_TIME_CHANGED) + .emitOnStart() + .map { Date(systemClock.currentTimeMillis()) } + /** Launch the clock activity. */ fun launchClockActivity() { val nextAlarmIntent = repository.nextAlarmIntent @@ -38,8 +59,22 @@ constructor( } else { activityStarter.postStartActivityDismissingKeyguard( Intent(AlarmClock.ACTION_SHOW_ALARMS), - 0 + 0, ) } } + + /** + * Returns a `Flow` that, when collected, emits `Unit` whenever a broadcast matching one of the + * given [actionsToFilter] is received. + */ + private fun broadcastFlowForActions( + vararg actionsToFilter: String, + user: UserHandle = UserHandle.SYSTEM, + ): Flow<Unit> { + return broadcastDispatcher.broadcastFlow( + filter = IntentFilter().apply { actionsToFilter.forEach(::addAction) }, + user = user, + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt index 8c38d2e7550c..20b44d73e097 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt @@ -18,16 +18,15 @@ package com.android.systemui.shade.ui.viewmodel import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.icu.text.DateFormat import android.icu.text.DisplayContext -import android.os.UserHandle import android.provider.Settings import android.view.ViewGroup +import androidx.compose.material3.ColorScheme import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.Color import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.battery.BatteryMeterViewController -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.plugins.ActivityStarter @@ -42,7 +41,6 @@ import com.android.systemui.shade.domain.interactor.PrivacyChipInteractor import com.android.systemui.shade.domain.interactor.ShadeHeaderClockInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.ShadeModeInteractor -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager @@ -50,18 +48,18 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIc import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import java.util.Date import java.util.Locale +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.mapLatest /** Models UI state for the shade header. */ +@OptIn(ExperimentalCoroutinesApi::class) class ShadeHeaderViewModel @AssistedInject constructor( @@ -70,16 +68,15 @@ constructor( private val sceneInteractor: SceneInteractor, private val shadeInteractor: ShadeInteractor, private val shadeModeInteractor: ShadeModeInteractor, - private val mobileIconsInteractor: MobileIconsInteractor, + mobileIconsInteractor: MobileIconsInteractor, val mobileIconsViewModel: MobileIconsViewModel, private val privacyChipInteractor: PrivacyChipInteractor, private val clockInteractor: ShadeHeaderClockInteractor, private val tintedIconManagerFactory: TintedIconManager.Factory, private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, val statusBarIconController: StatusBarIconController, - val notificationIconContainerStatusBarViewBinder: NotificationIconContainerStatusBarViewBinder, - private val broadcastDispatcher: BroadcastDispatcher, ) : ExclusiveActivatable() { + private val hydrator = Hydrator("ShadeHeaderViewModel.hydrator") val createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager = @@ -127,9 +124,16 @@ constructor( /** True if there is exactly one mobile connection. */ val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier - private val _mobileSubIds = MutableStateFlow(emptyList<Int>()) /** The list of subscription Ids for current mobile connections. */ - val mobileSubIds: StateFlow<List<Int>> = _mobileSubIds.asStateFlow() + val mobileSubIds: List<Int> by + hydrator.hydratedStateOf( + traceName = "mobileSubIds", + initialValue = emptyList(), + source = + mobileIconsInteractor.filteredSubscriptions.map { list -> + list.map { it.subscriptionId } + }, + ) /** The list of PrivacyItems to be displayed by the privacy chip. */ val privacyItems: StateFlow<List<PrivacyItem>> = privacyChipInteractor.privacyItems @@ -150,45 +154,34 @@ constructor( private val longerPattern = context.getString(R.string.abbrev_wday_month_day_no_year_alarm) private val shorterPattern = context.getString(R.string.abbrev_month_day_no_year) - private val longerDateFormat = MutableStateFlow(getFormatFromPattern(longerPattern)) - private val shorterDateFormat = MutableStateFlow(getFormatFromPattern(shorterPattern)) - private val _shorterDateText: MutableStateFlow<String> = MutableStateFlow("") - val shorterDateText: StateFlow<String> = _shorterDateText.asStateFlow() + private val longerDateFormat: Flow<DateFormat> = + clockInteractor.onTimezoneOrLocaleChanged.mapLatest { getFormatFromPattern(longerPattern) } + private val shorterDateFormat: Flow<DateFormat> = + clockInteractor.onTimezoneOrLocaleChanged.mapLatest { getFormatFromPattern(shorterPattern) } - private val _longerDateText: MutableStateFlow<String> = MutableStateFlow("") - val longerDateText: StateFlow<String> = _longerDateText.asStateFlow() + val longerDateText: String by + hydrator.hydratedStateOf( + traceName = "longerDateText", + initialValue = "", + source = + combine(longerDateFormat, clockInteractor.currentTime) { format, time -> + format.format(time) + }, + ) + + val shorterDateText: String by + hydrator.hydratedStateOf( + traceName = "shorterDateText", + initialValue = "", + source = + combine(shorterDateFormat, clockInteractor.currentTime) { format, time -> + format.format(time) + }, + ) override suspend fun onActivated(): Nothing { coroutineScope { - launch { - broadcastDispatcher - .broadcastFlow( - filter = - IntentFilter().apply { - addAction(Intent.ACTION_TIME_TICK) - addAction(Intent.ACTION_TIME_CHANGED) - addAction(Intent.ACTION_TIMEZONE_CHANGED) - addAction(Intent.ACTION_LOCALE_CHANGED) - }, - user = UserHandle.SYSTEM, - map = { intent, _ -> - intent.action == Intent.ACTION_TIMEZONE_CHANGED || - intent.action == Intent.ACTION_LOCALE_CHANGED - }, - ) - .onEach { invalidateFormats -> updateDateTexts(invalidateFormats) } - .launchIn(this) - } - - launch { updateDateTexts(false) } - - launch { - mobileIconsInteractor.filteredSubscriptions - .map { list -> list.map { it.subscriptionId } } - .collect { _mobileSubIds.value = it } - } - launch { hydrator.activate() } awaitCancellation() @@ -253,33 +246,34 @@ constructor( /** Represents the background highlight of a header icons chip. */ sealed interface HeaderChipHighlight { - data object None : HeaderChipHighlight - data object Weak : HeaderChipHighlight + fun backgroundColor(colorScheme: ColorScheme): Color - data object Strong : HeaderChipHighlight - } + fun foregroundColor(colorScheme: ColorScheme): Color + + data object None : HeaderChipHighlight { + override fun backgroundColor(colorScheme: ColorScheme): Color = Color.Unspecified + + override fun foregroundColor(colorScheme: ColorScheme): Color = colorScheme.primary + } + + data object Weak : HeaderChipHighlight { + override fun backgroundColor(colorScheme: ColorScheme): Color = + colorScheme.primary.copy(alpha = 0.1f) - private fun updateDateTexts(invalidateFormats: Boolean) { - if (invalidateFormats) { - longerDateFormat.value = getFormatFromPattern(longerPattern) - shorterDateFormat.value = getFormatFromPattern(shorterPattern) + override fun foregroundColor(colorScheme: ColorScheme): Color = colorScheme.primary } - val currentTime = Date() + data object Strong : HeaderChipHighlight { + override fun backgroundColor(colorScheme: ColorScheme): Color = colorScheme.secondary - _longerDateText.value = longerDateFormat.value.format(currentTime) - _shorterDateText.value = shorterDateFormat.value.format(currentTime) + override fun foregroundColor(colorScheme: ColorScheme): Color = colorScheme.onSecondary + } } private fun getFormatFromPattern(pattern: String?): DateFormat { - val l = Locale.getDefault() - val format = DateFormat.getInstanceForSkeleton(pattern, l) - // The use of CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE instead of - // CAPITALIZATION_FOR_STANDALONE is to address - // https://unicode-org.atlassian.net/browse/ICU-21631 - // TODO(b/229287642): Switch back to CAPITALIZATION_FOR_STANDALONE - format.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE) + val format = DateFormat.getInstanceForSkeleton(pattern, Locale.getDefault()) + format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE) return format } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 97de61969ffb..7dc2ae71b63e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -58,7 +58,6 @@ import android.view.KeyEvent; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsController.Appearance; import android.view.WindowInsetsController.Behavior; -import android.view.accessibility.Flags; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -987,13 +986,7 @@ public class CommandQueue extends IStatusBar.Stub implements @Override public void addQsTile(ComponentName tile) { - if (Flags.a11yQsShortcut()) { - addQsTileToFrontOrEnd(tile, false); - } else { - synchronized (mLock) { - mHandler.obtainMessage(MSG_ADD_QS_TILE, tile).sendToTarget(); - } - } + addQsTileToFrontOrEnd(tile, false); } /** @@ -1003,13 +996,11 @@ public class CommandQueue extends IStatusBar.Stub implements */ @Override public void addQsTileToFrontOrEnd(ComponentName tile, boolean end) { - if (Flags.a11yQsShortcut()) { - synchronized (mLock) { - SomeArgs args = SomeArgs.obtain(); - args.arg1 = tile; - args.arg2 = end; - mHandler.obtainMessage(MSG_ADD_QS_TILE, args).sendToTarget(); - } + synchronized (mLock) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = tile; + args.arg2 = end; + mHandler.obtainMessage(MSG_ADD_QS_TILE, args).sendToTarget(); } } @@ -1692,18 +1683,12 @@ public class CommandQueue extends IStatusBar.Stub implements } break; case MSG_ADD_QS_TILE: { - if (Flags.a11yQsShortcut()) { - SomeArgs someArgs = (SomeArgs) msg.obj; - for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).addQsTileToFrontOrEnd( - (ComponentName) someArgs.arg1, (boolean) someArgs.arg2); - } - someArgs.recycle(); - } else { - for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).addQsTile((ComponentName) msg.obj); - } + SomeArgs someArgs = (SomeArgs) msg.obj; + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).addQsTileToFrontOrEnd( + (ComponentName) someArgs.arg1, (boolean) someArgs.arg2); } + someArgs.recycle(); break; } case MSG_REMOVE_QS_TILE: diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index 79a872edd2c5..bfd512fa6a2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -607,13 +607,6 @@ public final class KeyboardShortcutListSearch { context.getString(R.string.group_system_lock_screen), Arrays.asList( Pair.create(KeyEvent.KEYCODE_L, KeyEvent.META_META_ON))), - /* Pull up Notes app for quick memo: Meta + Ctrl + N */ - new ShortcutKeyGroupMultiMappingInfo( - context.getString(R.string.group_system_quick_memo), - Arrays.asList( - Pair.create( - KeyEvent.KEYCODE_N, - KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON))), /* Access system settings: Meta + I */ new ShortcutKeyGroupMultiMappingInfo( context.getString(R.string.group_system_access_system_settings), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index f06565f1b6d2..32da6fff6bcc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -24,7 +24,7 @@ import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_ import static android.os.Flags.allowPrivateProfile; import static android.os.UserHandle.USER_ALL; import static android.os.UserHandle.USER_NULL; -import static android.provider.Settings.Secure.REDACT_OTP_NOTIFICATION_IMMEDIATELY; +import static android.provider.Settings.Secure.OTP_NOTIFICATION_REDACTION_LOCK_TIME; import static android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS; import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS; import static android.provider.Settings.Secure.REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI; @@ -124,10 +124,10 @@ public class NotificationLockscreenUserManagerImpl implements private static final Uri REDACT_OTP_ON_WIFI = Settings.Secure.getUriFor(REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI); - private static final Uri REDACT_OTP_IMMEDIATELY = - Settings.Secure.getUriFor(REDACT_OTP_NOTIFICATION_IMMEDIATELY); + private static final Uri OTP_REDACTION_LOCK_TIME = + Settings.Secure.getUriFor(OTP_NOTIFICATION_REDACTION_LOCK_TIME); - private static final long LOCK_TIME_FOR_SENSITIVE_REDACTION_MS = + private static final long DEFAULT_LOCK_TIME_FOR_SENSITIVE_REDACTION_MS = TimeUnit.MINUTES.toMillis(10); private final Lazy<NotificationVisibilityProvider> mVisibilityProviderLazy; private final Lazy<CommonNotifCollection> mCommonNotifCollectionLazy; @@ -316,7 +316,8 @@ public class NotificationLockscreenUserManagerImpl implements protected final AtomicBoolean mConnectedToWifi = new AtomicBoolean(false); protected final AtomicBoolean mRedactOtpOnWifi = new AtomicBoolean(true); - protected final AtomicBoolean mRedactOtpImmediately = new AtomicBoolean(false); + protected final AtomicLong mOtpRedactionRequiredLockTimeMs = + new AtomicLong(DEFAULT_LOCK_TIME_FOR_SENSITIVE_REDACTION_MS); protected int mCurrentUserId = 0; @@ -375,7 +376,7 @@ public class NotificationLockscreenUserManagerImpl implements mLockScreenUris.add(SHOW_LOCKSCREEN); mLockScreenUris.add(SHOW_PRIVATE_LOCKSCREEN); mLockScreenUris.add(REDACT_OTP_ON_WIFI); - mLockScreenUris.add(REDACT_OTP_IMMEDIATELY); + mLockScreenUris.add(OTP_REDACTION_LOCK_TIME); dumpManager.registerDumpable(this); @@ -447,8 +448,8 @@ public class NotificationLockscreenUserManagerImpl implements changed |= updateUserShowPrivateSettings(user.getIdentifier()); } else if (REDACT_OTP_ON_WIFI.equals(uri)) { changed |= updateRedactOtpOnWifiSetting(); - } else if (REDACT_OTP_IMMEDIATELY.equals(uri)) { - changed |= updateRedactOtpImmediatelySetting(); + } else if (OTP_REDACTION_LOCK_TIME.equals(uri)) { + changed |= updateOtpLockTimeSetting(); } } @@ -487,7 +488,7 @@ public class NotificationLockscreenUserManagerImpl implements mLockscreenSettingsObserver ); mSecureSettings.registerContentObserverAsync( - REDACT_OTP_IMMEDIATELY, + OTP_REDACTION_LOCK_TIME, mLockscreenSettingsObserver ); @@ -638,13 +639,13 @@ public class NotificationLockscreenUserManagerImpl implements } @WorkerThread - private boolean updateRedactOtpImmediatelySetting() { - boolean originalValue = mRedactOtpImmediately.get(); - boolean newValue = mSecureSettings.getIntForUser( - REDACT_OTP_NOTIFICATION_IMMEDIATELY, - 0, - Process.myUserHandle().getIdentifier()) != 0; - mRedactOtpImmediately.set(newValue); + private boolean updateOtpLockTimeSetting() { + long originalValue = mOtpRedactionRequiredLockTimeMs.get(); + long newValue = mSecureSettings.getLongForUser( + OTP_NOTIFICATION_REDACTION_LOCK_TIME, + DEFAULT_LOCK_TIME_FOR_SENSITIVE_REDACTION_MS, + Process.myUserHandle().getIdentifier()); + mOtpRedactionRequiredLockTimeMs.set(newValue); return originalValue != newValue; } @@ -832,14 +833,9 @@ public class NotificationLockscreenUserManagerImpl implements return false; } - long latestTimeForRedaction; - if (mRedactOtpImmediately.get()) { - latestTimeForRedaction = mLastLockTime.get(); - } else { - // If the lock screen was not already locked for LOCK_TIME_FOR_SENSITIVE_REDACTION_MS - // when this notification arrived, do not redact - latestTimeForRedaction = mLastLockTime.get() + LOCK_TIME_FOR_SENSITIVE_REDACTION_MS; - } + // If the lock screen was not already locked for at least mOtpRedactionRequiredLockTimeMs + // when this notification arrived, do not redact + long latestTimeForRedaction = mLastLockTime.get() + mOtpRedactionRequiredLockTimeMs.get(); if (ent.getSbn().getPostTime() < latestTimeForRedaction) { return false; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java index 0e3f103c152e..24ab6959b9e1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java @@ -21,9 +21,14 @@ import static android.app.NotificationChannel.PROMOTIONS_ID; import static android.app.NotificationChannel.RECS_ID; import static android.app.NotificationChannel.SOCIAL_MEDIA_ID; +import android.app.Notification; +import android.content.Context; +import android.os.Build; + import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.systemui.statusbar.notification.icon.IconPack; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import java.util.List; @@ -79,6 +84,43 @@ public class BundleEntry extends PipelineEntry { public EntryAdapter getGroupRoot() { return this; } + + @Override + public boolean isClearable() { + // TODO(b/394483200): check whether all of the children are clearable, when implemented + return true; + } + + @Override + public int getTargetSdk() { + return Build.VERSION_CODES.CUR_DEVELOPMENT; + } + + @Override + public String getSummarization() { + return null; + } + + @Override + public int getContrastedColor(Context context, boolean isLowPriority, int backgroundColor) { + return Notification.COLOR_DEFAULT; + } + + @Override + public boolean canPeek() { + return false; + } + + @Override + public long getWhen() { + return 0; + } + + @Override + public IconPack getIcons() { + // TODO(b/396446620): implement bundle icons + return null; + } } public static final List<BundleEntry> ROOT_BUNDLES = List.of( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java index 4df81c97e21e..6431cacf2107 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java @@ -16,9 +16,12 @@ package com.android.systemui.statusbar.notification.collection; +import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.systemui.statusbar.notification.icon.IconPack; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; /** @@ -59,9 +62,49 @@ public interface EntryAdapter { EntryAdapter getGroupRoot(); /** + * @return whether the row can be removed with the 'Clear All' action + */ + boolean isClearable(); + + /** * Returns whether the entry is attached to the current shade list */ default boolean isAttached() { return getParent() != null; } + + /** + * Returns the target sdk of the package that owns this entry. + */ + int getTargetSdk(); + + /** + * Returns the summarization for this entry, if there is one + */ + @Nullable String getSummarization(); + + /** + * Performs any steps needed to set or reset data before an inflation or reinflation. + */ + default void prepareForInflation() {} + + /** + * Gets a color that would have sufficient contrast on the given background color. + */ + int getContrastedColor(Context context, boolean isLowPriority, int backgroundColor); + + /** + * Whether this entry can peek on screen as a heads up view + */ + boolean canPeek(); + + /** + * Returns the visible 'time', in milliseconds, of the entry + */ + long getWhen(); + + /** + * Retrieves the pack of icons associated with this entry + */ + IconPack getIcons(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 90f9525c7683..698a56363465 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -78,6 +78,7 @@ import com.android.systemui.statusbar.notification.row.NotificationGuts; import com.android.systemui.statusbar.notification.row.shared.HeadsUpStatusBarModel; import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel; import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.stack.PriorityBucket; import com.android.systemui.util.ListenerSet; @@ -309,6 +310,47 @@ public final class NotificationEntry extends ListEntry { } return null; } + + @Override + public boolean isClearable() { + return NotificationEntry.this.isClearable(); + } + + @Override + public int getTargetSdk() { + return NotificationEntry.this.targetSdk; + } + + @Override + public String getSummarization() { + return getRanking().getSummarization(); + } + + @Override + public void prepareForInflation() { + getSbn().clearPackageContext(); + } + + @Override + public int getContrastedColor(Context context, boolean isLowPriority, int backgroundColor) { + return NotificationEntry.this.getContrastedColor( + context, isLowPriority, backgroundColor); + } + + @Override + public boolean canPeek() { + return isStickyAndNotDemoted(); + } + + @Override + public long getWhen() { + return getSbn().getNotification().getWhen(); + } + + @Override + public IconPack getIcons() { + return NotificationEntry.this.getIcons(); + } } public EntryAdapter getEntryAdapter() { @@ -580,6 +622,7 @@ public final class NotificationEntry extends ListEntry { } public boolean hasFinishedInitialization() { + NotificationBundleUi.assertInLegacyMode(); return initializationTime != -1 && SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY; } @@ -663,10 +706,12 @@ public final class NotificationEntry extends ListEntry { } public void resetInitializationTime() { + NotificationBundleUi.assertInLegacyMode(); initializationTime = -1; } public void setInitializationTime(long time) { + NotificationBundleUi.assertInLegacyMode(); if (initializationTime == -1) { initializationTime = time; } @@ -683,9 +728,13 @@ public final class NotificationEntry extends ListEntry { * @return {@code true} if we are a media notification */ public boolean isMediaNotification() { - if (row == null) return false; + if (NotificationBundleUi.isEnabled()) { + return getSbn().getNotification().isMediaNotification(); + } else { + if (row == null) return false; - return row.isMediaRow(); + return row.isMediaRow(); + } } public boolean containsCustomViews() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java index 9c1d0735a65b..ac11c15e8bf8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java @@ -67,6 +67,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable; import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt; import com.android.systemui.util.Assert; import com.android.systemui.util.NamedListenerSet; @@ -1282,7 +1283,13 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { entry.getAttachState().setExcludingFilter(filter); if (filter != null) { // notification is removed from the list, so we reset its initialization time - entry.resetInitializationTime(); + if (NotificationBundleUi.isEnabled()) { + if (entry.getRow() != null) { + entry.getRow().resetInitializationTime(); + } + } else { + entry.resetInitializationTime(); + } } return filter != null; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt index dba8a9a6ddec..867db8ec8235 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt @@ -163,6 +163,7 @@ constructor( if (entry in nextMap) nextMap.remove(entry) if (entry in nextList) nextList.remove(entry) + outcome = "add next" addToNext(entry, runnable) // Shorten headsUpEntryShowing display time @@ -174,7 +175,7 @@ constructor( headsUpEntryShowing!!.updateEntry( /* updatePostTime= */ false, /* updateEarliestRemovalTime= */ false, - /* reason= */ "avalanche duration update", + /* reason= */ "shorten duration of previously-last HUN", ) } } @@ -269,8 +270,10 @@ constructor( } nextList.sort() val entryList = showingList + nextList + val thisKey = getKey(entry) if (entryList.isEmpty()) { - log { "No avalanche HUNs, use default ms: $autoDismissMs" } + headsUpManagerLogger.logAvalancheDuration( + thisKey, autoDismissMs, "No avalanche HUNs, use default", nextKey = "") return autoDismissMs } // entryList.indexOf(entry) returns -1 even when the entry is in entryList @@ -281,27 +284,29 @@ constructor( } } if (thisEntryIndex == -1) { - log { "Untracked entry, use default ms: $autoDismissMs" } + headsUpManagerLogger.logAvalancheDuration( + thisKey, autoDismissMs, "Untracked entry, use default", nextKey = "") return autoDismissMs } val nextEntryIndex = thisEntryIndex + 1 - - // If last entry, use default duration if (nextEntryIndex >= entryList.size) { - log { "Last entry, use default ms: $autoDismissMs" } + headsUpManagerLogger.logAvalancheDuration( + thisKey, autoDismissMs, "Last entry, use default", nextKey = "") return autoDismissMs } val nextEntry = entryList[nextEntryIndex] + val nextKey = getKey(nextEntry) if (nextEntry.compareNonTimeFields(entry) == -1) { - // Next entry is higher priority - log { "Next entry is higher priority: 500ms" } + headsUpManagerLogger.logAvalancheDuration( + thisKey, 500, "LOWER priority than next: ", nextKey) return 500 } else if (nextEntry.compareNonTimeFields(entry) == 0) { - // Next entry is same priority - log { "Next entry is same priority: 1000ms" } + headsUpManagerLogger.logAvalancheDuration( + thisKey, 1000, "SAME priority as next: ", nextKey) return 1000 } else { - log { "Next entry is lower priority, use default ms: $autoDismissMs" } + headsUpManagerLogger.logAvalancheDuration( + thisKey, autoDismissMs, "HIGHER priority than next: ", nextKey) return autoDismissMs } } @@ -355,25 +360,28 @@ constructor( } private fun showNow(entry: HeadsUpEntry, runnableList: MutableList<Runnable>) { - log { "SHOW: " + getKey(entry) } - + headsUpManagerLogger.logAvalancheStage("show", getKey(entry)) uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_SHOWN) headsUpEntryShowing = entry - runnableList.forEach { - if (it in debugRunnableLabelMap) { - log { "RUNNABLE: ${debugRunnableLabelMap[it]}" } + runnableList.forEach { runnable -> + if (debug) { + debugRunnableLabelMap[runnable]?.let { label -> + headsUpManagerLogger.logAvalancheStage("run", label) + // Remove label after logging to avoid memory leak + debugRunnableLabelMap.remove(runnable) + } } - it.run() + runnable.run() } } private fun showNext() { - log { "SHOW NEXT" } + headsUpManagerLogger.logAvalancheStage("show next", key = "") headsUpEntryShowing = null if (nextList.isEmpty()) { - log { "NO MORE TO SHOW" } + headsUpManagerLogger.logAvalancheStage("no more", key = "") previousHunKey = "" return } @@ -395,11 +403,7 @@ constructor( debugRunnableLabelMap.remove(r) } } - val queue = ArrayList<String>() - for (entry in listToDrop) { - queue.add("[${getKey(entry)}]") - } - val dropList = java.lang.String.join("\n", queue) + val dropList = listToDrop.joinToString("\n ") { getKey(it) } headsUpManagerLogger.logDroppedHuns(dropList) } clearNext() @@ -424,38 +428,24 @@ constructor( // Methods below are for logging only ========================================================== - private inline fun log(s: () -> String) { - if (debug) { - Log.d(tag, s()) - } - } - private fun getStateStr(): String { - return "\navalanche state:" + - "\n\tshowing: [${getKey(headsUpEntryShowing)}]" + - "\n\tprevious: [$previousHunKey]" + - "\n\tnext list: $nextListStr" + - "\n\tnext map: $nextMapStr" + - "\nBHUM.mHeadsUpEntryMap: " + - baseEntryMapStr() + return "\n[AC state]" + + "\nshow: ${getKey(headsUpEntryShowing)}" + + "\nprevious: $previousHunKey" + + "\n$nextStr" + + "\n[HeadsUpManagerImpl.mHeadsUpEntryMap] " + baseEntryMapStr() + "\n" } - private val nextListStr: String + private val nextStr: String get() { - val queue = ArrayList<String>() - for (entry in nextList) { - queue.add("[${getKey(entry)}]") + val nextListStr = nextList.joinToString("\n ") { getKey(it) } + if (nextList.toSet() == nextMap.keys.toSet()) { + return "next (${nextList.size}):\n $nextListStr" } - return java.lang.String.join("\n", queue) - } - - private val nextMapStr: String - get() { - val queue = ArrayList<String>() - for (entry in nextMap.keys) { - queue.add("[${getKey(entry)}]") - } - return java.lang.String.join("\n", queue) + // This should never happen + val nextMapStr = nextMap.keys.joinToString("\n ") { getKey(it) } + return "next list (${nextList.size}):\n $nextListStr" + + "\nnext map (${nextMap.size}):\n $nextMapStr" } fun getKey(entry: HeadsUpEntry?): String { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java index 7d74a496853f..87b9bf87c680 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java @@ -856,11 +856,11 @@ public class HeadsUpManagerImpl private String getEntryMapStr() { if (mHeadsUpEntryMap.isEmpty()) { - return "EMPTY"; + return ""; } StringBuilder entryMapStr = new StringBuilder(); for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) { - entryMapStr.append("\n\t").append( + entryMapStr.append("\n ").append( entry.mEntry == null ? "null" : entry.mEntry.getKey()); } return entryMapStr.toString(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt index 65fb9ca558d7..388d357b3b15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt @@ -71,7 +71,7 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { str3 = outcome bool1 = isEnabled }, - { "$str1\n\t=> AC[isEnabled:$bool1] update: $str2\n\t=> $str3" }, + { "$str1\n=> AC[enabled:$bool1] update: $str2\n=> $str3" }, ) } @@ -90,7 +90,33 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { str3 = outcome bool1 = isEnabled }, - { "$str1\n\t=> AC[isEnabled:$bool1] delete: $str2\n\t=> $str3" }, + { "$str1\n=> AC[enabled:$bool1] delete: $str2\n=> $str3" }, + ) + } + + fun logAvalancheStage(stage: String, key: String) { + buffer.log( + TAG, + INFO, + { + str1 = stage + str2 = key + }, + { "[AC] $str1 $str2" }, + ) + } + + fun logAvalancheDuration(thisKey: String, duration: Int, reason: String, nextKey: String) { + buffer.log( + TAG, + INFO, + { + str1 = thisKey + int1 = duration + str2 = reason + str3 = nextKey + }, + { "[AC] $str1 | $int1 ms | $str2 $str3" }, ) } @@ -325,7 +351,7 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { } fun logDroppedHuns(entryList: String) { - buffer.log(TAG, VERBOSE, { str1 = entryList }, { "[AC] Drop HUNs: $str1" }) + buffer.log(TAG, VERBOSE, { str1 = entryList }, { "[AC] dropped:\n $str1" }) } } 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 b4092cd785bf..b0773276a3e9 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 @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.row; import static com.android.systemui.Flags.notificationBackgroundTintOptimization; +import static com.android.systemui.Flags.notificationRowTransparency; import static com.android.systemui.statusbar.notification.row.ExpandableView.ClipSide.BOTTOM; import static com.android.systemui.statusbar.notification.row.ExpandableView.ClipSide.TOP; @@ -38,6 +39,7 @@ import com.android.app.animation.Interpolators; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor.Configuration; import com.android.systemui.Gefingerpoken; +import com.android.systemui.common.shared.colors.SurfaceEffectColors; import com.android.systemui.res.R; import com.android.systemui.shade.TouchLogger; import com.android.systemui.statusbar.NotificationShelf; @@ -122,8 +124,12 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } private void updateColors() { - mNormalColor = mContext.getColor( - com.android.internal.R.color.materialColorSurfaceContainerHigh); + if (notificationRowTransparency()) { + mNormalColor = SurfaceEffectColors.surfaceEffect1(getResources()); + } else { + mNormalColor = mContext.getColor( + com.android.internal.R.color.materialColorSurfaceContainerHigh); + } mTintedRippleColor = mContext.getColor( R.color.notification_ripple_tinted_color); mNormalRippleColor = mContext.getColor( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 66a0fb4ee4ab..6f7eade1c0f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -21,6 +21,7 @@ import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_ import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static com.android.systemui.Flags.notificationsPinnedHunInShade; +import static com.android.systemui.Flags.notificationRowTransparency; import static com.android.systemui.flags.Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE; import static com.android.systemui.statusbar.notification.NotificationUtils.logKey; import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED; @@ -45,6 +46,7 @@ import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; +import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; @@ -116,7 +118,6 @@ import com.android.systemui.statusbar.notification.headsup.PinnedStatus; import com.android.systemui.statusbar.notification.logging.NotificationCounters; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; -import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction; import com.android.systemui.statusbar.notification.row.wrapper.NotificationCompactMessagingTemplateViewWrapper; @@ -168,6 +169,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private static final long RECENTLY_ALERTED_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30); private static final SourceType BASE_VALUE = SourceType.from("BaseValue"); private static final SourceType FROM_PARENT = SourceType.from("FromParent(ENR)"); + private static final long INITIALIZATION_DELAY = 400; // We don't correctly track dark mode until the content views are inflated, so always update // the background on first content update just in case it happens to be during a theme change. @@ -266,7 +268,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private NotificationContentView mPublicLayout; private NotificationContentView mPrivateLayout; private NotificationContentView[] mLayouts; - private int mNotificationColor; private ExpandableNotificationRowLogger mLogger; private String mLoggingKey; private NotificationGuts mGuts; @@ -351,6 +352,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView */ private boolean mSaveSpaceOnLockscreen; + // indicates when this view was first attached to a window + // this value will reset when the view is completely removed from the shade (ie: filtered out) + private long initializationTime = -1; + /** * It is added for unit testing purpose. * Please do not use it for other purposes. @@ -624,26 +629,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** - * Marks a content view as freeable, setting it so that future inflations do not reinflate - * and ensuring that the view is freed when it is safe to remove. - * - * @param inflationFlag flag corresponding to the content view to be freed - * @deprecated By default, {@link NotificationRowContentBinder#unbindContent} will tell the - * view hierarchy to only free when the view is safe to remove so this method is no longer - * needed. Will remove when all uses are gone. - */ - @Deprecated - public void freeContentViewWhenSafe(@InflationFlag int inflationFlag) { - RowContentBindParams params = mRowContentBindStage.getStageParams(mEntry); - params.markContentViewsFreeable(inflationFlag); - mRowContentBindStage.requestRebind(mEntry, null /* callback */); - } - - /** * Returns whether this row is considered non-blockable (i.e. it's a non-blockable system notif * or is in an allowList). */ public boolean getIsNonblockable() { + NotificationBundleUi.assertInLegacyMode(); if (mEntry == null) { return true; } @@ -665,9 +655,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView l.onNotificationUpdated(mEntry); } mShowingPublicInitialized = false; - updateNotificationColor(); if (mMenuRow != null) { - mMenuRow.onNotificationUpdated(mEntry.getSbn()); + mMenuRow.onNotificationUpdated(); mMenuRow.setAppName(mAppName); } if (mIsSummaryWithChildren) { @@ -726,15 +715,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** - * Called when the notification's ranking was changed (but nothing else changed). - */ - public void onNotificationRankingUpdated() { - if (mMenuRow != null) { - mMenuRow.onNotificationUpdated(mEntry.getSbn()); - } - } - - /** * Call when bubble state has changed and the button on the notification should be updated. */ public void updateBubbleButton() { @@ -745,7 +725,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @VisibleForTesting void updateShelfIconColor() { - StatusBarIconView expandedIcon = mEntry.getIcons().getShelfIcon(); + StatusBarIconView expandedIcon = getShelfIcon(); boolean isPreL = Boolean.TRUE.equals(expandedIcon.getTag(R.id.icon_is_pre_L)); boolean colorize = !isPreL || NotificationUtils.isGrayscale(expandedIcon, ContrastColorUtil.getInstance(mContext)); @@ -766,8 +746,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (color != Notification.COLOR_INVALID) { return color; } else { - return mEntry.getContrastedColor(mContext, mIsMinimized && !isExpanded(), - getBackgroundColorWithoutTint()); + if (NotificationBundleUi.isEnabled()) { + return mEntryAdapter.getContrastedColor(mContext, mIsMinimized && !isExpanded(), + getBackgroundColorWithoutTint()); + } else { + return mEntry.getContrastedColor(mContext, mIsMinimized && !isExpanded(), + getBackgroundColorWithoutTint()); + } } } @@ -869,15 +854,29 @@ public class ExpandableNotificationRow extends ActivatableNotificationView boolean customView = contractedView != null && contractedView.getId() != com.android.internal.R.id.status_bar_latest_event_content; - boolean beforeN = mEntry.targetSdk < Build.VERSION_CODES.N; - boolean beforeP = mEntry.targetSdk < Build.VERSION_CODES.P; - boolean beforeS = mEntry.targetSdk < Build.VERSION_CODES.S; + int targetSdk = Build.VERSION_CODES.CUR_DEVELOPMENT; + if (NotificationBundleUi.isEnabled()) { + targetSdk = mEntryAdapter.getTargetSdk(); + } else { + targetSdk = mEntry.targetSdk; + } + + boolean beforeN = targetSdk < Build.VERSION_CODES.N; + boolean beforeP = targetSdk < Build.VERSION_CODES.P; + boolean beforeS = targetSdk < Build.VERSION_CODES.S; int smallHeight; boolean isCallLayout = contractedView instanceof CallLayout; boolean isMessagingLayout = contractedView instanceof MessagingLayout || contractedView instanceof ConversationLayout; + String summarization = null; + if (NotificationBundleUi.isEnabled()) { + summarization = mEntryAdapter.getSummarization(); + } else { + summarization = mEntry.getRanking().getSummarization(); + } + if (customView && beforeS && !mIsSummaryWithChildren) { if (beforeN) { smallHeight = mMaxSmallHeightBeforeN; @@ -890,7 +889,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView smallHeight = maxExpandedHeight; } else if (NmSummarizationUiFlag.isEnabled() && isMessagingLayout - && !TextUtils.isEmpty(mEntry.getRanking().getSummarization())) { + && !TextUtils.isEmpty(summarization)) { smallHeight = mMaxSmallHeightWithSummarization; } else { smallHeight = mMaxSmallHeight; @@ -1523,7 +1522,22 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public boolean hasFinishedInitialization() { - return getEntry().hasFinishedInitialization(); + if (NotificationBundleUi.isEnabled()) { + return initializationTime != -1 + && SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY; + } else { + return getEntry().hasFinishedInitialization(); + } + } + + public void resetInitializationTime() { + initializationTime = -1; + } + + public void setInitializationTime(long time) { + if (initializationTime == -1) { + initializationTime = time; + } } /** @@ -1538,7 +1552,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return null; } if (mMenuRow.getMenuView() == null) { - mMenuRow.createMenu(this, mEntry.getSbn()); + mMenuRow.createMenu(this); mMenuRow.setAppName(mAppName); FrameLayout.LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); @@ -1580,7 +1594,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (oldMenu != null) { int menuIndex = indexOfChild(oldMenu); removeView(oldMenu); - mMenuRow.createMenu(ExpandableNotificationRow.this, mEntry.getSbn()); + mMenuRow.createMenu(ExpandableNotificationRow.this); mMenuRow.setAppName(mAppName); addView(mMenuRow.getMenuView(), menuIndex); } @@ -1588,12 +1602,17 @@ public class ExpandableNotificationRow extends ActivatableNotificationView l.reinflate(); l.reInflateViews(); } - mEntry.getSbn().clearPackageContext(); + if (NotificationBundleUi.isEnabled()) { + mEntryAdapter.prepareForInflation(); + } else { + mEntry.getSbn().clearPackageContext(); + } // TODO: Move content inflation logic out of this call RowContentBindParams params = mRowContentBindStage.getStageParams(mEntry); params.setNeedsReinflation(true); - var rebindEndCallback = mRebindingTracker.trackRebinding(mEntry.getKey()); + var rebindEndCallback = mRebindingTracker.trackRebinding(NotificationBundleUi.isEnabled() + ? mEntryAdapter.getKey() : mEntry.getKey()); mRowContentBindStage.requestRebind(mEntry, (e) -> rebindEndCallback.onFinished()); Trace.endSection(); } @@ -1636,6 +1655,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (view != null) { view.setBackgroundTintColor(color); } + if (notificationRowTransparency() + && (mBackgroundNormal != null) + && (mEntry != null)) { + mBackgroundNormal.setBgIsColorized( + mEntry.getSbn().getNotification().isColorized()); + } } public void closeRemoteInput() { @@ -1651,20 +1676,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mPrivateLayout.setSingleLineWidthIndention(indention); } - public int getNotificationColor() { - return mNotificationColor; - } - - public void updateNotificationColor() { - Configuration currentConfig = getResources().getConfiguration(); - boolean nightMode = (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) - == Configuration.UI_MODE_NIGHT_YES; - - mNotificationColor = ContrastColorUtil.resolveContrastColor(mContext, - mEntry.getSbn().getNotification().color, - getBackgroundColorWithoutTint(), nightMode); - } - public HybridNotificationView getSingleLineView() { return mPrivateLayout.getSingleLineView(); } @@ -2253,6 +2264,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mEntry = entry; } + @VisibleForTesting + protected void setEntryAdapter(EntryAdapter entry) { + mEntryAdapter = entry; + } + private final Runnable mExpireRecentlyAlertedFlag = () -> applyAudiblyAlertedRecently(false); private void applyAudiblyAlertedRecently(boolean audiblyAlertedRecently) { @@ -2490,7 +2506,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mTranslateableViews.get(i).setTranslationX(0); } invalidateOutline(); - getEntry().getIcons().getShelfIcon().setScrollX(0); + getShelfIcon().setScrollX(0); } if (mMenuRow != null) { @@ -2609,7 +2625,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView // In order to keep the shelf in sync with this swiping, we're simply translating // it's icon by the same amount. The translation is already being used for the normal // positioning, so we can use the scrollX instead. - getEntry().getIcons().getShelfIcon().setScrollX((int) -translationX); + getShelfIcon().setScrollX((int) -translationX); } if (mMenuRow != null && mMenuRow.getMenuView() != null) { @@ -2836,7 +2852,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public @NonNull StatusBarIconView getShelfIcon() { - return getEntry().getIcons().getShelfIcon(); + if (NotificationBundleUi.isEnabled()) { + return getEntryAdapter().getIcons().getShelfIcon(); + } else { + return mEntry.getIcons().getShelfIcon(); + } } @Override @@ -3065,8 +3085,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * except for legacy use cases. */ public boolean canShowHeadsUp() { + boolean canEntryHun = NotificationBundleUi.isEnabled() + ? mEntryAdapter.canPeek() + : mEntry.isStickyAndNotDemoted(); if (mOnKeyguard && !isDozing() && !isBypassEnabled() && - (!mEntry.isStickyAndNotDemoted() + (!canEntryHun || (!mIgnoreLockscreenConstraints && mSaveSpaceOnLockscreen))) { return false; } @@ -3105,7 +3128,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } if (!mIsSummaryWithChildren && wasSummary) { // Reset the 'when' once the row stops being a summary - mPublicLayout.setNotificationWhen(mEntry.getSbn().getNotification().getWhen()); + if (NotificationBundleUi.isEnabled()) { + mPublicLayout.setNotificationWhen(mEntryAdapter.getWhen()); + } else { + mPublicLayout.setNotificationWhen(mEntry.getSbn().getNotification().getWhen()); + } } getShowingLayout().updateBackgroundColor(false /* animate */); mPrivateLayout.updateExpandButtons(isExpandable()); @@ -3374,7 +3401,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView */ @Override public boolean canExpandableViewBeDismissed() { - if (areGutsExposed() || !mEntry.hasFinishedInitialization()) { + if (areGutsExposed() || !hasFinishedInitialization()) { return false; } return canViewBeDismissed(); @@ -3398,7 +3425,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * clearability see {@link NotificationEntry#isClearable()}. */ public boolean canViewBeCleared() { - return mEntry.isClearable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral); + if (NotificationBundleUi.isEnabled()) { + return mEntryAdapter.isClearable() + && (!shouldShowPublic() || !mSensitiveHiddenInGeneral); + } else { + return mEntry.isClearable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral); + } } private boolean shouldShowPublic() { @@ -4075,6 +4107,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public boolean isMediaRow() { + NotificationBundleUi.assertInLegacyMode(); return mEntry.getSbn().getNotification().isMediaNotification(); } @@ -4197,7 +4230,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public void dump(PrintWriter pwOriginal, String[] args) { IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal); // Skip super call; dump viewState ourselves - pw.println("Notification: " + mEntry.getKey()); + if (NotificationBundleUi.isEnabled()) { + pw.println("Notification: " + mEntryAdapter.getKey()); + } else { + pw.println("Notification: " + mEntry.getKey()); + } DumpUtilsKt.withIncreasedIndent(pw, () -> { pw.println(this); pw.print("visibility: " + getVisibility()); @@ -4228,6 +4265,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView pw.print(", isPinned: " + isPinned()); pw.print(", expandedWhenPinned: " + mExpandedWhenPinned); pw.print(", isMinimized: " + mIsMinimized); + pw.print(", isAboveShelf: " + isAboveShelf()); pw.println(); if (NotificationContentView.INCLUDE_HEIGHTS_TO_DUMP) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index b43a387a5edb..07711b6e0eb8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -374,7 +374,11 @@ public class ExpandableNotificationRowController implements NotifViewController mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { - mView.getEntry().setInitializationTime(mClock.elapsedRealtime()); + if (NotificationBundleUi.isEnabled()) { + mView.setInitializationTime(mClock.elapsedRealtime()); + } else { + mView.getEntry().setInitializationTime(mClock.elapsedRealtime()); + } mPluginManager.addPluginListener(mView, NotificationMenuRowPlugin.class, false /* Allow multiple */); if (!SceneContainerFlag.isEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java index dd3a9c9dcf21..33c36d8c4c76 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row; +import static com.android.systemui.Flags.notificationRowTransparency; import static com.android.systemui.util.ColorUtilKt.hexColorString; import android.content.Context; @@ -23,6 +24,7 @@ import android.content.res.ColorStateList; import android.graphics.Canvas; import android.graphics.Path; import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; @@ -34,8 +36,10 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.graphics.ColorUtils; import com.android.internal.util.ContrastColorUtil; import com.android.systemui.Dumpable; +import com.android.systemui.common.shared.colors.SurfaceEffectColors; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.shared.NotificationAddXOnHoverToDismiss; import com.android.systemui.util.DrawableDumpKt; @@ -69,6 +73,7 @@ public class NotificationBackgroundView extends View implements Dumpable, private final ColorStateList mLightColoredStatefulColors; private final ColorStateList mDarkColoredStatefulColors; private final int mNormalColor; + private boolean mBgIsColorized = false; private final int convexR = 9; private final int concaveR = 22; @@ -82,8 +87,12 @@ public class NotificationBackgroundView extends View implements Dumpable, R.color.notification_state_color_light); mDarkColoredStatefulColors = getResources().getColorStateList( R.color.notification_state_color_dark); - mNormalColor = mContext.getColor( - com.android.internal.R.color.materialColorSurfaceContainerHigh); + if (notificationRowTransparency()) { + mNormalColor = SurfaceEffectColors.surfaceEffect1(getResources()); + } else { + mNormalColor = mContext.getColor( + com.android.internal.R.color.materialColorSurfaceContainerHigh); + } mFocusOverlayStroke = getResources().getDimension(R.dimen.notification_focus_stroke_width); } @@ -132,6 +141,21 @@ public class NotificationBackgroundView extends View implements Dumpable, } } + /** + * A way to tell whether the background has been colorized. + */ + public boolean isColorized() { + return mBgIsColorized; + } + + /** + * A way to inform this class whether the background has been colorized. + * We need to know this, in order to *not* override that color. + */ + public void setBgIsColorized(boolean b) { + mBgIsColorized = b; + } + private Path calculateDismissButtonCutoutPath(Rect backgroundBounds) { // TODO(b/365585705): Adapt to RTL after the UX design is finalized. @@ -280,7 +304,7 @@ public class NotificationBackgroundView extends View implements Dumpable, setCustomBackground(d); } - private Drawable getBaseBackgroundLayer() { + public Drawable getBaseBackgroundLayer() { return ((LayerDrawable) mBackground).getDrawable(0); } @@ -288,11 +312,27 @@ public class NotificationBackgroundView extends View implements Dumpable, return ((LayerDrawable) mBackground).getDrawable(1); } + private void updateBaseLayerColor() { + // BG base layer being a drawable, there isn't a method like setColor() to color it. + // Instead, we set a color filter that essentially replaces every pixel of the drawable. + // For non-colorized notifications, this function specifies a new color token. + // For colorized notifications, this uses a color that matches the tint color at 90% alpha. + getBaseBackgroundLayer().setColorFilter( + new PorterDuffColorFilter( + isColorized() + ? ColorUtils.setAlphaComponent(mTintColor, (int) (255 * 0.9f)) + : SurfaceEffectColors.surfaceEffect1(getResources()), + PorterDuff.Mode.SRC)); // SRC operator discards the drawable's color+alpha + } + public void setTint(int tintColor) { Drawable baseLayer = getBaseBackgroundLayer(); baseLayer.mutate().setTintMode(PorterDuff.Mode.SRC_ATOP); baseLayer.setTint(tintColor); mTintColor = tintColor; + if (notificationRowTransparency()) { + updateBaseLayerColor(); + } setStatefulColors(); invalidate(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 6e638f5de209..9a75253295d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -73,6 +73,7 @@ import com.android.systemui.statusbar.notification.collection.render.NotifGutsVi import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.row.icon.AppIconProvider; import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.policy.DeviceProvisionedController; @@ -436,7 +437,9 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta onNasFeedbackClick, mUiEventLogger, mDeviceProvisionedController.isDeviceProvisioned(), - row.getIsNonblockable(), + NotificationBundleUi.isEnabled() + ? !row.getEntry().isBlockable() + : row.getIsNonblockable(), mHighPriorityProvider.isHighPriority(row.getEntry()), mAssistantFeedbackController, mMetricsLogger, @@ -480,7 +483,9 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta row.getEntry(), onSettingsClick, mDeviceProvisionedController.isDeviceProvisioned(), - row.getIsNonblockable()); + NotificationBundleUi.isEnabled() + ? !row.getEntry().isBlockable() + : row.getIsNonblockable()); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java index b96b224a7d2e..ab382df13d10 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java @@ -186,7 +186,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl } @Override - public void createMenu(ViewGroup parent, StatusBarNotification sbn) { + public void createMenu(ViewGroup parent) { mParent = (ExpandableNotificationRow) parent; createMenuViews(true /* resetState */); } @@ -227,7 +227,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl } @Override - public void onNotificationUpdated(StatusBarNotification sbn) { + public void onNotificationUpdated() { if (mMenuContainer == null) { // Menu hasn't been created yet, no need to do anything. return; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 3d60e03d7ca4..3ff18efeae53 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -6686,7 +6686,7 @@ public class NotificationStackScrollLayout static boolean canChildBeCleared(View v) { if (v instanceof ExpandableNotificationRow row) { - if (row.areGutsExposed() || !row.getEntry().hasFinishedInitialization()) { + if (row.areGutsExposed() || !row.hasFinishedInitialization()) { return false; } return row.canViewBeCleared(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 06b989aaab57..08692bea2292 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -1227,13 +1227,17 @@ public class StackScrollAlgorithm { float baseZ = ambientState.getBaseZHeight(); if (SceneContainerFlag.isEnabled()) { - // SceneContainer flags off this logic, and just sets the baseZ because: + // SceneContainer simplifies this logic, because: // - there are no overlapping HUNs anymore, no need for multiplying their shadows // - shadows for HUNs overlapping with the stack are now set from updateHeadsUpStates - // - shadows for HUNs overlapping with the shelf are NOT set anymore, because it only - // happens on AOD/Pulsing, where they're displayed on a black background so a shadow - // wouldn't be visible. - childViewState.setZTranslation(baseZ); + if (child.isPinned() || ambientState.getTrackedHeadsUpRow() == child) { + // set a default elevation on the HUN, which would be overridden + // from updateHeadsUpStates if it is displayed in the shade + childViewState.setZTranslation(baseZ + mPinnedZTranslationExtra); + } else { + // set baseZ for every notification + childViewState.setZTranslation(baseZ); + } } else { if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible && !ambientState.isDozingAndNotPulsing(child) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index c0ebe7dd7023..5c81c8e22bfc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -49,6 +49,7 @@ import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.DozingToDreamingTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel @@ -139,6 +140,7 @@ constructor( private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel, private val aodToGlanceableHubTransitionViewModel: AodToGlanceableHubTransitionViewModel, private val aodToPrimaryBouncerTransitionViewModel: AodToPrimaryBouncerTransitionViewModel, + private val dozingToDreamingTransitionViewModel: DozingToDreamingTransitionViewModel, dozingToGlanceableHubTransitionViewModel: DozingToGlanceableHubTransitionViewModel, private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel, @@ -577,6 +579,7 @@ constructor( aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), aodToGlanceableHubTransitionViewModel.lockscreenAlpha(viewState), aodToPrimaryBouncerTransitionViewModel.notificationAlpha, + dozingToDreamingTransitionViewModel.notificationAlpha, dozingToLockscreenTransitionViewModel.lockscreenAlpha, dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState), dozingToPrimaryBouncerTransitionViewModel.notificationAlpha, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java index 4ff09d3bc6af..e8b50d580c33 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java @@ -45,6 +45,9 @@ import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import android.os.SystemClock; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.ArrayMap; @@ -74,10 +77,14 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable; import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener; +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.row.NotificationTestHelper; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.util.time.FakeSystemClock; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -127,6 +134,9 @@ public class ShadeListBuilderTest extends SysuiTestCase { private TestableStabilityManager mStabilityManager; private TestableNotifFilter mFinalizeFilter; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private Map<String, Integer> mNextIdMap = new ArrayMap<>(); private int mNextRank = 0; @@ -561,6 +571,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) public void testFilter_resetsInitalizationTime() { // GIVEN a NotifFilter that filters out a specific package NotifFilter filter1 = spy(new PackageFilter(PACKAGE_1)); @@ -584,6 +595,31 @@ public class ShadeListBuilderTest extends SysuiTestCase { } @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void testFilter_resetsInitializationTime_onRow() throws Exception { + // GIVEN a NotifFilter that filters out a specific package + NotifFilter filter1 = spy(new PackageFilter(PACKAGE_1)); + mListBuilder.addFinalizeFilter(filter1); + + // GIVEN a notification that was initialized 1 second ago that will be filtered out + final NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(PACKAGE_1) + .setId(nextId(PACKAGE_1)) + .setRank(nextRank()) + .build(); + entry.setRow(new NotificationTestHelper(mContext, mDependency).createRow()); + entry.getRow().setInitializationTime(SystemClock.elapsedRealtime() - 1000); + assertTrue(entry.getRow().hasFinishedInitialization()); + + // WHEN the pipeline is kicked off + mReadyForBuildListener.onBuildList(singletonList(entry), "test"); + mPipelineChoreographer.runIfScheduled(); + + // THEN the entry's initialization time is reset + assertFalse(entry.getRow().hasFinishedInitialization()); + } + + @Test public void testNotifFiltersCanBePreempted() { // GIVEN two notif filters NotifFilter filter1 = spy(new PackageFilter(PACKAGE_2)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index 1b5353127f25..24d8d1cc8fae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -75,6 +75,7 @@ import com.android.systemui.statusbar.notification.headsup.PinnedStatus; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization; import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -589,6 +590,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) public void testGetIsNonblockable() throws Exception { ExpandableNotificationRow row = mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt index 47238fedee4d..c874bc6056c6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt @@ -36,6 +36,7 @@ import com.android.internal.widget.NotificationActionListLayout import com.android.internal.widget.NotificationExpandButton import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.notification.FeedbackIcon +import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier import com.android.systemui.statusbar.notification.shared.NotificationBundleUi @@ -82,6 +83,7 @@ class NotificationContentViewTest : SysuiTestCase() { fakeParent = spy(FrameLayout(mContext, /* attrs= */ null).also { it.visibility = View.GONE }) val mockEntry = createMockNotificationEntry() + val mockEntryAdapter = createMockNotificationEntryAdapter() row = spy( when (NotificationBundleUi.isEnabled) { @@ -92,6 +94,7 @@ class NotificationContentViewTest : SysuiTestCase() { UserHandle.CURRENT ).apply { entry = mockEntry + entryAdapter = mockEntryAdapter } } false -> { @@ -611,6 +614,7 @@ class NotificationContentViewTest : SysuiTestCase() { whenever(this.entry).thenReturn(notificationEntry) whenever(this.context).thenReturn(mContext) whenever(this.bubbleClickListener).thenReturn(View.OnClickListener {}) + whenever(this.entryAdapter).thenReturn(createMockNotificationEntryAdapter()) } private fun createMockNotificationEntry() = @@ -624,6 +628,9 @@ class NotificationContentViewTest : SysuiTestCase() { whenever(sbnMock.user).thenReturn(userMock) } + private fun createMockNotificationEntryAdapter() = + mock<EntryAdapter>() + private fun createLinearLayoutWithBottomMargin(bottomMargin: Int): LinearLayout { val outerLayout = LinearLayout(mContext) val innerLayout = LinearLayout(mContext) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt index 3d4c90140adb..99b99ee1860b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt @@ -427,7 +427,6 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE) .setImportance(NotificationManager.IMPORTANCE_HIGH) .build() - whenever(row.getIsNonblockable()).thenReturn(false) whenever(highPriorityProvider.isHighPriority(entry)).thenReturn(true) val statusBarNotification = entry.sbn gutsManager.initializeNotificationInfo(row, notificationInfoView) @@ -463,7 +462,6 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { NotificationEntryHelper.modifyRanking(row.entry) .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE) .build() - whenever(row.getIsNonblockable()).thenReturn(false) val statusBarNotification = row.entry.sbn val entry = row.entry gutsManager.initializeNotificationInfo(row, notificationInfoView) @@ -499,7 +497,6 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { NotificationEntryHelper.modifyRanking(row.entry) .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE) .build() - whenever(row.getIsNonblockable()).thenReturn(false) val statusBarNotification = row.entry.sbn val entry = row.entry gutsManager.initializeNotificationInfo(row, notificationInfoView) @@ -566,7 +563,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { ): NotificationMenuRowPlugin.MenuItem { val menuRow: NotificationMenuRowPlugin = NotificationMenuRow(mContext, peopleNotificationIdentifier) - menuRow.createMenu(row, row!!.entry.sbn) + menuRow.createMenu(row) val menuItem = menuRow.getLongpressMenuItem(mContext) Assert.assertNotNull(menuItem) return menuItem diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 6ac20d40f2dc..955de273c426 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -675,7 +675,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { @Test public void testClearNotifications_clearAllInProgress() { ExpandableNotificationRow row = createClearableRow(); - when(row.getEntry().hasFinishedInitialization()).thenReturn(true); + when(row.hasFinishedInitialization()).thenReturn(true); doReturn(true).when(mStackScroller).isVisible(row); mStackScroller.addContainerView(row); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt index 8d4db8b74061..8a6f68c5da68 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.panels.domain.interactor +import com.android.internal.logging.uiEventLoggerFake import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.log.core.FakeLogBuffer @@ -29,6 +30,7 @@ val Kosmos.iconTilesInteractor by defaultLargeTilesRepository, currentTilesInteractor, qsPreferencesInteractor, + uiEventLoggerFake, largeTileSpanRepository, FakeLogBuffer.Factory.create(), applicationCoroutineScope, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorKosmos.kt index 6fd7cf6edbe4..2ea2119970ad 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorKosmos.kt @@ -16,14 +16,18 @@ package com.android.systemui.shade.domain.interactor +import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.kosmos.Kosmos import com.android.systemui.plugins.activityStarter import com.android.systemui.shade.data.repository.shadeHeaderClockRepository +import com.android.systemui.util.time.systemClock var Kosmos.shadeHeaderClockInteractor: ShadeHeaderClockInteractor by Kosmos.Fixture { ShadeHeaderClockInteractor( repository = shadeHeaderClockRepository, activityStarter = activityStarter, + broadcastDispatcher = broadcastDispatcher, + systemClock = systemClock, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt index 08de73be1128..34e5bfde43c9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt @@ -18,7 +18,6 @@ package com.android.systemui.shade.ui.viewmodel import android.content.applicationContext import com.android.systemui.battery.batteryMeterViewControllerFactory -import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.kosmos.Kosmos import com.android.systemui.plugins.activityStarter import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -26,7 +25,6 @@ import com.android.systemui.shade.domain.interactor.privacyChipInteractor import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.domain.interactor.shadeModeInteractor -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.tintedIconManagerFactory import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.mobileIconsInteractor @@ -48,9 +46,6 @@ val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by tintedIconManagerFactory = tintedIconManagerFactory, batteryMeterViewControllerFactory = batteryMeterViewControllerFactory, statusBarIconController = mock<StatusBarIconController>(), - notificationIconContainerStatusBarViewBinder = - mock<NotificationIconContainerStatusBarViewBinder>(), - broadcastDispatcher = broadcastDispatcher, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt index 17ef208fe12e..51bb94fd2ab9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt @@ -30,6 +30,7 @@ import com.android.systemui.keyguard.ui.viewmodel.aodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.dozingToDreamingTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToOccludedTransitionViewModel @@ -83,6 +84,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel, aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel, aodToPrimaryBouncerTransitionViewModel = aodToPrimaryBouncerTransitionViewModel, + dozingToDreamingTransitionViewModel = dozingToDreamingTransitionViewModel, dozingToGlanceableHubTransitionViewModel = dozingToGlanceableHubTransitionViewModel, dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel, dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt index 06af32e69b75..b23ccbfbd93f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt @@ -14,12 +14,15 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.util.time import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.currentTime var Kosmos.systemClock by @@ -27,6 +30,7 @@ var Kosmos.systemClock by mock { whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } whenever(uptimeMillis()).thenAnswer { testScope.currentTime } + whenever(currentTimeMillis()).thenAnswer { testScope.currentTime } } } diff --git a/packages/Vcn/TEST_MAPPING b/packages/Vcn/TEST_MAPPING index 9722a838ab8e..9ca5304eb8d9 100644 --- a/packages/Vcn/TEST_MAPPING +++ b/packages/Vcn/TEST_MAPPING @@ -14,5 +14,13 @@ { "name": "CtsVcnTestCases" } + ], + "tethering-mainline-presubmit": [ + { + "name": "FrameworksVcnTests" + }, + { + "name": "CtsVcnTestCases" + } ] }
\ No newline at end of file diff --git a/packages/Vcn/framework-b/Android.bp b/packages/Vcn/framework-b/Android.bp index edb22c0e7aa0..c53123359872 100644 --- a/packages/Vcn/framework-b/Android.bp +++ b/packages/Vcn/framework-b/Android.bp @@ -77,8 +77,7 @@ framework_connectivity_b_defaults_soong_config { ], soong_config_variables: { is_vcn_in_mainline: { - //TODO: b/380155299 Make it Baklava when aidl tool can understand it - min_sdk_version: "current", + min_sdk_version: "36", static_libs: ["android.net.vcn.flags-aconfig-java"], apex_available: ["com.android.tethering"], 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 4fa0d506f09e..aa82df493f84 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -101,8 +101,15 @@ public class AutoclickController extends BaseEventStreamTransformation { } @Override - public void toggleAutoclickPause() { - // TODO(b/388872274): allows users to pause the autoclick. + public void toggleAutoclickPause(boolean paused) { + if (paused) { + if (mClickScheduler != null) { + mClickScheduler.cancel(); + } + if (mAutoclickIndicatorScheduler != null) { + mAutoclickIndicatorScheduler.cancel(); + } + } } }; @@ -133,7 +140,9 @@ public class AutoclickController extends BaseEventStreamTransformation { mAutoclickIndicatorScheduler); } - handleMouseMotion(event, policyFlags); + if (!isPaused()) { + handleMouseMotion(event, policyFlags); + } } else if (mClickScheduler != null) { mClickScheduler.cancel(); } @@ -216,6 +225,11 @@ public class AutoclickController extends BaseEventStreamTransformation { } } + private boolean isPaused() { + // TODO (b/397460424): Unpause when hovering over panel. + return Flags.enableAutoclickIndicator() && mAutoclickTypePanel.isPaused(); + } + /** * Observes autoclick setting values, and updates ClickScheduler delay and indicator size * whenever the setting value changes. diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java index 23c5cc4111f6..ba3e3d14b9c6 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java @@ -79,11 +79,20 @@ public class AutoclickTypePanel { // An interface exposed to {@link AutoclickController) to handle different actions on the panel, // including changing autoclick type, pausing/resuming autoclick. public interface ClickPanelControllerInterface { - // Allows users to change a different autoclick type. + /** + * Allows users to change a different autoclick type. + * + * @param clickType The new autoclick type to use. Should be one of the values defined in + * {@link AutoclickType}. + */ void handleAutoclickTypeChange(@AutoclickType int clickType); - // Allows users to pause/resume the autoclick. - void toggleAutoclickPause(); + /** + * Allows users to pause or resume autoclick. + * + * @param paused {@code true} to pause autoclick, {@code false} to resume. + */ + void toggleAutoclickPause(boolean paused); } private final Context mContext; @@ -211,6 +220,10 @@ public class AutoclickTypePanel { mWindowManager.removeView(mContentView); } + public boolean isPaused() { + return mPaused; + } + /** Toggles the panel expanded or collapsed state. */ private void togglePanelExpansion(@AutoclickType int clickType) { final LinearLayout button = getButtonFromClickType(clickType); @@ -234,6 +247,7 @@ public class AutoclickTypePanel { private void togglePause() { mPaused = !mPaused; + mClickPanelController.toggleAutoclickPause(mPaused); ImageButton imageButton = (ImageButton) mPauseButton.getChildAt(/* index= */ 0); if (mPaused) { diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 05301fdd8385..4f56483f487e 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -31,6 +31,8 @@ import static android.os.UserHandle.getCallingUserId; import static com.android.internal.util.CollectionUtils.any; import static com.android.internal.util.Preconditions.checkState; +import static com.android.server.companion.association.DisassociationProcessor.REASON_API; +import static com.android.server.companion.association.DisassociationProcessor.REASON_PKG_DATA_CLEARED; import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature; import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage; @@ -250,7 +252,7 @@ public class CompanionDeviceManagerService extends SystemService { + packageName + "]. Cleaning up CDM data..."); for (AssociationInfo association : associationsForPackage) { - mDisassociationProcessor.disassociate(association.getId()); + mDisassociationProcessor.disassociate(association.getId(), REASON_PKG_DATA_CLEARED); } mCompanionAppBinder.onPackageChanged(userId); @@ -426,7 +428,7 @@ public class CompanionDeviceManagerService extends SystemService { @Override public void disassociate(int associationId) { - mDisassociationProcessor.disassociate(associationId); + mDisassociationProcessor.disassociate(associationId, REASON_API); } @Override diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index e7d1460aa66a..c5ac7c31b5c3 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -18,6 +18,8 @@ package com.android.server.companion; import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_CONTEXT_SYNC; +import static com.android.server.companion.association.DisassociationProcessor.REASON_SHELL; + import android.companion.AssociationInfo; import android.companion.ContextSyncMessage; import android.companion.Flags; @@ -122,7 +124,7 @@ class CompanionDeviceShellCommand extends ShellCommand { if (association == null) { out.println("Association doesn't exist."); } else { - mDisassociationProcessor.disassociate(association.getId()); + mDisassociationProcessor.disassociate(association.getId(), REASON_SHELL); } } break; @@ -132,7 +134,7 @@ class CompanionDeviceShellCommand extends ShellCommand { final List<AssociationInfo> userAssociations = mAssociationStore.getAssociationsByUser(userId); for (AssociationInfo association : userAssociations) { - mDisassociationProcessor.disassociate(association.getId()); + mDisassociationProcessor.disassociate(association.getId(), REASON_SHELL); } } break; diff --git a/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java b/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java index f2d019bde703..ce7dcd0fa1d4 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java +++ b/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java @@ -58,6 +58,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; @@ -164,6 +165,7 @@ public final class AssociationDiskStore { private static final String FILE_NAME_LEGACY = "companion_device_manager_associations.xml"; private static final String FILE_NAME = "companion_device_manager.xml"; + private static final String FILE_NAME_LAST_REMOVED_ASSOCIATION = "last_removed_association.txt"; private static final String XML_TAG_STATE = "state"; private static final String XML_TAG_ASSOCIATIONS = "associations"; @@ -268,6 +270,46 @@ public final class AssociationDiskStore { } } + /** + * Read the last removed association from disk. + */ + public String readLastRemovedAssociation(@UserIdInt int userId) { + final AtomicFile file = createStorageFileForUser( + userId, FILE_NAME_LAST_REMOVED_ASSOCIATION); + StringBuilder sb = new StringBuilder(); + int c; + try (FileInputStream fis = file.openRead()) { + while ((c = fis.read()) != -1) { + sb.append((char) c); + } + fis.close(); + return sb.toString(); + } catch (FileNotFoundException e) { + Slog.e(TAG, "File " + file + " for user=" + userId + " doesn't exist."); + return null; + } catch (IOException e) { + Slog.e(TAG, "Can't read file " + file + " for user=" + userId); + return null; + } + } + + /** + * Write the last removed association to disk. + */ + public void writeLastRemovedAssociation(AssociationInfo association, String reason) { + Slog.i(TAG, "Writing last removed association=" + association.getId() + " to disk..."); + + final AtomicFile file = createStorageFileForUser( + association.getUserId(), FILE_NAME_LAST_REMOVED_ASSOCIATION); + writeToFileSafely(file, out -> { + out.write(String.valueOf(System.currentTimeMillis()).getBytes()); + out.write(' '); + out.write(reason.getBytes()); + out.write(' '); + out.write(association.toString().getBytes()); + }); + } + @NonNull private static Associations readAssociationsFromFile(@UserIdInt int userId, @NonNull AtomicFile file, @NonNull String rootTag) { diff --git a/services/companion/java/com/android/server/companion/association/AssociationStore.java b/services/companion/java/com/android/server/companion/association/AssociationStore.java index 757abd927ac8..f70c434e6b46 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationStore.java +++ b/services/companion/java/com/android/server/companion/association/AssociationStore.java @@ -276,7 +276,7 @@ public class AssociationStore { /** * Remove an association. */ - public void removeAssociation(int id) { + public void removeAssociation(int id, String reason) { Slog.i(TAG, "Removing association id=[" + id + "]..."); final AssociationInfo association; @@ -291,6 +291,8 @@ public class AssociationStore { writeCacheToDisk(association.getUserId()); + mDiskStore.writeLastRemovedAssociation(association, reason); + Slog.i(TAG, "Done removing association."); } @@ -525,6 +527,14 @@ public class AssociationStore { out.append(" ").append(a.toString()).append('\n'); } } + + out.append("Last Removed Association:\n"); + for (UserInfo user : mUserManager.getAliveUsers()) { + String lastRemovedAssociation = mDiskStore.readLastRemovedAssociation(user.id); + if (lastRemovedAssociation != null) { + out.append(" ").append(lastRemovedAssociation).append('\n'); + } + } } private void broadcastChange(@ChangeType int changeType, AssociationInfo association) { diff --git a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java index 150e8da5f614..248056f32a4f 100644 --- a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java +++ b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java @@ -47,6 +47,13 @@ import com.android.server.companion.transport.CompanionTransportManager; @SuppressLint("LongLogTag") public class DisassociationProcessor { + public static final String REASON_REVOKED = "revoked"; + public static final String REASON_SELF_IDLE = "self-idle"; + public static final String REASON_SHELL = "shell"; + public static final String REASON_LEGACY = "legacy"; + public static final String REASON_API = "api"; + public static final String REASON_PKG_DATA_CLEARED = "pkg-data-cleared"; + private static final String TAG = "CDM_DisassociationProcessor"; private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW = @@ -94,7 +101,7 @@ public class DisassociationProcessor { * Disassociate an association by id. */ // TODO: also revoke notification access - public void disassociate(int id) { + public void disassociate(int id, String reason) { Slog.i(TAG, "Disassociating id=[" + id + "]..."); final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(id); @@ -126,7 +133,7 @@ public class DisassociationProcessor { // Association cleanup. mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, id); - mAssociationStore.removeAssociation(association.getId()); + mAssociationStore.removeAssociation(association.getId(), reason); // If role is not in use by other associations, revoke the role. // Do not need to remove the system role since it was pre-granted by the system. @@ -151,7 +158,7 @@ public class DisassociationProcessor { } /** - * @deprecated Use {@link #disassociate(int)} instead. + * @deprecated Use {@link #disassociate(int, String)} instead. */ @Deprecated public void disassociate(int userId, String packageName, String macAddress) { @@ -165,7 +172,7 @@ public class DisassociationProcessor { mAssociationStore.getAssociationWithCallerChecks(association.getId()); - disassociate(association.getId()); + disassociate(association.getId(), REASON_LEGACY); } @SuppressLint("MissingPermission") @@ -223,7 +230,7 @@ public class DisassociationProcessor { Slog.i(TAG, "Removing inactive self-managed association=[" + association.toShortString() + "]."); - disassociate(id); + disassociate(id, REASON_SELF_IDLE); } } @@ -234,7 +241,7 @@ public class DisassociationProcessor { * * Lastly remove the role holder for the revoked associations for the same packages. * - * @see #disassociate(int) + * @see #disassociate(int, String) */ private class OnPackageVisibilityChangeListener implements ActivityManager.OnUidImportanceListener { @@ -260,7 +267,7 @@ public class DisassociationProcessor { int userId = UserHandle.getUserId(uid); for (AssociationInfo association : mAssociationStore.getRevokedAssociations(userId, packageName)) { - disassociate(association.getId()); + disassociate(association.getId(), REASON_REVOKED); } if (mAssociationStore.getRevokedAssociations().isEmpty()) { diff --git a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java index d8e10f842665..7eb7072520de 100644 --- a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java +++ b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java @@ -365,7 +365,7 @@ public class ContextualSearchManagerService extends SystemService { } } final ScreenshotHardwareBuffer shb = mWmInternal.takeContextualSearchScreenshot( - (Flags.contextualSearchWindowLayer() ? csUid : -1)); + (Flags.contextualSearchPreventSelfCapture() ? csUid : -1)); final Bitmap bm = shb != null ? shb.asBitmap() : null; // Now that everything is fetched, putting it in the launchIntent. if (bm != null) { @@ -549,7 +549,7 @@ public class ContextualSearchManagerService extends SystemService { Binder.withCleanCallingIdentity(() -> { final ScreenshotHardwareBuffer shb = mWmInternal.takeContextualSearchScreenshot( - (Flags.contextualSearchWindowLayer() ? callingUid : -1)); + (Flags.contextualSearchPreventSelfCapture() ? callingUid : -1)); final Bitmap bm = shb != null ? shb.asBitmap() : null; if (bm != null) { bundle.putParcelable(ContextualSearchManager.EXTRA_SCREENSHOT, diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index b677297dfef2..4bfee1d8398f 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -1464,7 +1464,10 @@ public class CachedAppOptimizer { void onProcessFrozen(ProcessRecord frozenProc) { if (useCompaction()) { synchronized (mProcLock) { - compactApp(frozenProc, CompactProfile.FULL, CompactSource.APP, false); + // only full-compact if process is cached + if (frozenProc.mState.getSetAdj() >= mCompactThrottleMinOomAdj) { + compactApp(frozenProc, CompactProfile.FULL, CompactSource.APP, false); + } } } frozenProc.onProcessFrozen(); diff --git a/services/core/java/com/android/server/am/OWNERS b/services/core/java/com/android/server/am/OWNERS index cc6fabc8fd67..4b6d6bc955cc 100644 --- a/services/core/java/com/android/server/am/OWNERS +++ b/services/core/java/com/android/server/am/OWNERS @@ -68,7 +68,7 @@ per-file CarUserSwitchingDialog.java = file:platform/packages/services/Car:/OWNE per-file ActivityManager* = file:/ACTIVITY_SECURITY_OWNERS # Aconfig Flags -per-file flags.aconfig = yamasani@google.com, bills@google.com, nalini@google.com +per-file flags.aconfig = yamasani@google.com, nalini@google.com # Londoners michaelwr@google.com #{LAST_RESORT_SUGGESTION} @@ -77,4 +77,4 @@ narayan@google.com #{LAST_RESORT_SUGGESTION} # Default yamasani@google.com hackbod@google.com #{LAST_RESORT_SUGGESTION} -omakoto@google.com #{LAST_RESORT_SUGGESTION}
\ No newline at end of file +omakoto@google.com #{LAST_RESORT_SUGGESTION} diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 61c5501a7b5a..13d367a95942 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -446,6 +446,8 @@ public class OomAdjuster { private static final int CACHING_UI_SERVICE_CLIENT_ADJ_THRESHOLD = Flags.raiseBoundUiServiceThreshold() ? SERVICE_ADJ : PERCEPTIBLE_APP_ADJ; + static final long PERCEPTIBLE_TASK_TIMEOUT_MILLIS = 5 * 60 * 1000; + @VisibleForTesting public static class Injector { boolean isChangeEnabled(@CachedCompatChangeId int cachedCompatChangeId, @@ -1847,7 +1849,7 @@ public class OomAdjuster { mHasVisibleActivities = false; } - void onOtherActivity() { + void onOtherActivity(long perceptibleTaskStoppedTimeMillis) { if (procState > PROCESS_STATE_CACHED_ACTIVITY) { procState = PROCESS_STATE_CACHED_ACTIVITY; mAdjType = "cch-act"; @@ -1856,6 +1858,28 @@ public class OomAdjuster { "Raise procstate to cached activity: " + app); } } + if (Flags.perceptibleTasks() && adj > PERCEPTIBLE_MEDIUM_APP_ADJ) { + if (perceptibleTaskStoppedTimeMillis >= 0) { + final long now = mInjector.getUptimeMillis(); + if (now - perceptibleTaskStoppedTimeMillis < PERCEPTIBLE_TASK_TIMEOUT_MILLIS) { + adj = PERCEPTIBLE_MEDIUM_APP_ADJ; + mAdjType = "perceptible-act"; + if (procState > PROCESS_STATE_IMPORTANT_BACKGROUND) { + procState = PROCESS_STATE_IMPORTANT_BACKGROUND; + } + + maybeSetProcessFollowUpUpdateLocked(app, + perceptibleTaskStoppedTimeMillis + PERCEPTIBLE_TASK_TIMEOUT_MILLIS, + now); + } else if (adj > PREVIOUS_APP_ADJ) { + adj = PREVIOUS_APP_ADJ; + mAdjType = "stale-perceptible-act"; + if (procState > PROCESS_STATE_LAST_ACTIVITY) { + procState = PROCESS_STATE_LAST_ACTIVITY; + } + } + } + } mHasVisibleActivities = false; } } diff --git a/services/core/java/com/android/server/am/ProcessStateRecord.java b/services/core/java/com/android/server/am/ProcessStateRecord.java index b0f808b39053..25175e6bee5f 100644 --- a/services/core/java/com/android/server/am/ProcessStateRecord.java +++ b/services/core/java/com/android/server/am/ProcessStateRecord.java @@ -1120,7 +1120,8 @@ final class ProcessStateRecord { } else if ((flags & ACTIVITY_STATE_FLAG_IS_STOPPING) != 0) { callback.onStoppingActivity((flags & ACTIVITY_STATE_FLAG_IS_STOPPING_FINISHING) != 0); } else { - callback.onOtherActivity(); + final long ts = mApp.getWindowProcessController().getPerceptibleTaskStoppedTimeMillis(); + callback.onOtherActivity(ts); } mCachedAdj = callback.adj; diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index 27c384a22fb6..c8fedf3d1765 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -293,6 +293,13 @@ flag { } flag { + name: "perceptible_tasks" + namespace: "system_performance" + description: "Boost the oom_score_adj of activities in perceptible tasks" + bug: "370890207" +} + +flag { name: "expedite_activity_launch_on_cold_start" namespace: "system_performance" description: "Notify ActivityTaskManager of cold starts early to fix app launch behavior." diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java index 88f5c81231b8..c41b8db1ce75 100644 --- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java +++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java @@ -333,7 +333,7 @@ public class AutomaticBrightnessController { int ambientLightHorizonLong, float userLux, float userNits, DisplayManagerFlags displayManagerFlags) { mInjector = injector; - mClock = injector.createClock(displayManagerFlags.offloadControlsDozeAutoBrightness()); + mClock = injector.createClock(); mContext = context; mCallbacks = callbacks; mSensorManager = sensorManager; @@ -1402,8 +1402,7 @@ public class AutomaticBrightnessController { public void onSensorChanged(SensorEvent event) { if (mLightSensorEnabled) { // The time received from the sensor is in nano seconds, hence changing it to ms - final long time = (mDisplayManagerFlags.offloadControlsDozeAutoBrightness()) - ? TimeUnit.NANOSECONDS.toMillis(event.timestamp) : mClock.uptimeMillis(); + final long time = TimeUnit.NANOSECONDS.toMillis(event.timestamp); final float lux = event.values[0]; handleLightSensorEvent(time, lux); } @@ -1616,20 +1615,13 @@ public class AutomaticBrightnessController { } private static class RealClock implements Clock { - private final boolean mOffloadControlsDozeBrightness; - - RealClock(boolean offloadControlsDozeBrightness) { - mOffloadControlsDozeBrightness = offloadControlsDozeBrightness; - } - @Override public long uptimeMillis() { return SystemClock.uptimeMillis(); } public long getSensorEventScaleTime() { - return (mOffloadControlsDozeBrightness) - ? SystemClock.elapsedRealtime() : uptimeMillis(); + return SystemClock.elapsedRealtime(); } } @@ -1638,8 +1630,8 @@ public class AutomaticBrightnessController { return BackgroundThread.getHandler(); } - Clock createClock(boolean offloadControlsDozeBrightness) { - return new RealClock(offloadControlsDozeBrightness); + Clock createClock() { + return new RealClock(); } } } diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java index 2c6f37448735..6510441ba28f 100644 --- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java +++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java @@ -291,8 +291,7 @@ public class DisplayBrightnessStrategySelector { void setAllowAutoBrightnessWhileDozing( DisplayManagerInternal.DisplayOffloadSession displayOffloadSession) { mAllowAutoBrightnessWhileDozing = mAllowAutoBrightnessWhileDozingConfig; - if (mDisplayManagerFlags.offloadControlsDozeAutoBrightness() - && mDisplayManagerFlags.isDisplayOffloadEnabled() + if (mDisplayManagerFlags.isDisplayOffloadEnabled() && displayOffloadSession != null) { mAllowAutoBrightnessWhileDozing &= displayOffloadSession.allowAutoBrightnessInDoze(); } diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index e4b595ab7c55..c3057ded66eb 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -156,11 +156,6 @@ public class DisplayManagerFlags { Flags.FLAG_DOZE_BRIGHTNESS_FLOAT, Flags::dozeBrightnessFloat); - private final FlagState mOffloadControlsDozeAutoBrightness = new FlagState( - Flags.FLAG_OFFLOAD_CONTROLS_DOZE_AUTO_BRIGHTNESS, - Flags::offloadControlsDozeAutoBrightness - ); - private final FlagState mPeakRefreshRatePhysicalLimit = new FlagState( Flags.FLAG_ENABLE_PEAK_REFRESH_RATE_PHYSICAL_LIMIT, Flags::enablePeakRefreshRatePhysicalLimit @@ -440,13 +435,6 @@ public class DisplayManagerFlags { return mDozeBrightnessFloat.isEnabled(); } - /** - * @return Whether DisplayOffload should control auto-brightness in doze - */ - public boolean offloadControlsDozeAutoBrightness() { - return mOffloadControlsDozeAutoBrightness.isEnabled(); - } - public boolean isPeakRefreshRatePhysicalLimitEnabled() { return mPeakRefreshRatePhysicalLimit.isEnabled(); } @@ -647,7 +635,6 @@ public class DisplayManagerFlags { pw.println(" " + mResolutionBackupRestore); pw.println(" " + mUseFusionProxSensor); pw.println(" " + mDozeBrightnessFloat); - pw.println(" " + mOffloadControlsDozeAutoBrightness); pw.println(" " + mPeakRefreshRatePhysicalLimit); pw.println(" " + mIgnoreAppPreferredRefreshRate); pw.println(" " + mSynthetic60hzModes); diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index acdc0e0cf891..f5451307afb7 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -255,17 +255,6 @@ flag { } flag { - name: "offload_controls_doze_auto_brightness" - namespace: "display_manager" - description: "Allows the registered DisplayOffloader to control if auto-brightness is used in doze" - bug: "327392714" - is_fixed_read_only: true - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "enable_peak_refresh_rate_physical_limit" namespace: "display_manager" description: "Flag for adding physical refresh rate limit if smooth display setting is on " diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index 7e8bb28b6a37..2af74f620c95 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -569,7 +569,8 @@ public final class DreamManagerService extends SystemService { } private void requestDreamInternal() { - if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront()) { + if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront() + && !isDozingInternal()) { return; } diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index 977c029f3a29..d71c8a1056d9 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -146,11 +146,6 @@ final class InputGestureManager { KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL ), createKeyGesture( - KeyEvent.KEYCODE_N, - KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON, - KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES - ), - createKeyGesture( KeyEvent.KEYCODE_S, KeyEvent.META_META_ON, KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 508bc2f811e0..dfdd9e54fe2e 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -3485,7 +3485,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. || (windowPerceptible != null && windowPerceptible == perceptible)) { return; } - mFocusedWindowPerceptible.put(windowToken, windowPerceptible); + mFocusedWindowPerceptible.put(windowToken, perceptible); updateSystemUiLocked(userId); } }); diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 0fc182f3f1bb..fff812c038e7 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -42,6 +42,7 @@ import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_P import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED; +import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_BUBBLE; import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_PROMOTABLE; import android.annotation.FlaggedApi; @@ -286,7 +287,7 @@ public class PreferencesHelper implements RankingConfig { if (!TAG_RANKING.equals(tag)) return; final int xmlVersion = parser.getAttributeInt(null, ATT_VERSION, -1); - boolean upgradeForBubbles = xmlVersion == XML_VERSION_BUBBLES_UPGRADE; + boolean upgradeForBubbles = xmlVersion >= XML_VERSION_BUBBLES_UPGRADE; boolean migrateToPermission = (xmlVersion < XML_VERSION_NOTIF_PERMISSION); if (mShowReviewPermissionsNotification && (xmlVersion < XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION)) { @@ -337,15 +338,19 @@ public class PreferencesHelper implements RankingConfig { } boolean skipWarningLogged = false; boolean skipGroupWarningLogged = false; - boolean hasSAWPermission = false; - if (upgradeForBubbles && uid != UNKNOWN_UID) { - hasSAWPermission = mAppOps.noteOpNoThrow( - OP_SYSTEM_ALERT_WINDOW, uid, name, null, - "check-notif-bubble") == AppOpsManager.MODE_ALLOWED; - } - int bubblePref = hasSAWPermission - ? BUBBLE_PREFERENCE_ALL - : parser.getAttributeInt(null, ATT_ALLOW_BUBBLE, DEFAULT_BUBBLE_PREFERENCE); + int bubblePref = parser.getAttributeInt(null, ATT_ALLOW_BUBBLE, + DEFAULT_BUBBLE_PREFERENCE); + boolean bubbleLocked = (parser.getAttributeInt(null, + ATT_APP_USER_LOCKED_FIELDS, DEFAULT_LOCKED_APP_FIELDS) & USER_LOCKED_BUBBLE) + != 0; + if (!bubbleLocked + && upgradeForBubbles + && uid != UNKNOWN_UID + && mAppOps.noteOpNoThrow(OP_SYSTEM_ALERT_WINDOW, uid, name, null, + "check-notif-bubble") == AppOpsManager.MODE_ALLOWED) { + // User hasn't changed bubble pref & the app has SAW, so allow all bubbles. + bubblePref = BUBBLE_PREFERENCE_ALL; + } int appImportance = parser.getAttributeInt(null, ATT_IMPORTANCE, DEFAULT_IMPORTANCE); // when data is loaded from disk it's loaded as USER_ALL, but restored data that diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 76c5240ab623..980fb155999e 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -1163,6 +1163,15 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } + private boolean shouldShowHub() { + final boolean hubEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), Settings.Secure.GLANCEABLE_HUB_ENABLED, + 1, mCurrentUserId) == 1; + + return mUserManagerInternal != null && mUserManagerInternal.isUserUnlocked(mCurrentUserId) + && hubEnabled && mDreamManagerInternal.dreamConditionActive(); + } + @VisibleForTesting void powerPress(long eventTime, int count, int displayId) { // SideFPS still needs to know about suppressed power buttons, in case it needs to block @@ -1251,7 +1260,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { mContext.getContentResolver(), Settings.Secure.GLANCEABLE_HUB_ENABLED, 1, mCurrentUserId) == 1; - if (mDreamManagerInternal.isDreaming() || isKeyguardShowing()) { + if ((mDreamManagerInternal != null && mDreamManagerInternal.isDreaming()) + || isKeyguardShowing()) { // If the device is already dreaming or on keyguard, go to sleep. sleepDefaultDisplayFromPowerButton(eventTime, 0); break; @@ -1261,9 +1271,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { // show hub. boolean keyguardAvailable = !mLockPatternUtils.isLockScreenDisabled( mCurrentUserId); - if (mUserManagerInternal.isUserUnlocked(mCurrentUserId) && hubEnabled - && keyguardAvailable && mDreamManagerInternal.dreamConditionActive()) { - // If the hub can be launched, send a message to keyguard. + if (shouldShowHub() && keyguardAvailable) { + // If the hub can be launched, send a message to keyguard. We do not know if + // the hub is already running or not, keyguard handles turning screen off if + // it is. Bundle options = new Bundle(); options.putBoolean(EXTRA_TRIGGER_HUB, true); lockNow(options); @@ -1324,14 +1335,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { * @param isScreenOn Whether the screen is currently on. * @param noDreamAction The action to perform if dreaming is not possible. */ - private void attemptToDreamFromShortPowerButtonPress( + private boolean attemptToDreamFromShortPowerButtonPress( boolean isScreenOn, Runnable noDreamAction) { if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP) { // If the power button behavior isn't one that should be able to trigger the dream, give // up. noDreamAction.run(); - return; + return false; } final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal(); @@ -1339,7 +1350,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { Slog.d(TAG, "Can't start dreaming when attempting to dream from short power" + " press (isScreenOn=" + isScreenOn + ")"); noDreamAction.run(); - return; + return false; } synchronized (mLock) { @@ -1350,6 +1361,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } dreamManagerInternal.requestDream(); + + return true; } /** @@ -2327,6 +2340,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { WindowWakeUpPolicy getWindowWakeUpPolicy() { return new WindowWakeUpPolicy(mContext); } + + DreamManagerInternal getDreamManagerInternal() { + return LocalServices.getService(DreamManagerInternal.class); + } } /** {@inheritDoc} */ @@ -2345,7 +2362,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class); mInputManager = mContext.getSystemService(InputManager.class); mInputManagerInternal = LocalServices.getService(InputManagerInternal.class); - mDreamManagerInternal = LocalServices.getService(DreamManagerInternal.class); + mDreamManagerInternal = injector.getDreamManagerInternal(); mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); mAppOpsManager = mContext.getSystemService(AppOpsManager.class); mSensorPrivacyManager = mContext.getSystemService(SensorPrivacyManager.class); @@ -3701,15 +3718,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { break; case KeyEvent.KEYCODE_N: if (firstDown && event.isMetaPressed()) { - if (event.isCtrlPressed()) { - sendSystemKeyToStatusBarAsync(event); - notifyKeyGestureCompleted(event, - KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES); - } else { - toggleNotificationPanel(); - notifyKeyGestureCompleted(event, - KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL); - } + toggleNotificationPanel(); + notifyKeyGestureCompleted(event, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL); return true; } break; @@ -6398,6 +6409,17 @@ public class PhoneWindowManager implements WindowManagerPolicy { event.getDisplayId(), event.getKeyCode(), "wakeUpFromWakeKey")) { return; } + + if (!shouldShowHub() + && mShortPressOnPowerBehavior == SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP + && event.getKeyCode() == KEYCODE_POWER + && attemptToDreamFromShortPowerButtonPress(false, () -> {})) { + // In the case that we should wake to dream and successfully initiate dreaming, do not + // continue waking up. Doing so will exit the dream state and cause UI to react + // accordingly. + return; + } + wakeUpFromWakeKey( event.getEventTime(), event.getKeyCode(), diff --git a/services/core/java/com/android/server/power/stats/processor/BinaryStatePowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/BinaryStatePowerStatsProcessor.java index 5eaaac9570d5..c89dddf45609 100644 --- a/services/core/java/com/android/server/power/stats/processor/BinaryStatePowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/BinaryStatePowerStatsProcessor.java @@ -274,12 +274,13 @@ abstract class BinaryStatePowerStatsProcessor extends PowerStatsProcessor { int uid = uids.get(k); if (stats.getUidStats(mTmpUidStatsArray, uid, proportionalEstimate.stateValues)) { - double power = intermediates.power - * mStatsLayout.getUidUsageDuration(mTmpUidStatsArray) - / intermediates.duration; - mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); - stats.setUidStats(uid, proportionalEstimate.stateValues, - mTmpUidStatsArray); + long duration = mStatsLayout.getUidUsageDuration(mTmpUidStatsArray); + if (duration != 0) { + double power = intermediates.power * duration / intermediates.duration; + mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, proportionalEstimate.stateValues, + mTmpUidStatsArray); + } } } } diff --git a/services/core/java/com/android/server/power/stats/processor/BluetoothPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/BluetoothPowerStatsProcessor.java index 9e8b553dfe29..c1cd3acf1656 100644 --- a/services/core/java/com/android/server/power/stats/processor/BluetoothPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/BluetoothPowerStatsProcessor.java @@ -299,8 +299,10 @@ class BluetoothPowerStatsProcessor extends PowerStatsProcessor { / intermediates.txBytes; } } - mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); - stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + if (power != 0) { + mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + } } } } diff --git a/services/core/java/com/android/server/power/stats/processor/CpuPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/CpuPowerStatsProcessor.java index b442dc2655d4..ab2489c81449 100644 --- a/services/core/java/com/android/server/power/stats/processor/CpuPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/CpuPowerStatsProcessor.java @@ -119,6 +119,8 @@ class CpuPowerStatsProcessor extends PowerStatsProcessor { mTmpUidStatsArray = new long[descriptor.uidStatsArrayLength]; mWakelockDescriptor = null; + + initEnergyConsumerToPowerBracketMaps(); } /** @@ -157,9 +159,6 @@ class CpuPowerStatsProcessor extends PowerStatsProcessor { if (mPlan == null) { mPlan = new PowerEstimationPlan(stats.getConfig()); - if (mStatsLayout.getEnergyConsumerCount() != 0) { - initEnergyConsumerToPowerBracketMaps(); - } } Intermediates intermediates = new Intermediates(); @@ -255,6 +254,10 @@ class CpuPowerStatsProcessor extends PowerStatsProcessor { */ private void initEnergyConsumerToPowerBracketMaps() { int energyConsumerCount = mStatsLayout.getEnergyConsumerCount(); + if (energyConsumerCount == 0) { + return; + } + int powerBracketCount = mStatsLayout.getCpuPowerBracketCount(); mEnergyConsumerToCombinedEnergyConsumerMap = new int[energyConsumerCount]; @@ -545,8 +548,10 @@ class CpuPowerStatsProcessor extends PowerStatsProcessor { power = Math.max(0, power - wakelockPowerEstimate); } - mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); - stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + if (power != 0) { + mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + } } } } diff --git a/services/core/java/com/android/server/power/stats/processor/CustomEnergyConsumerPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/CustomEnergyConsumerPowerStatsProcessor.java index 05865e2074dd..76ea7e841106 100644 --- a/services/core/java/com/android/server/power/stats/processor/CustomEnergyConsumerPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/CustomEnergyConsumerPowerStatsProcessor.java @@ -72,9 +72,12 @@ class CustomEnergyConsumerPowerStatsProcessor extends PowerStatsProcessor { int uid = uids.get(k); if (stats.getUidStats(mTmpUidStatsArray, uid, proportionalEstimate.stateValues)) { - sLayout.setUidPowerEstimate(mTmpUidStatsArray, - uCtoMah(sLayout.getUidConsumedEnergy(mTmpUidStatsArray, 0))); - stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + double power = uCtoMah(sLayout.getUidConsumedEnergy(mTmpUidStatsArray, 0)); + if (power != 0) { + sLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, proportionalEstimate.stateValues, + mTmpUidStatsArray); + } } } } diff --git a/services/core/java/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessor.java index c5db19bb3f32..a544daad82f1 100644 --- a/services/core/java/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessor.java @@ -383,8 +383,10 @@ class MobileRadioPowerStatsProcessor extends PowerStatsProcessor { / intermediates.txPackets; } - mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); - stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + if (power != 0) { + mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + } if (DEBUG) { Slog.d(TAG, "UID: " + uid diff --git a/services/core/java/com/android/server/power/stats/processor/MultiStateStats.java b/services/core/java/com/android/server/power/stats/processor/MultiStateStats.java index 28474a554b38..69325757c79d 100644 --- a/services/core/java/com/android/server/power/stats/processor/MultiStateStats.java +++ b/services/core/java/com/android/server/power/stats/processor/MultiStateStats.java @@ -16,6 +16,7 @@ package com.android.server.power.stats.processor; +import android.annotation.Nullable; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; @@ -333,9 +334,9 @@ class MultiStateStats { /** * Adds the delta to the metrics. The number of values must correspond to the dimension count - * supplied to the Factory constructor + * supplied to the Factory constructor. Null values is equivalent to an array of zeros. */ - void increment(long[] values, long timestampMs) { + void increment(@Nullable long[] values, long timestampMs) { mCounter.incrementValues(values, timestampMs); mTracking = true; } diff --git a/services/core/java/com/android/server/power/stats/processor/PowerComponentAggregatedPowerStats.java b/services/core/java/com/android/server/power/stats/processor/PowerComponentAggregatedPowerStats.java index 33baad8cc441..f9b9da9171cb 100644 --- a/services/core/java/com/android/server/power/stats/processor/PowerComponentAggregatedPowerStats.java +++ b/services/core/java/com/android/server/power/stats/processor/PowerComponentAggregatedPowerStats.java @@ -72,7 +72,6 @@ class PowerComponentAggregatedPowerStats { private MultiStateStats mDeviceStats; private final SparseArray<MultiStateStats> mStateStats = new SparseArray<>(); private final SparseArray<UidStats> mUidStats = new SparseArray<>(); - private long[] mZeroArray; private static class UidStats { public int[] states; @@ -251,11 +250,8 @@ class PowerComponentAggregatedPowerStats { for (int i = mUidStats.size() - 1; i >= 0; i--) { PowerComponentAggregatedPowerStats.UidStats uidStats = mUidStats.valueAt(i); if (!uidStats.updated && uidStats.stats != null) { - if (mZeroArray == null - || mZeroArray.length != mPowerStatsDescriptor.uidStatsArrayLength) { - mZeroArray = new long[mPowerStatsDescriptor.uidStatsArrayLength]; - } - uidStats.stats.increment(mZeroArray, timestampMs); + // Null stands for an array of zeros + uidStats.stats.increment(null, timestampMs); } uidStats.updated = false; } diff --git a/services/core/java/com/android/server/power/stats/processor/ScreenPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/ScreenPowerStatsProcessor.java index 5a8e8b40bc3c..9df3d7eea27b 100644 --- a/services/core/java/com/android/server/power/stats/processor/ScreenPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/ScreenPowerStatsProcessor.java @@ -230,9 +230,11 @@ class ScreenPowerStatsProcessor extends PowerStatsProcessor { int uid = uids.get(j); if (stats.getUidStats(mTmpUidStatsArray, uid, uidStateValues)) { long duration = mStatsLayout.getUidTopActivityDuration(mTmpUidStatsArray); - double power = intermediates.power * duration / totalTopActivityDuration; - mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); - stats.setUidStats(uid, uidStateValues, mTmpUidStatsArray); + if (duration != 0) { + double power = intermediates.power * duration / totalTopActivityDuration; + mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, uidStateValues, mTmpUidStatsArray); + } } } } diff --git a/services/core/java/com/android/server/power/stats/processor/WifiPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/WifiPowerStatsProcessor.java index f1797bd2301d..8cc0b6eb6150 100644 --- a/services/core/java/com/android/server/power/stats/processor/WifiPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/WifiPowerStatsProcessor.java @@ -375,8 +375,10 @@ class WifiPowerStatsProcessor extends PowerStatsProcessor { / intermediates.batchedScanDuration; } } - mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); - stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + if (power != 0) { + mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + } if (DEBUG) { Slog.d(TAG, "UID: " + uid diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index f4da1bb332fb..75b1b202bcfd 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -61,6 +61,7 @@ import android.os.VibratorInfo; import android.os.vibrator.Flags; import android.os.vibrator.IVibrationSessionCallback; import android.os.vibrator.PrebakedSegment; +import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibrationEffectSegment; import android.os.vibrator.VibratorInfoFactory; @@ -2672,7 +2673,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { CombinedVibration.ParallelCombination combination = CombinedVibration.startParallel(); while ("-v".equals(getNextOption())) { - int vibratorId = Integer.parseInt(getNextArgRequired()); + int vibratorId = parseInt(getNextArgRequired(), "Expected vibrator id after -v"); combination.addVibrator(vibratorId, nextEffect()); } runVibrate(commonOptions, combination.combine()); @@ -2684,7 +2685,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { CombinedVibration.SequentialCombination combination = CombinedVibration.startSequential(); while ("-v".equals(getNextOption())) { - int vibratorId = Integer.parseInt(getNextArgRequired()); + int vibratorId = parseInt(getNextArgRequired(), "Expected vibrator id after -v"); combination.addNext(vibratorId, nextEffect()); } runVibrate(commonOptions, combination.combine()); @@ -2709,7 +2710,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private int runHapticFeedback() { CommonOptions commonOptions = new CommonOptions(); - int constant = Integer.parseInt(getNextArgRequired()); + int constant = parseInt(getNextArgRequired(), "Expected haptic feedback constant id"); IBinder deathBinder = commonOptions.background ? VibratorManagerService.this : mShellCallbacksToken; @@ -2757,12 +2758,13 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if ("-a".equals(nextOption)) { hasAmplitude = true; } else if ("-w".equals(nextOption)) { - delay = Integer.parseInt(getNextArgRequired()); + delay = parseInt(getNextArgRequired(), "Expected delay millis after -w"); } } - long duration = Long.parseLong(getNextArgRequired()); - int amplitude = hasAmplitude ? Integer.parseInt(getNextArgRequired()) + long duration = parseInt(getNextArgRequired(), "Expected one-shot duration millis"); + int amplitude = hasAmplitude + ? parseInt(getNextArgRequired(), "Expected one-shot amplitude") : VibrationEffect.DEFAULT_AMPLITUDE; composition.addOffDuration(Duration.ofMillis(delay)); composition.addEffect(VibrationEffect.createOneShot(duration, amplitude)); @@ -2837,8 +2839,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { while ((nextOption = getNextOption()) != null) { switch (nextOption) { case "-a" -> isAdvanced = true; - case "-i" -> initialSharpness = Float.parseFloat(getNextArgRequired()); - case "-r" -> repeat = Integer.parseInt(getNextArgRequired()); + case "-i" -> initialSharpness = parseFloat(getNextArgRequired(), + "Expected initial sharpness after -i"); + case "-r" -> repeat = parseInt(getNextArgRequired(), + "Expected repeat index after -r"); } } @@ -2864,8 +2868,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // nextArg is not a duration, finish reading. break; } - intensity = Float.parseFloat(getNextArgRequired()); - sharpness = Float.parseFloat(getNextArgRequired()); + intensity = parseFloat(getNextArgRequired(), "Expected envelope intensity"); + sharpness = parseFloat(getNextArgRequired(), "Expected envelope sharpness"); builder.addControlPoint(intensity, sharpness, duration); pos++; } @@ -2893,16 +2897,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { getNextArgRequired(); // consume "waveform" String nextOption; while ((nextOption = getNextOption()) != null) { - if ("-a".equals(nextOption)) { - hasAmplitudes = true; - } else if ("-r".equals(nextOption)) { - repeat = Integer.parseInt(getNextArgRequired()); - } else if ("-w".equals(nextOption)) { - delay = Integer.parseInt(getNextArgRequired()); - } else if ("-f".equals(nextOption)) { - hasFrequencies = true; - } else if ("-c".equals(nextOption)) { - isContinuous = true; + switch (nextOption) { + case "-a" -> hasAmplitudes = true; + case "-f" -> hasFrequencies = true; + case "-c" -> isContinuous = true; + case "-r" -> repeat = parseInt(getNextArgRequired(), + "Expected repeat index after -r"); + case "-w" -> delay = parseInt(getNextArgRequired(), + "Expected delay millis after -w"); } } List<Integer> durations = new ArrayList<>(); @@ -2920,14 +2922,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { break; } if (hasAmplitudes) { - amplitudes.add( - Float.parseFloat(getNextArgRequired()) / VibrationEffect.MAX_AMPLITUDE); + int amplitude = parseInt(getNextArgRequired(), "Expected waveform amplitude"); + amplitudes.add((float) amplitude / VibrationEffect.MAX_AMPLITUDE); } else { amplitudes.add(nextAmplitude); nextAmplitude = 1 - nextAmplitude; } if (hasFrequencies) { - frequencies.add(Float.parseFloat(getNextArgRequired())); + frequencies.add( + parseFloat(getNextArgRequired(), "Expected waveform frequency")); } } @@ -2986,27 +2989,37 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if ("-b".equals(nextOption)) { shouldFallback = true; } else if ("-w".equals(nextOption)) { - delay = Integer.parseInt(getNextArgRequired()); + delay = parseInt(getNextArgRequired(), "Expected delay millis after -w"); } } - int effectId = Integer.parseInt(getNextArgRequired()); + int effectId = parseInt(getNextArgRequired(), "Expected prebaked effect id"); composition.addOffDuration(Duration.ofMillis(delay)); composition.addEffect(VibrationEffect.get(effectId, shouldFallback)); } private void addPrimitivesToComposition(VibrationEffect.Composition composition) { getNextArgRequired(); // consume "primitives" - String nextArg; - while ((nextArg = peekNextArg()) != null) { + while (peekNextArg() != null) { int delay = 0; - if ("-w".equals(nextArg)) { - getNextArgRequired(); // consume "-w" - delay = Integer.parseInt(getNextArgRequired()); - nextArg = peekNextArg(); + float scale = 1f; + int delayType = PrimitiveSegment.DEFAULT_DELAY_TYPE; + + String nextOption; + while ((nextOption = getNextOption()) != null) { + if ("-s".equals(nextOption)) { + scale = parseFloat(getNextArgRequired(), "Expected scale after -s"); + } else if ("-o".equals(nextOption)) { + delayType = VibrationEffect.Composition.DELAY_TYPE_RELATIVE_START_OFFSET; + delay = parseInt(getNextArgRequired(), "Expected offset millis after -o"); + } else if ("-w".equals(nextOption)) { + delayType = PrimitiveSegment.DEFAULT_DELAY_TYPE; + delay = parseInt(getNextArgRequired(), "Expected delay millis after -w"); + } } try { - composition.addPrimitive(Integer.parseInt(nextArg), /* scale= */ 1, delay); + String nextArg = peekNextArg(); // Just in case this is not a primitive. + composition.addPrimitive(Integer.parseInt(nextArg), scale, delay, delayType); getNextArgRequired(); // consume the primitive id } catch (NumberFormatException | NullPointerException e) { // nextArg is not describing a primitive, leave it to be consumed by outer loops @@ -3032,17 +3045,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { VibrationXmlParser.parseDocument(new StringReader(xml)); VibratorInfo combinedVibratorInfo = getCombinedVibratorInfo(); if (combinedVibratorInfo == null) { - throw new IllegalStateException( - "No combined vibrator info to parse vibration XML " + xml); + throw new IllegalStateException("No vibrator info available to parse XML"); } VibrationEffect effect = parsedVibration.resolve(combinedVibratorInfo); if (effect == null) { - throw new IllegalArgumentException( - "Parsed vibration cannot be resolved for vibration XML " + xml); + throw new IllegalArgumentException("Parsed XML cannot be resolved: " + xml); } return CombinedVibration.createParallel(effect); } catch (IOException e) { - throw new RuntimeException("Error parsing vibration XML " + xml, e); + throw new RuntimeException("Error parsing XML: " + xml, e); } } @@ -3060,16 +3071,30 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + private static int parseInt(String text, String errorMessage) { + try { + return Integer.parseInt(text); + } catch (NumberFormatException | NullPointerException e) { + throw new IllegalArgumentException(errorMessage, e); + } + } + + private static float parseFloat(String text, String errorMessage) { + try { + return Float.parseFloat(text); + } catch (NumberFormatException | NullPointerException e) { + throw new IllegalArgumentException(errorMessage, e); + } + } + @Override public void onHelp() { try (PrintWriter pw = getOutPrintWriter();) { pw.println("Vibrator Manager commands:"); pw.println(" help"); pw.println(" Prints this help text."); - pw.println(""); pw.println(" list"); - pw.println(" Prints the id of device vibrators. This does not include any "); - pw.println(" connected input device."); + pw.println(" Prints device vibrator ids; does not include input devices."); pw.println(" synced [options] <effect>..."); pw.println(" Vibrates effect on all vibrators in sync."); pw.println(" combined [options] (-v <vibrator-id> <effect>...)..."); @@ -3079,51 +3104,41 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { pw.println(" xml [options] <xml>"); pw.println(" Vibrates using combined vibration described in given XML string"); pw.println(" on all vibrators in sync. The XML could be:"); - pw.println(" XML containing a single effect, or"); - pw.println(" A vibration select XML containing multiple effects."); - pw.println(" Vibrates using combined vibration described in given XML string."); - pw.println(" XML containing a single effect it runs on all vibrators in sync."); + pw.println(" A single <vibration-effect>, or"); + pw.println(" A <vibration-select> containing multiple effects."); + pw.println(" feedback [options] <constant>"); + pw.println(" Performs a haptic feedback with the given constant."); pw.println(" cancel"); pw.println(" Cancels any active vibration"); - pw.println(" feedback [-f] [-d <description>] <constant>"); - pw.println(" Performs a haptic feedback with the given constant."); - pw.println(" The force (-f) option enables the `always` configuration, which"); - pw.println(" plays the haptic irrespective of the vibration intensity settings"); pw.println(""); pw.println("Effect commands:"); pw.println(" oneshot [-w delay] [-a] <duration> [<amplitude>]"); - pw.println(" Vibrates for duration milliseconds; ignored when device is on "); - pw.println(" DND (Do Not Disturb) mode; touch feedback strength user setting "); - pw.println(" will be used to scale amplitude."); + pw.println(" Vibrates for duration milliseconds."); pw.println(" If -w is provided, the effect will be played after the specified"); pw.println(" wait time in milliseconds."); pw.println(" If -a is provided, the command accepts a second argument for "); pw.println(" amplitude, in a scale of 1-255."); pw.print(" waveform [-w delay] [-r index] [-a] [-f] [-c] "); pw.println("(<duration> [<amplitude>] [<frequency>])..."); - pw.println(" Vibrates for durations and amplitudes in list; ignored when "); - pw.println(" device is on DND (Do Not Disturb) mode; touch feedback strength "); - pw.println(" user setting will be used to scale amplitude."); + pw.println(" Vibrates for durations and amplitudes in list."); pw.println(" If -w is provided, the effect will be played after the specified"); pw.println(" wait time in milliseconds."); pw.println(" If -r is provided, the waveform loops back to the specified"); - pw.println(" index (e.g. 0 loops from the beginning)"); + pw.println(" index (e.g. 0 loops from the beginning)."); pw.println(" If -a is provided, the command expects amplitude to follow each"); pw.println(" duration; otherwise, it accepts durations only and alternates"); - pw.println(" off/on"); + pw.println(" off/on."); pw.println(" If -f is provided, the command expects frequency to follow each"); - pw.println(" amplitude or duration; otherwise, it uses resonant frequency"); + pw.println(" amplitude or duration; otherwise, it uses resonant frequency."); pw.println(" If -c is provided, the waveform is continuous and will ramp"); pw.println(" between values; otherwise each entry is a fixed step."); pw.println(" Duration is in milliseconds; amplitude is a scale of 1-255;"); - pw.println(" frequency is an absolute value in hertz;"); + pw.println(" frequency is an absolute value in hertz."); pw.print(" envelope [-a] [-i initial sharpness] [-r index] "); pw.println("[<duration1> <intensity1> <sharpness1>]..."); pw.println(" Generates a vibration pattern based on a series of duration, "); pw.println(" intensity, and sharpness values. The total vibration time is "); - pw.println(" the sum of all durations; Ignored when device is on "); - pw.println(" DND (Do Not Disturb) mode; touch feedback strength user setting "); - pw.println(" will be used to scale amplitude."); + pw.println(" the sum of all durations."); pw.println(" If -a is provided, the waveform will use the advanced APIs to "); pw.println(" generate the vibration pattern and the input parameters "); pw.println(" become [<duration1> <amplitude1> <frequency1>]."); @@ -3132,19 +3147,20 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { pw.println(" If -r is provided, the waveform loops back to the specified index"); pw.println(" (e.g. 0 loops from the beginning)."); pw.println(" prebaked [-w delay] [-b] <effect-id>"); - pw.println(" Vibrates with prebaked effect; ignored when device is on DND "); - pw.println(" (Do Not Disturb) mode; touch feedback strength user setting "); - pw.println(" will be used to scale amplitude."); + pw.println(" Vibrates with prebaked effect."); pw.println(" If -w is provided, the effect will be played after the specified"); pw.println(" wait time in milliseconds."); pw.println(" If -b is provided, the prebaked fallback effect will be played if"); pw.println(" the device doesn't support the given effect-id."); - pw.println(" primitives ([-w delay] <primitive-id>)..."); - pw.println(" Vibrates with a composed effect; ignored when device is on DND "); - pw.println(" (Do Not Disturb) mode; touch feedback strength user setting "); - pw.println(" will be used to scale primitive intensities."); + pw.print(" primitives ([-w delay] [-o time] [-s scale]"); + pw.println("<primitive-id> [<scale>])..."); + pw.println(" Vibrates with a composed effect."); pw.println(" If -w is provided, the next primitive will be played after the "); pw.println(" specified wait time in milliseconds."); + pw.println(" If -o is provided, the next primitive will be played at the "); + pw.println(" specified start offset time in milliseconds."); + pw.println(" If -s is provided, the next primitive will be played with the"); + pw.println(" specified amplitude scale, in a scale of [0,1]."); pw.println(""); pw.println("Common Options:"); pw.println(" -f"); @@ -3155,6 +3171,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { pw.println(" -d <description>"); pw.println(" Add description to the vibration."); pw.println(""); + pw.println("Notes"); + pw.println(" Vibrations triggered by these commands will be ignored when"); + pw.println(" device is on DND (Do Not Disturb) mode; notification strength"); + pw.println(" user settings will be applied for scale."); + pw.println(""); } } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 5b9dc654cc75..7b6d408fbe2c 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -490,6 +490,7 @@ final class ActivityRecord extends WindowToken { private long createTime = System.currentTimeMillis(); long lastVisibleTime; // last time this activity became visible long pauseTime; // last time we started pausing the activity + long mStoppedTime; // last time we completely stopped the activity long launchTickTime; // base time for launch tick messages long topResumedStateLossTime; // last time we reported top resumed state loss to an activity // Last configuration reported to the activity in the client process. @@ -6378,6 +6379,7 @@ final class ActivityRecord extends WindowToken { Slog.w(TAG, "Exception thrown during pause", e); // Just in case, assume it to be stopped. mAppStopped = true; + mStoppedTime = SystemClock.uptimeMillis(); ProtoLog.v(WM_DEBUG_STATES, "Stop failed; moving to STOPPED: %s", this); setState(STOPPED, "stopIfPossible"); } @@ -6411,6 +6413,7 @@ final class ActivityRecord extends WindowToken { if (isStopping) { ProtoLog.v(WM_DEBUG_STATES, "Moving to STOPPED: %s (stop complete)", this); + mStoppedTime = SystemClock.uptimeMillis(); setState(STOPPED, "activityStopped"); } @@ -9572,7 +9575,18 @@ final class ActivityRecord extends WindowToken { && !mDisplayContent.isSleeping()) { // Visibility of starting activities isn't calculated until pause-complete, so if // this is not paused yet, don't consider it ready. - return false; + // However, due to pip1 having an intermediate state, add a special exception here + // that skips waiting if the next activity is already visible. + final ActivityRecord toResume = isPip2ExperimentEnabled() ? null + : mDisplayContent.getActivity((r) -> !r.finishing + && r.isVisibleRequested() + && !r.isTaskOverlay() + && !r.isAlwaysOnTop()); + if (toResume == null || !toResume.isVisible()) { + return false; + } else { + Slog.i(TAG, "Assuming sync-finish while pausing due to visible target"); + } } return true; } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 37783781a901..6f83822ee97a 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -2130,6 +2130,26 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override + public boolean setTaskIsPerceptible(int taskId, boolean isPerceptible) { + enforceTaskPermission("setTaskIsPerceptible()"); + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + final Task task = mRootWindowContainer.anyTaskForId(taskId, + MATCH_ATTACHED_TASK_ONLY); + if (task == null) { + Slog.w(TAG, "setTaskIsPerceptible: No task to set with id=" + taskId); + return false; + } + task.mIsPerceptible = isPerceptible; + } + return true; + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override public boolean removeTask(int taskId) { mAmInternal.enforceCallingPermission(REMOVE_TASKS, "removeTask()"); synchronized (mGlobalLock) { diff --git a/services/core/java/com/android/server/wm/PresentationController.java b/services/core/java/com/android/server/wm/PresentationController.java index 69463433827f..b3cff9c6cc3d 100644 --- a/services/core/java/com/android/server/wm/PresentationController.java +++ b/services/core/java/com/android/server/wm/PresentationController.java @@ -56,11 +56,6 @@ class PresentationController { ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Presentation added to display %d: %s", win.getDisplayId(), win); mPresentingDisplayIds.add(win.getDisplayId()); - if (enablePresentationForConnectedDisplays()) { - // A presentation hides all activities behind on the same display. - win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, - /*notifyClients=*/ true); - } win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ true); } @@ -76,11 +71,6 @@ class PresentationController { if (displayIdIndex != -1) { mPresentingDisplayIds.remove(displayIdIndex); } - if (enablePresentationForConnectedDisplays()) { - // A presentation hides all activities behind on the same display. - win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, - /*notifyClients=*/ true); - } win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ false); } } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index bc4392e0dd0e..8e5e2f311489 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -503,6 +503,17 @@ class Task extends TaskFragment { int mOffsetYForInsets; /** + * When set to true, the task will be kept at a PERCEPTIBLE_APP_ADJ, and downgraded + * to PREVIOUS_APP_ADJ if not in foreground for a period of time. + * One example use case is for desktop form factors, where it is important keep tasks in the + * perceptible state (rather than cached where it may be frozen) when a user moves it to the + * foreground. + * On startup, restored Tasks will not be perceptible, until user actually interacts with it + * (i.e. brings it to the foreground) + */ + boolean mIsPerceptible = false; + + /** * Whether the compatibility overrides that change the resizability of the app should be allowed * for the specific app. */ @@ -3847,6 +3858,7 @@ class Task extends TaskFragment { pw.print(ActivityInfo.resizeModeToString(mResizeMode)); pw.print(" mSupportsPictureInPicture="); pw.print(mSupportsPictureInPicture); pw.print(" isResizeable="); pw.println(isResizeable()); + pw.print(" isPerceptible="); pw.println(mIsPerceptible); pw.print(prefix); pw.print("lastActiveTime="); pw.print(lastActiveTime); pw.println(" (inactive for " + (getInactiveDuration() / 1000) + "s)"); pw.print(prefix); pw.println(" isTrimmable=" + mIsTrimmableFromRecents); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 7854a4ada276..9d18d6c4cc89 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -1884,7 +1884,8 @@ class TaskFragment extends WindowContainer<WindowContainer> { if (!hasDirectChildActivities()) { return false; } - if (mResumedActivity != null && mTransitionController.isTransientLaunch(mResumedActivity)) { + if (mResumedActivity != null && !mResumedActivity.finishing + && mTransitionController.isTransientLaunch(mResumedActivity)) { // Even if the transient activity is occluded, defer pausing (addToStopping will still // be called) it until the transient transition is done. So the current resuming // activity won't need to wait for additional pause complete. diff --git a/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java index 242883612124..e612d8ec0ce6 100644 --- a/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java +++ b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java @@ -20,6 +20,7 @@ import static android.graphics.Matrix.MSCALE_X; import static android.graphics.Matrix.MSCALE_Y; import static android.graphics.Matrix.MSKEW_X; import static android.graphics.Matrix.MSKEW_Y; +import static android.view.Display.INVALID_DISPLAY; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_TPL; @@ -35,6 +36,7 @@ import android.util.ArrayMap; import android.util.IntArray; import android.util.Pair; import android.util.Size; +import android.util.SparseArray; import android.view.InputWindowHandle; import android.window.ITrustedPresentationListener; import android.window.TrustedPresentationThresholds; @@ -251,7 +253,7 @@ public class TrustedPresentationListenerController { Rect tmpLogicalDisplaySize = new Rect(); Matrix tmpInverseMatrix = new Matrix(); float[] tmpMatrix = new float[9]; - Region coveredRegionsAbove = new Region(); + SparseArray<Region> coveredRegionsAboveByDisplay = new SparseArray<>(); long currTimeMs = System.currentTimeMillis(); ProtoLog.v(WM_DEBUG_TPL, "Checking %d windows", mLastWindowHandles.first.length); @@ -262,7 +264,7 @@ public class TrustedPresentationListenerController { ProtoLog.v(WM_DEBUG_TPL, "Skipping %s", windowHandle.name); continue; } - var displayFound = false; + int displayId = INVALID_DISPLAY; tmpRectF.set(windowHandle.frame); for (var displayHandle : mLastWindowHandles.second) { if (displayHandle.mDisplayId == windowHandle.displayId) { @@ -273,17 +275,18 @@ public class TrustedPresentationListenerController { tmpLogicalDisplaySize.set(0, 0, displayHandle.mLogicalSize.getWidth(), displayHandle.mLogicalSize.getHeight()); tmpRect.intersect(tmpLogicalDisplaySize); - displayFound = true; + displayId = displayHandle.mDisplayId; break; } } - if (!displayFound) { + if (displayId == INVALID_DISPLAY) { ProtoLog.v(WM_DEBUG_TPL, "Skipping %s, no associated display %d", windowHandle.name, windowHandle.displayId); continue; } + Region coveredRegionsAbove = coveredRegionsAboveByDisplay.get(displayId, new Region()); var listeners = mRegisteredListeners.get(windowHandle.getWindowToken()); if (listeners != null) { Region region = new Region(); @@ -304,6 +307,7 @@ public class TrustedPresentationListenerController { } coveredRegionsAbove.op(tmpRect, Region.Op.UNION); + coveredRegionsAboveByDisplay.put(displayId, coveredRegionsAbove); ProtoLog.v(WM_DEBUG_TPL, "coveredRegionsAbove updated with %s frame:%s region:%s", windowHandle.name, tmpRect.toShortString(), coveredRegionsAbove); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 0b20911bcab2..8aed91b2dc66 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -157,6 +157,7 @@ import static com.android.server.wm.WindowManagerServiceDumpProto.POLICY; import static com.android.server.wm.WindowManagerServiceDumpProto.ROOT_WINDOW_CONTAINER; import static com.android.server.wm.WindowManagerServiceDumpProto.WINDOW_FRAMES_VALID; import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions; +import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static com.android.window.flags.Flags.multiCrop; import static com.android.window.flags.Flags.setScPropertiesInClient; @@ -1820,8 +1821,28 @@ public class WindowManagerService extends IWindowManager.Stub final boolean hideSystemAlertWindows = shouldHideNonSystemOverlayWindow(win); win.setForceHideNonSystemOverlayWindowIfNeeded(hideSystemAlertWindows); - res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState, - outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs); + // Only a presentation window needs a transition because its visibility affets the + // lifecycle of apps below (b/390481865). + if (enablePresentationForConnectedDisplays() && win.isPresentation()) { + Transition transition = null; + if (!win.mTransitionController.isCollecting()) { + transition = win.mTransitionController.createAndStartCollecting(TRANSIT_OPEN); + } + win.mTransitionController.collect(win.mToken); + res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState, + outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs); + // A presentation hides all activities behind on the same display. + win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, + /*notifyClients=*/ true); + win.mTransitionController.getCollectingTransition().setReady(win.mToken, true); + if (transition != null) { + win.mTransitionController.requestStartTransition(transition, null, + null /* remoteTransition */, null /* displayChange */); + } + } else { + res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState, + outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs); + } } Binder.restoreCallingIdentity(origId); diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index 30dde543b9d4..b2d28a369edc 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -37,6 +37,7 @@ import static com.android.server.wm.ActivityRecord.State.PAUSING; import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityRecord.State.STARTED; import static com.android.server.wm.ActivityRecord.State.STOPPING; +import static com.android.server.wm.ActivityRecord.State.STOPPED; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RELEASE; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_CONFIGURATION; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RELEASE; @@ -344,6 +345,12 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio */ private volatile int mActivityStateFlags = ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER; + /** + * The most recent timestamp of when one of this process's stopped activities in a + * perceptible task became stopped. Written by window manager and read by activity manager. + */ + private volatile long mPerceptibleTaskStoppedTimeMillis = Long.MIN_VALUE; + public WindowProcessController(@NonNull ActivityTaskManagerService atm, @NonNull ApplicationInfo info, String name, int uid, int userId, Object owner, @NonNull WindowProcessListener listener) { @@ -1228,6 +1235,17 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio return mActivityStateFlags; } + /** + * Returns the most recent timestamp when one of this process's stopped activities in a + * perceptible task became stopped. It should only be called if {@link #hasActivities} + * returns {@code true} and {@link #getActivityStateFlags} does not have any of + * the ACTIVITY_STATE_FLAG_IS_(VISIBLE|PAUSING_OR_PAUSED|STOPPING) bit set. + */ + @HotPath(caller = HotPath.OOM_ADJUSTMENT) + public long getPerceptibleTaskStoppedTimeMillis() { + return mPerceptibleTaskStoppedTimeMillis; + } + void computeProcessActivityState() { // Since there could be more than one activities in a process record, we don't need to // compute the OomAdj with each of them, just need to find out the activity with the @@ -1239,6 +1257,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio int minTaskLayer = Integer.MAX_VALUE; int stateFlags = 0; int nonOccludedRatio = 0; + long perceptibleTaskStoppedTimeMillis = Long.MIN_VALUE; final boolean wasResumed = hasResumedActivity(); final boolean wasAnyVisible = (mActivityStateFlags & (ACTIVITY_STATE_FLAG_IS_VISIBLE | ACTIVITY_STATE_FLAG_IS_WINDOW_VISIBLE)) != 0; @@ -1287,6 +1306,11 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio bestInvisibleState = STOPPING; // Not "finishing" if any of activity isn't finishing. allStoppingFinishing &= r.finishing; + } else if (bestInvisibleState == DESTROYED && r.isState(STOPPED)) { + if (task.mIsPerceptible) { + perceptibleTaskStoppedTimeMillis = + Long.max(r.mStoppedTime, perceptibleTaskStoppedTimeMillis); + } } } } @@ -1324,6 +1348,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio } } mActivityStateFlags = stateFlags; + mPerceptibleTaskStoppedTimeMillis = perceptibleTaskStoppedTimeMillis; final boolean anyVisible = (stateFlags & (ACTIVITY_STATE_FLAG_IS_VISIBLE | ACTIVITY_STATE_FLAG_IS_WINDOW_VISIBLE)) != 0; diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index b13d510db159..bfedc90497ae 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -95,6 +95,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.view.WindowManager.LayoutParams.isSystemAlertWindowType; +import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME; import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_MULTIPLIER; import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET; @@ -182,6 +183,7 @@ import static com.android.server.wm.WindowStateProto.UNRESTRICTED_KEEP_CLEAR_ARE import static com.android.server.wm.WindowStateProto.VIEW_VISIBILITY; import static com.android.server.wm.WindowStateProto.WINDOW_CONTAINER; import static com.android.server.wm.WindowStateProto.WINDOW_FRAMES; +import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static com.android.window.flags.Flags.surfaceTrustedOverlay; import android.annotation.CallSuper; @@ -2286,11 +2288,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP dc.updateImeInputAndControlTarget(null); } - final int type = mAttrs.type; - - if (isPresentation()) { - mWmService.mPresentationController.onPresentationRemoved(this); - } // Check if window provides non decor insets before clearing its provided insets. final boolean windowProvidesDisplayDecorInsets = providesDisplayDecorInsets(); @@ -2431,11 +2428,33 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } } - removeImmediately(); - mWmService.updateFocusedWindowLocked(isFocused() - ? UPDATE_FOCUS_REMOVING_FOCUS - : UPDATE_FOCUS_NORMAL, - true /*updateInputWindows*/); + // Only a presentation window needs a transition because its visibility affets the + // lifecycle of apps below (b/390481865). + if (enablePresentationForConnectedDisplays() && isPresentation()) { + Transition transition = null; + if (!mTransitionController.isCollecting()) { + transition = mTransitionController.createAndStartCollecting(TRANSIT_CLOSE); + } + mTransitionController.collect(mToken); + mAnimatingExit = true; + mRemoveOnExit = true; + mToken.setVisibleRequested(false); + mWmService.mPresentationController.onPresentationRemoved(this); + // A presentation hides all activities behind on the same display. + mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, + /*notifyClients=*/ true); + mTransitionController.getCollectingTransition().setReady(mToken, true); + if (transition != null) { + mTransitionController.requestStartTransition(transition, null, + null /* remoteTransition */, null /* displayChange */); + } + } else { + removeImmediately(); + mWmService.updateFocusedWindowLocked(isFocused() + ? UPDATE_FOCUS_REMOVING_FOCUS + : UPDATE_FOCUS_NORMAL, + true /*updateInputWindows*/); + } } finally { Binder.restoreCallingIdentity(origId); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 191c21e661d0..aee32a0473a3 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -423,6 +423,7 @@ import android.database.ContentObserver; import android.database.Cursor; import android.graphics.Bitmap; import android.hardware.usb.UsbManager; +import android.health.connect.HealthConnectManager; import android.location.Location; import android.location.LocationManager; import android.media.AudioManager; @@ -2149,6 +2150,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { .hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); mBackgroundHandler = BackgroundThread.getHandler(); + // Add the health permission to the list of restricted permissions. + if (android.permission.flags.Flags.replaceBodySensorPermissionEnabled()) { + Set<String> healthPermissions = HealthConnectManager.getHealthPermissions(mContext); + for (String permission : healthPermissions) { + SENSOR_PERMISSIONS.add(permission); + } + } + // Needed when mHasFeature == false, because it controls the certificate warning text. mCertificateMonitor = new CertificateMonitor(this, mInjector, mBackgroundHandler); diff --git a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java index 7d25acd7f5e7..a42116351c2d 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java @@ -152,7 +152,7 @@ public class AutomaticBrightnessControllerTest { } @Override - AutomaticBrightnessController.Clock createClock(boolean isEnabled) { + AutomaticBrightnessController.Clock createClock() { return new AutomaticBrightnessController.Clock() { @Override public long uptimeMillis() { @@ -618,39 +618,46 @@ public class AutomaticBrightnessControllerTest { long increment = 500; // set autobrightness to low // t = 0 - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); // t = 500 mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); // t = 1000 mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(0.0f, mController.getAmbientLux(), EPSILON); // t = 1500 mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(0.0f, mController.getAmbientLux(), EPSILON); // t = 2000 // ensure that our reading is at 0. mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(0.0f, mController.getAmbientLux(), EPSILON); // t = 2500 // first 10000 lux sensor event reading mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertTrue(mController.getAmbientLux() > 0.0f); assertTrue(mController.getAmbientLux() < 10000.0f); // t = 3000 // lux reading should still not yet be 10000. mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertTrue(mController.getAmbientLux() > 0.0f); assertTrue(mController.getAmbientLux() < 10000.0f); @@ -659,45 +666,53 @@ public class AutomaticBrightnessControllerTest { // lux has been high (10000) for 1000ms. // lux reading should be 10000 // short horizon (ambient lux) is high, long horizon is still not high - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(10000.0f, mController.getAmbientLux(), EPSILON); // t = 4000 // stay high mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(10000.0f, mController.getAmbientLux(), EPSILON); // t = 4500 Mockito.clearInvocations(mBrightnessMappingStrategy); mClock.fastForward(increment); // short horizon is high, long horizon is high too - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); verify(mBrightnessMappingStrategy, times(1)).getBrightness(10000, null, -1); assertEquals(10000.0f, mController.getAmbientLux(), EPSILON); // t = 5000 mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertTrue(mController.getAmbientLux() > 0.0f); assertTrue(mController.getAmbientLux() < 10000.0f); // t = 5500 mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertTrue(mController.getAmbientLux() > 0.0f); assertTrue(mController.getAmbientLux() < 10000.0f); // t = 6000 mClock.fastForward(increment); // ambient lux goes to 0 - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(0.0f, mController.getAmbientLux(), EPSILON); // only the values within the horizon should be kept assertArrayEquals(new float[] {10000, 10000, 0, 0, 0}, mController.getLastSensorValues(), EPSILON); - assertArrayEquals(new long[] {4000, 4500, 5000, 5500, 6000}, + assertArrayEquals(new long[]{4000 + ANDROID_SLEEP_TIME, 4500 + ANDROID_SLEEP_TIME, + 5000 + ANDROID_SLEEP_TIME, 5500 + ANDROID_SLEEP_TIME, + 6000 + ANDROID_SLEEP_TIME}, mController.getLastSensorTimestamps()); } @@ -793,7 +808,8 @@ public class AutomaticBrightnessControllerTest { for (int i = 0; i < 1000; i++) { lux += increment; mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); } int valuesCount = (int) Math.ceil((double) AMBIENT_LIGHT_HORIZON_LONG / increment + 1); @@ -807,17 +823,17 @@ public class AutomaticBrightnessControllerTest { long sensorTimestamp = mClock.now(); for (int i = valuesCount - 1; i >= 1; i--) { assertEquals(lux, sensorValues[i], EPSILON); - assertEquals(sensorTimestamp, sensorTimestamps[i]); + assertEquals(sensorTimestamp + ANDROID_SLEEP_TIME, sensorTimestamps[i]); lux -= increment; sensorTimestamp -= increment; } assertEquals(lux, sensorValues[0], EPSILON); - assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG, sensorTimestamps[0]); + assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG + ANDROID_SLEEP_TIME, + sensorTimestamps[0]); } @Test public void testAmbientLuxBuffers_prunedBeyondLongHorizonExceptLatestValue() throws Exception { - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); ArgumentCaptor<SensorEventListener> listenerCaptor = ArgumentCaptor.forClass(SensorEventListener.class); verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor), @@ -867,7 +883,8 @@ public class AutomaticBrightnessControllerTest { for (int i = 0; i < 20; i++) { lux += increment1; mClock.fastForward(increment1); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); } int valuesCount = (int) Math.ceil((double) AMBIENT_LIGHT_HORIZON_LONG / increment1 + 1); @@ -877,7 +894,8 @@ public class AutomaticBrightnessControllerTest { for (int i = 0; i < initialCapacity - valuesCount; i++) { lux += increment2; mClock.fastForward(increment2); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); } float[] sensorValues = mController.getLastSensorValues(); @@ -890,7 +908,7 @@ public class AutomaticBrightnessControllerTest { long sensorTimestamp = mClock.now(); for (int i = initialCapacity - 1; i >= 1; i--) { assertEquals(lux, sensorValues[i], EPSILON); - assertEquals(sensorTimestamp, sensorTimestamps[i]); + assertEquals(sensorTimestamp + ANDROID_SLEEP_TIME, sensorTimestamps[i]); if (i >= valuesCount) { lux -= increment2; @@ -901,7 +919,8 @@ public class AutomaticBrightnessControllerTest { } } assertEquals(lux, sensorValues[0], EPSILON); - assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG, sensorTimestamps[0]); + assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG + ANDROID_SLEEP_TIME, + sensorTimestamps[0]); } @Test @@ -951,25 +970,29 @@ public class AutomaticBrightnessControllerTest { // t = 0 // Initial lux - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(500, mController.getAmbientLux(), EPSILON); // t = 1000 // Lux isn't steady yet mClock.fastForward(1000); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(500, mController.getAmbientLux(), EPSILON); // t = 1500 // Lux isn't steady yet mClock.fastForward(500); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(500, mController.getAmbientLux(), EPSILON); // t = 2500 // Lux is steady now mClock.fastForward(1000); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(1200, mController.getAmbientLux(), EPSILON); } @@ -992,25 +1015,29 @@ public class AutomaticBrightnessControllerTest { // t = 0 // Initial lux - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(1200, mController.getAmbientLux(), EPSILON); // t = 2000 // Lux isn't steady yet mClock.fastForward(2000); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(1200, mController.getAmbientLux(), EPSILON); // t = 2500 // Lux isn't steady yet mClock.fastForward(500); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(1200, mController.getAmbientLux(), EPSILON); // t = 4500 // Lux is steady now mClock.fastForward(2000); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(500, mController.getAmbientLux(), EPSILON); } @@ -1031,19 +1058,22 @@ public class AutomaticBrightnessControllerTest { // t = 0 // Initial lux - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(500, mController.getAmbientLux(), EPSILON); // t = 500 // Lux isn't steady yet mClock.fastForward(500); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(500, mController.getAmbientLux(), EPSILON); // t = 1500 // Lux is steady now mClock.fastForward(1000); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(1200, mController.getAmbientLux(), EPSILON); } @@ -1068,19 +1098,22 @@ public class AutomaticBrightnessControllerTest { // t = 0 // Initial lux - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(1200, mController.getAmbientLux(), EPSILON); // t = 1000 // Lux isn't steady yet mClock.fastForward(1000); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(1200, mController.getAmbientLux(), EPSILON); // t = 2500 // Lux is steady now mClock.fastForward(1500); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(500, mController.getAmbientLux(), EPSILON); } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index aed1f9858660..db94958f769e 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -1066,7 +1066,6 @@ public final class DisplayPowerControllerTest { com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, true); mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID); when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true); - when(mDisplayManagerFlagsMock.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(true); mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession); @@ -1172,7 +1171,6 @@ public final class DisplayPowerControllerTest { com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, true); mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID); when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true); - when(mDisplayManagerFlagsMock.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(false); mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession); diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java index 2ebb6c2a3ce4..ef39167dbabc 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java @@ -240,7 +240,6 @@ public final class DisplayBrightnessStrategySelectorTest { @Test public void selectStrategyDoesNotSelectDozeStrategyWhenOffloadSessionAutoBrightnessIsEnabled() { - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayManagerFlags.isDisplayOffloadEnabled()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(true); when(mResources.getBoolean(R.bool.config_allowAutoBrightnessWhileDozing)).thenReturn( @@ -378,7 +377,6 @@ public final class DisplayBrightnessStrategySelectorTest { @Test public void selectStrategy_selectsAutomaticStrategyWhenValid() { when(mDisplayManagerFlags.isRefactorDisplayPowerControllerEnabled()).thenReturn(true); - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayManagerFlags.isDisplayOffloadEnabled()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(true); when(mResources.getBoolean(R.bool.config_allowAutoBrightnessWhileDozing)).thenReturn( @@ -409,7 +407,6 @@ public final class DisplayBrightnessStrategySelectorTest { @Test public void selectStrategy_doesNotSelectAutomaticStrategyWhenStylusInUse() { when(mDisplayManagerFlags.isRefactorDisplayPowerControllerEnabled()).thenReturn(true); - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayManagerFlags.isDisplayOffloadEnabled()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(true); when(mResources.getBoolean(R.bool.config_allowAutoBrightnessWhileDozing)).thenReturn( @@ -536,7 +533,6 @@ public final class DisplayBrightnessStrategySelectorTest { @Test public void setAllowAutoBrightnessWhileDozing_enabledWhenConfigAndOffloadSessionAreEnabled() { - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayManagerFlags.isDisplayOffloadEnabled()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(true); when(mResources.getBoolean(R.bool.config_allowAutoBrightnessWhileDozing)).thenReturn( @@ -550,7 +546,6 @@ public final class DisplayBrightnessStrategySelectorTest { @Test public void setAllowAutoBrightnessWhileDozing_disabledWhenOffloadSessionFlagIsDisabled() { - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayManagerFlags.isDisplayOffloadEnabled()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(false); when(mResources.getBoolean(R.bool.config_allowAutoBrightnessWhileDozing)).thenReturn( @@ -564,7 +559,6 @@ public final class DisplayBrightnessStrategySelectorTest { @Test public void setAllowAutoBrightnessWhileDozing_disabledWhenABWhileDozingConfigIsDisabled() { - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayManagerFlags.isDisplayOffloadEnabled()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(true); when(mResources.getBoolean(R.bool.config_allowAutoBrightnessWhileDozing)).thenReturn( @@ -588,7 +582,6 @@ public final class DisplayBrightnessStrategySelectorTest { @Test public void setAllowAutoBrightnessWhileDozing_EnabledWhenFlagsAreDisabled() { - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mResources.getBoolean(R.bool.config_allowAutoBrightnessWhileDozing)).thenReturn( true); mDisplayBrightnessStrategySelector = new DisplayBrightnessStrategySelector(mContext, @@ -600,11 +593,5 @@ public final class DisplayBrightnessStrategySelectorTest { mDisplayBrightnessStrategySelector .setAllowAutoBrightnessWhileDozing(mDisplayOffloadSession); assertTrue(mDisplayBrightnessStrategySelector.isAllowAutoBrightnessWhileDozing()); - - when(mDisplayManagerFlags.isDisplayOffloadEnabled()).thenReturn(true); - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(false); - mDisplayBrightnessStrategySelector - .setAllowAutoBrightnessWhileDozing(mDisplayOffloadSession); - assertTrue(mDisplayBrightnessStrategySelector.isAllowAutoBrightnessWhileDozing()); } } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java index fa5847560782..4b53f1309337 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java @@ -23,18 +23,11 @@ import static com.android.server.am.ActivityManagerService.Injector; import static com.google.common.truth.Truth.assertThat; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import android.app.ActivityManagerInternal; import android.app.ActivityManagerInternal.FrozenProcessListener; import android.content.ComponentName; import android.content.Context; @@ -44,14 +37,12 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.MessageQueue; import android.os.Process; -import android.os.SystemClock; import android.platform.test.annotations.Presubmit; import android.provider.DeviceConfig; import android.text.TextUtils; import androidx.test.platform.app.InstrumentationRegistry; -import com.android.internal.annotations.GuardedBy; import com.android.modules.utils.testing.ExtendedMockitoRule; import com.android.modules.utils.testing.TestableDeviceConfig; import com.android.server.LocalServices; @@ -68,11 +59,9 @@ import org.mockito.Mock; import java.io.File; import java.io.IOException; -import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; /** @@ -164,7 +153,7 @@ public final class CachedAppOptimizerTest { app.info.uid = packageUid; // Exact value does not mater, it can be any state for which compaction is allowed. app.mState.setSetProcState(PROCESS_STATE_BOUND_FOREGROUND_SERVICE); - app.mState.setSetAdj(899); + app.mState.setSetAdj(940); app.mState.setCurAdj(940); return app; } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 1ef758cf192e..340115a7d465 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -3337,6 +3337,108 @@ public class MockingOomAdjusterTests { followUpTimeCaptor.capture()); } + /** + * For Perceptible Tasks adjustment, this solely unit-tests OomAdjuster -> onOtherActivity() + */ + @SuppressWarnings("GuardedBy") + @Test + @EnableFlags(Flags.FLAG_PERCEPTIBLE_TASKS) + public void testPerceptibleAdjustment() { + ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, + MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); + + long now = mInjector.getUptimeMillis(); + + // GIVEN: perceptible adjustment is NOT enabled (perceptible stop time is not set) + // EXPECT: zero adjustment + // TLDR: App is not set as a perceptible task and hence no oom_adj boosting. + mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.initialize(app, CACHED_APP_MIN_ADJ, + false, false, PROCESS_STATE_CACHED_ACTIVITY, + SCHED_GROUP_DEFAULT, 0, 0, PROCESS_STATE_IMPORTANT_FOREGROUND); + mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.onOtherActivity(-1); + assertEquals(CACHED_APP_MIN_ADJ, mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.adj); + + // GIVEN: perceptible adjustment is enabled (perceptible stop time is set) and + // elapsed time < PERCEPTIBLE_TASK_TIMEOUT + // EXPECT: adjustment to PERCEPTIBLE_MEDIUM_APP_ADJ + // TLDR: App is a perceptible task (e.g. opened from launcher) and has oom_adj boosting. + mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.initialize(app, CACHED_APP_MIN_ADJ, + false, false, PROCESS_STATE_CACHED_ACTIVITY, + SCHED_GROUP_DEFAULT, 0, 0, PROCESS_STATE_IMPORTANT_FOREGROUND); + mInjector.reset(); + mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.onOtherActivity(now); + assertEquals(PERCEPTIBLE_MEDIUM_APP_ADJ, + mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.adj); + + // GIVEN: perceptible adjustment is enabled (perceptible stop time is set) and + // elapsed time > PERCEPTIBLE_TASK_TIMEOUT + // EXPECT: adjustment to PREVIOUS_APP_ADJ + // TLDR: App is a perceptible task (e.g. opened from launcher) and has oom_adj boosting, but + // time has elapsed and has dropped to a lower boosting of PREVIOUS_APP_ADJ + mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.initialize(app, CACHED_APP_MIN_ADJ, + false, false, PROCESS_STATE_CACHED_ACTIVITY, + SCHED_GROUP_DEFAULT, 0, 0, PROCESS_STATE_IMPORTANT_FOREGROUND); + mInjector.jumpUptimeAheadTo(OomAdjuster.PERCEPTIBLE_TASK_TIMEOUT_MILLIS + 1000); + mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.onOtherActivity(0); + assertEquals(PREVIOUS_APP_ADJ, mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.adj); + } + + /** + * For Perceptible Tasks adjustment, this tests overall adjustment flow. + */ + @SuppressWarnings("GuardedBy") + @Test + @EnableFlags(Flags.FLAG_PERCEPTIBLE_TASKS) + public void testUpdateOomAdjPerceptible() { + ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, + MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); + WindowProcessController wpc = app.getWindowProcessController(); + + // Set uptime to be at least the timeout time + buffer, so that we don't end up with + // negative stopTime in our test input + mInjector.jumpUptimeAheadTo(OomAdjuster.PERCEPTIBLE_TASK_TIMEOUT_MILLIS + 60L * 1000L); + long now = mInjector.getUptimeMillis(); + doReturn(true).when(wpc).hasActivities(); + + // GIVEN: perceptible adjustment is is enabled + // EXPECT: perceptible-act adjustment + doReturn(WindowProcessController.ACTIVITY_STATE_FLAG_IS_STOPPING_FINISHING) + .when(wpc).getActivityStateFlags(); + doReturn(now).when(wpc).getPerceptibleTaskStoppedTimeMillis(); + updateOomAdj(app); + assertProcStates(app, PROCESS_STATE_IMPORTANT_BACKGROUND, PERCEPTIBLE_MEDIUM_APP_ADJ, + SCHED_GROUP_BACKGROUND, "perceptible-act"); + + // GIVEN: perceptible adjustment is is enabled and timeout has been reached + // EXPECT: stale-perceptible-act adjustment + doReturn(WindowProcessController.ACTIVITY_STATE_FLAG_IS_STOPPING_FINISHING) + .when(wpc).getActivityStateFlags(); + + doReturn(now - OomAdjuster.PERCEPTIBLE_TASK_TIMEOUT_MILLIS).when( + wpc).getPerceptibleTaskStoppedTimeMillis(); + updateOomAdj(app); + assertProcStates(app, PROCESS_STATE_LAST_ACTIVITY, PREVIOUS_APP_ADJ, + SCHED_GROUP_BACKGROUND, "stale-perceptible-act"); + + // GIVEN: perceptible adjustment is is disabled + // EXPECT: no perceptible adjustment + doReturn(WindowProcessController.ACTIVITY_STATE_FLAG_IS_STOPPING_FINISHING) + .when(wpc).getActivityStateFlags(); + doReturn(Long.MIN_VALUE).when(wpc).getPerceptibleTaskStoppedTimeMillis(); + updateOomAdj(app); + assertProcStates(app, PROCESS_STATE_CACHED_ACTIVITY, CACHED_APP_MIN_ADJ, + SCHED_GROUP_BACKGROUND, "cch-act"); + + // GIVEN: perceptible app is in foreground + // EXPECT: no perceptible adjustment + doReturn(WindowProcessController.ACTIVITY_STATE_FLAG_IS_VISIBLE) + .when(wpc).getActivityStateFlags(); + doReturn(now).when(wpc).getPerceptibleTaskStoppedTimeMillis(); + updateOomAdj(app); + assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, + SCHED_GROUP_DEFAULT, "vis-activity"); + } + @SuppressWarnings("GuardedBy") @Test public void testUpdateOomAdj_DoAll_Multiple_Provider_Retention() { diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java index 8fc8c9f677a6..6be9c6d4b80c 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java @@ -48,6 +48,7 @@ import org.junit.runner.RunWith; import java.io.File; import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; @@ -121,7 +122,7 @@ public class BatteryUsageStatsProviderPerfTest { } @Test - public void getBatteryUsageStats_accumulated() { + public void getBatteryUsageStats_accumulated() throws IOException { BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder() .setMaxStatsAgeMs(0) .includePowerStateData() @@ -155,6 +156,8 @@ public class BatteryUsageStatsProviderPerfTest { // Verify that all iterations produce the same result assertThat(cpuConsumedPower).isEqualTo(expectedCpuPower); } + stats.close(); + state.resumeTiming(); } } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MultiStateStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MultiStateStatsTest.java index a232c0c7aec9..3b614bdb38ed 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MultiStateStatsTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MultiStateStatsTest.java @@ -143,6 +143,8 @@ public class MultiStateStatsTest { multiStateStats.increment(new long[]{200, 200}, 5000); + multiStateStats.increment(null, 6000); // No-op + long[] stats = new long[DIMENSION_COUNT]; multiStateStats.getStats(stats, new int[]{0, BatteryConsumer.PROCESS_STATE_FOREGROUND, 0}); // (400 - 100) * 0.5 + (600 - 400) * 0.5 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 7f60caaa569b..0745c68fd337 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 @@ -25,6 +25,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.content.Context; import android.platform.test.annotations.DisableFlags; @@ -400,6 +401,46 @@ public class AutoclickControllerTest { .isNotEqualTo(initialScheduledTime); } + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void pauseButton_flagOn_clickNotTriggeredWhenPaused() { + injectFakeMouseActionHoverMoveEvent(); + + // Pause autoclick. + AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class); + when(mockAutoclickTypePanel.isPaused()).thenReturn(true); + mController.mAutoclickTypePanel = mockAutoclickTypePanel; + + // Send hover move event. + MotionEvent hoverMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30f, + /* y= */ 0f, + /* metaState= */ 0); + hoverMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + + // Verify there is not a pending click. + assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); + assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isEqualTo(-1); + + // Resume autoclick. + when(mockAutoclickTypePanel.isPaused()).thenReturn(false); + + // Send initial move event again. Because this is the first recorded move, a click won't be + // scheduled. + injectFakeMouseActionHoverMoveEvent(); + assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); + assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isEqualTo(-1); + + // Send move again to trigger click and verify there is now a pending click. + mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); + assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isNotEqualTo(-1); + } + private void injectFakeMouseActionHoverMoveEvent() { MotionEvent event = getFakeMotionHoverMoveEvent(); event.setSource(InputDevice.SOURCE_MOUSE); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java index f3016f4b17f9..ba672dcd299b 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java @@ -68,6 +68,7 @@ public class AutoclickTypePanelTest { private LinearLayout mPositionButton; private @AutoclickType int mActiveClickType = AUTOCLICK_TYPE_LEFT_CLICK; + private boolean mPaused; private final ClickPanelControllerInterface clickPanelController = new ClickPanelControllerInterface() { @@ -77,7 +78,9 @@ public class AutoclickTypePanelTest { } @Override - public void toggleAutoclickPause() {} + public void toggleAutoclickPause(boolean paused) { + mPaused = paused; + } }; @Before @@ -199,6 +202,17 @@ public class AutoclickTypePanelTest { } } + @Test + public void pauseButton_onClick() { + mPauseButton.callOnClick(); + assertThat(mPaused).isTrue(); + assertThat(mAutoclickTypePanel.isPaused()).isTrue(); + + mPauseButton.callOnClick(); + assertThat(mPaused).isFalse(); + assertThat(mAutoclickTypePanel.isPaused()).isFalse(); + } + private void verifyButtonHasSelectedStyle(@NonNull LinearLayout button) { GradientDrawable gradientDrawable = (GradientDrawable) button.getBackground(); assertThat(gradientDrawable.getColor().getDefaultColor()) 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 e5fac7ac5e0c..00b0c558b4e3 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java @@ -263,6 +263,11 @@ public class DpmMockContext extends MockContext { } @Override + public Context getApplicationContext() { + return this; + } + + @Override public PackageManager getPackageManager() { return mMockSystemServices.packageManager; } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 5dea44d6ebf4..67e85ff2445d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -4495,6 +4495,27 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test + public void testBubblePreference_sameVersionWithSAWPermission() throws Exception { + when(mAppOpsManager.noteOpNoThrow(eq(OP_SYSTEM_ALERT_WINDOW), anyInt(), + anyString(), eq(null), anyString())).thenReturn(MODE_ALLOWED); + + final String xml = "<ranking version=\"4\">\n" + + "<package name=\"" + PKG_O + "\" uid=\"" + UID_O + "\">\n" + + "<channel id=\"someId\" name=\"hi\"" + + " importance=\"3\"/>" + + "</package>" + + "</ranking>"; + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream(new ByteArrayInputStream(xml.getBytes())), + null); + parser.nextTag(); + mHelper.readXml(parser, false, UserHandle.USER_ALL); + + assertEquals(BUBBLE_PREFERENCE_ALL, mHelper.getBubblePreference(PKG_O, UID_O)); + assertEquals(0, mHelper.getAppLockedFields(PKG_O, UID_O)); + } + + @Test public void testBubblePreference_upgradeWithSAWThenUserOverride() throws Exception { when(mAppOpsManager.noteOpNoThrow(eq(OP_SYSTEM_ALERT_WINDOW), anyInt(), anyString(), eq(null), anyString())).thenReturn(MODE_ALLOWED); diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java index 8e2cea7a8c78..6d8a48799112 100644 --- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java @@ -244,9 +244,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { {"Meta + L -> Lock Homescreen", new int[]{META_KEY, KeyEvent.KEYCODE_L}, KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, KeyEvent.KEYCODE_L, META_ON}, - {"Meta + Ctrl + N -> Open Notes", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_N}, - KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES, KeyEvent.KEYCODE_N, - META_ON | CTRL_ON}, {"Meta + Ctrl + DPAD_DOWN -> Enter desktop mode", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_DOWN}, KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE, diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java index 32a3b7f2c9cc..22c86eb3a9b8 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java @@ -272,6 +272,19 @@ public class PhoneWindowManagerTests { } @Test + public void powerPress_withoutDreamManagerInternal_doesNotCrash() { + when(mDisplayPolicy.isAwake()).thenReturn(true); + mDreamManagerInternal = null; + initPhoneWindowManager(); + + // Power button pressed. + int eventTime = 0; + mPhoneWindowManager.powerPress(eventTime, 1, 0); + + // verify no crash + } + + @Test public void powerPress_hubOrDreamOrSleep_hubAvailableLocks() { when(mDisplayPolicy.isAwake()).thenReturn(true); mContext.getTestablePermissions().setPermission(android.Manifest.permission.DEVICE_POWER, @@ -352,5 +365,10 @@ public class PhoneWindowManagerTests { WindowWakeUpPolicy getWindowWakeUpPolicy() { return mock(WindowWakeUpPolicy.class); } + + @Override + DreamManagerInternal getDreamManagerInternal() { + return mDreamManagerInternal; + } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java index db90c28ec7df..2d4101e40615 100644 --- a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java @@ -17,17 +17,21 @@ package com.android.server.wm; import static android.view.Display.FLAG_PRESENTATION; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; +import android.annotation.NonNull; import android.graphics.Rect; import android.os.UserHandle; -import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.DisplayInfo; @@ -41,6 +45,7 @@ import android.view.WindowManagerGlobal; import androidx.test.filters.SmallTest; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -53,35 +58,100 @@ import org.junit.runner.RunWith; @RunWith(WindowTestRunner.class) public class PresentationControllerTests extends WindowTestsBase { + TestTransitionPlayer mPlayer; + + @Before + public void setUp() { + mPlayer = registerTestTransitionPlayer(); + } + @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) @Test - public void testPresentationHidesActivitiesBehind() { - final DisplayInfo displayInfo = new DisplayInfo(); - displayInfo.copyFrom(mDisplayInfo); - displayInfo.flags = FLAG_PRESENTATION; - final DisplayContent dc = createNewDisplay(displayInfo); - final int displayId = dc.getDisplayId(); - doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId); + public void testPresentationShowAndHide() { + final DisplayContent dc = createPresentationDisplay(); final ActivityRecord activity = createActivityRecord(createTask(dc)); assertTrue(activity.isVisible()); - doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled()); - final int uid = 100000; // uid for non-system user + // Add a presentation window, which requests the activity to stop. + final WindowState window = addPresentationWindow(100000, dc.mDisplayId); + assertFalse(activity.isVisibleRequested()); + assertTrue(activity.isVisible()); + final Transition addTransition = window.mTransitionController.getCollectingTransition(); + assertEquals(TRANSIT_OPEN, addTransition.mType); + assertTrue(addTransition.isInTransition(window)); + assertTrue(addTransition.isInTransition(activity)); + + // Completing the transition makes the activity invisible. + completeTransition(addTransition, /*abortSync=*/ true); + assertFalse(activity.isVisible()); + + // Remove a Presentation window, which requests the activity to be resumed back. + window.removeIfPossible(); + final Transition removeTransition = window.mTransitionController.getCollectingTransition(); + assertEquals(TRANSIT_CLOSE, removeTransition.mType); + assertTrue(removeTransition.isInTransition(window)); + assertTrue(removeTransition.isInTransition(activity)); + assertTrue(activity.isVisibleRequested()); + assertFalse(activity.isVisible()); + + // Completing the transition makes the activity visible. + completeTransition(removeTransition, /*abortSync=*/ false); + assertTrue(activity.isVisible()); + } + + @DisableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) + @Test + public void testPresentationShowAndHide_flagDisabled() { + final DisplayContent dc = createPresentationDisplay(); + final ActivityRecord activity = createActivityRecord(createTask(dc)); + assertTrue(activity.isVisible()); + + final WindowState window = addPresentationWindow(100000, dc.mDisplayId); + assertFalse(window.mTransitionController.isCollecting()); + assertTrue(activity.isVisibleRequested()); + assertTrue(activity.isVisible()); + + window.removeIfPossible(); + assertFalse(window.mTransitionController.isCollecting()); + assertTrue(activity.isVisibleRequested()); + assertTrue(activity.isVisible()); + assertFalse(window.isAttached()); + } + + private WindowState addPresentationWindow(int uid, int displayId) { final Session session = createTestSession(mAtm, 1234 /* pid */, uid); final int userId = UserHandle.getUserId(uid); - doReturn(false).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId)); + doReturn(true).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId)); final WindowManager.LayoutParams params = new WindowManager.LayoutParams( WindowManager.LayoutParams.TYPE_PRESENTATION); - final IWindow clientWindow = new TestIWindow(); - final int result = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, + final int res = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, userId, WindowInsets.Type.defaultVisible(), null, new InsetsState(), new InsetsSourceControl.Array(), new Rect(), new float[1]); - assertTrue(result >= WindowManagerGlobal.ADD_OKAY); - assertFalse(activity.isVisible()); - + assertTrue(res >= WindowManagerGlobal.ADD_OKAY); final WindowState window = mWm.windowForClientLocked(session, clientWindow, false); - window.removeImmediately(); - assertTrue(activity.isVisible()); + window.mHasSurface = true; + return window; + } + + private DisplayContent createPresentationDisplay() { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.copyFrom(mDisplayInfo); + displayInfo.flags = FLAG_PRESENTATION; + final DisplayContent dc = createNewDisplay(displayInfo); + final int displayId = dc.getDisplayId(); + doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId); + return dc; + } + + private void completeTransition(@NonNull Transition transition, boolean abortSync) { + final ActionChain chain = ActionChain.testFinish(transition); + if (abortSync) { + // Forcefully finishing the active sync for testing purpose. + mWm.mSyncEngine.abort(transition.getSyncId()); + } else { + transition.onTransactionReady(transition.getSyncId(), mTransaction); + } + transition.finishTransition(chain); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 2ee34d3a4b36..77ad7f7bac7f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -683,7 +683,7 @@ public class TransitionTests extends WindowTestsBase { app.onStartingWindowDrawn(); // The task appeared event should be deferred until transition ready. assertFalse(task.taskAppearedReady()); - testPlayer.onTransactionReady(app.getSyncTransaction()); + testPlayer.onTransactionReady(); assertTrue(task.taskAppearedReady()); assertTrue(playerProc.isRunningRemoteTransition()); assertTrue(controller.mRemotePlayer.reportRunning(delegateProc.getThread())); @@ -1162,7 +1162,8 @@ public class TransitionTests extends WindowTestsBase { screenDecor.updateSurfacePosition(mMockT); assertEquals(prevPos, screenDecor.mLastSurfacePosition); - final SurfaceControl.Transaction startTransaction = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction startTransaction = mTransaction; + clearInvocations(startTransaction); final SurfaceControl.TransactionCommittedListener transactionCommittedListener = onRotationTransactionReady(player, startTransaction); @@ -1213,7 +1214,8 @@ public class TransitionTests extends WindowTestsBase { assertFalse(statusBar.mToken.inTransition()); assertTrue(app.getTask().inTransition()); - final SurfaceControl.Transaction startTransaction = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction startTransaction = mTransaction; + clearInvocations(startTransaction); final SurfaceControl leash = statusBar.mToken.getAnimationLeash(); doReturn(true).when(leash).isValid(); final SurfaceControl.TransactionCommittedListener transactionCommittedListener = @@ -1287,7 +1289,8 @@ public class TransitionTests extends WindowTestsBase { // Avoid DeviceStateController disturbing the test by triggering another rotation change. doReturn(false).when(mDisplayContent).updateRotationUnchecked(); - onRotationTransactionReady(player, mWm.mTransactionFactory.get()).onTransactionCommitted(); + clearInvocations(mTransaction); + onRotationTransactionReady(player, mTransaction).onTransactionCommitted(); assertEquals(ROTATION_ANIMATION_SEAMLESS, player.mLastReady.getChange( mDisplayContent.mRemoteToken.toWindowContainerToken()).getRotationAnimation()); spyOn(navBarInsetsProvider); @@ -1350,7 +1353,7 @@ public class TransitionTests extends WindowTestsBase { mDisplayContent.setFixedRotationLaunchingAppUnchecked(home); doReturn(true).when(home).hasFixedRotationTransform(any()); player.startTransition(); - player.onTransactionReady(mDisplayContent.getSyncTransaction()); + player.onTransactionReady(); final DisplayRotation displayRotation = mDisplayContent.getDisplayRotation(); final RemoteDisplayChangeController displayChangeController = mDisplayContent @@ -3071,8 +3074,11 @@ public class TransitionTests extends WindowTestsBase { TestTransitionPlayer player, SurfaceControl.Transaction startTransaction) { final ArgumentCaptor<SurfaceControl.TransactionCommittedListener> listenerCaptor = ArgumentCaptor.forClass(SurfaceControl.TransactionCommittedListener.class); - player.onTransactionReady(startTransaction); - verify(startTransaction).addTransactionCommittedListener(any(), listenerCaptor.capture()); + player.onTransactionReady(); + // The startTransaction is from mWm.mTransactionFactory.get() in SyncGroup#finishNow. + // 2 times are from SyncGroup#finishNow and AsyncRotationController#setupStartTransaction. + verify(startTransaction, times(2)).addTransactionCommittedListener( + any(), listenerCaptor.capture()); return listenerCaptor.getValue(); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 97c6ac6854c3..c6416850c464 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -2159,13 +2159,14 @@ public class WindowTestsBase extends SystemServiceTestsBase { mOrganizer.startTransition(mLastTransit.getToken(), null); } - void onTransactionReady(SurfaceControl.Transaction t) { - mLastTransit.onTransactionReady(mLastTransit.getSyncId(), t); + void onTransactionReady() { + // SyncGroup#finishNow -> Transition#onTransactionReady. + mController.mSyncEngine.abort(mLastTransit.getSyncId()); } void start() { startTransition(); - onTransactionReady(mock(SurfaceControl.Transaction.class)); + onTransactionReady(); } public void finish() { diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 99c5bad7b2b9..c666fb7e05f1 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -371,18 +371,6 @@ class KeyGestureControllerTests { intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), TestData( - "META + CTRL + N -> Open Notes", - intArrayOf( - KeyEvent.KEYCODE_META_LEFT, - KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.KEYCODE_N - ), - KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES, - intArrayOf(KeyEvent.KEYCODE_N), - KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) - ), - TestData( "META + S -> Take Screenshot", intArrayOf( KeyEvent.KEYCODE_META_LEFT, diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java index be0c7daebb57..e62a89b17927 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java @@ -19,6 +19,7 @@ package com.android.internal.protolog; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.endsWith; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.times; @@ -157,13 +158,15 @@ public class ProtoLogCommandHandlerTest { cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "enable", "MY_GROUP" }); - Mockito.verify(mProtoLogConfigurationService).enableProtoLogToLogcat("MY_GROUP"); + Mockito.verify(mProtoLogConfigurationService) + .enableProtoLogToLogcat(Mockito.any(), eq("MY_GROUP")); cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "enable", "MY_GROUP", "MY_OTHER_GROUP" }); Mockito.verify(mProtoLogConfigurationService) - .enableProtoLogToLogcat("MY_GROUP", "MY_OTHER_GROUP"); + .enableProtoLogToLogcat(Mockito.any(), + eq("MY_GROUP"), eq("MY_OTHER_GROUP")); } @Test @@ -173,13 +176,15 @@ public class ProtoLogCommandHandlerTest { cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "disable", "MY_GROUP" }); - Mockito.verify(mProtoLogConfigurationService).disableProtoLogToLogcat("MY_GROUP"); + Mockito.verify(mProtoLogConfigurationService) + .disableProtoLogToLogcat(Mockito.any(), eq("MY_GROUP")); cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "disable", "MY_GROUP", "MY_OTHER_GROUP" }); Mockito.verify(mProtoLogConfigurationService) - .disableProtoLogToLogcat("MY_GROUP", "MY_OTHER_GROUP"); + .disableProtoLogToLogcat(Mockito.any(), + eq("MY_GROUP"), eq("MY_OTHER_GROUP")); } @Test diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java index 3be725101252..1f3f91ebf557 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java @@ -62,6 +62,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.PrintWriter; import java.util.List; /** @@ -234,7 +235,7 @@ public class ProtoLogConfigurationServiceTest { service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); - service.enableProtoLogToLogcat(TEST_GROUP); + service.enableProtoLogToLogcat(Mockito.mock(PrintWriter.class), TEST_GROUP); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); Mockito.verify(mMockClient).toggleLogcat(eq(true), @@ -251,7 +252,7 @@ public class ProtoLogConfigurationServiceTest { service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); - service.disableProtoLogToLogcat(TEST_GROUP); + service.disableProtoLogToLogcat(Mockito.mock(PrintWriter.class), TEST_GROUP); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); Mockito.verify(mMockClient).toggleLogcat(eq(false), @@ -269,7 +270,7 @@ public class ProtoLogConfigurationServiceTest { service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); - service.enableProtoLogToLogcat(OTHER_TEST_GROUP); + service.enableProtoLogToLogcat(Mockito.mock(PrintWriter.class), OTHER_TEST_GROUP); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); Mockito.verify(mMockClient, never()).toggleLogcat(anyBoolean(), any()); @@ -280,7 +281,7 @@ public class ProtoLogConfigurationServiceTest { final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(); Truth.assertThat(service.getGroups()).asList().doesNotContain(TEST_GROUP); - service.enableProtoLogToLogcat(TEST_GROUP); + service.enableProtoLogToLogcat(Mockito.mock(PrintWriter.class), TEST_GROUP); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); final RegisterClientArgs args = new RegisterClientArgs(); diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index 1273826c8ef8..8cd89ce89e83 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -273,7 +273,7 @@ public class TestableLooper { messages.add(message); } - // Repost all Messages back to the queuewith a new time. + // Repost all Messages back to the queue with a new time. while (true) { Message message = messages.poll(); if (message == null) { diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index ff4d8ef2ec25..0a5cb1ff4956 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -2649,6 +2649,10 @@ int LinkCommand::Action(const std::vector<std::string>& args) { ".mpg", ".mpeg", ".mp4", ".m4a", ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2", ".wma", ".wmv", ".webm", ".mkv"}); + if (options_.no_compress_fonts) { + options_.extensions_to_not_compress.insert({".ttf", ".otf", ".ttc"}); + } + // Turn off auto versioning for static-libs. if (context.GetPackageType() == PackageType::kStaticLib) { options_.no_auto_version = true; diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index 2f17853718ec..977978834fcd 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -78,6 +78,7 @@ struct LinkOptions { bool use_sparse_encoding = false; std::unordered_set<std::string> extensions_to_not_compress; std::optional<std::regex> regex_to_not_compress; + bool no_compress_fonts = false; FeatureFlagValues feature_flag_values; // Static lib options. @@ -300,6 +301,14 @@ class LinkCommand : public Command { "use the '$' symbol for end of line. Uses a case-sensitive ECMAScript" "regular expression grammar.", &no_compress_regex); + AddOptionalSwitch("--no-compress-fonts", + "Do not compress files with common extensions for fonts.\n" + "This allows loading fonts directly from the APK, without needing to\n" + "decompress them first. Loading fonts will be faster and use less memory.\n" + "The downside is that the APK will be larger.\n" + "Passing this flag is functionally equivalent to passing the following flags:\n" + "-0 .ttf -0 .otf -0 .ttc", + &options_.no_compress_fonts); AddOptionalSwitch("--warn-manifest-validation", "Treat manifest validation errors as warnings.", &options_.manifest_fixer_options.warn_validation); diff --git a/tools/aapt2/cmd/Link_test.cpp b/tools/aapt2/cmd/Link_test.cpp index a2dc8f8ce0fd..41f8e250efd7 100644 --- a/tools/aapt2/cmd/Link_test.cpp +++ b/tools/aapt2/cmd/Link_test.cpp @@ -98,6 +98,7 @@ TEST_F(LinkTest, NoCompressAssets) { WriteFile(GetTestPath("assets/test.txt"), content); WriteFile(GetTestPath("assets/test.hello.txt"), content); WriteFile(GetTestPath("assets/test.hello.xml"), content); + WriteFile(GetTestPath("assets/fonts/myfont.ttf"), content); const std::string out_apk = GetTestPath("out.apk"); std::vector<std::string> link_args = { @@ -136,6 +137,10 @@ TEST_F(LinkTest, NoCompressAssets) { file = zip->FindFile("assets/test.hello.xml"); ASSERT_THAT(file, Ne(nullptr)); EXPECT_FALSE(file->WasCompressed()); + + file = zip->FindFile("assets/fonts/myfont.ttf"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_TRUE(file->WasCompressed()); } TEST_F(LinkTest, NoCompressResources) { @@ -182,6 +187,42 @@ TEST_F(LinkTest, NoCompressResources) { EXPECT_FALSE(file->WasCompressed()); } +TEST_F(LinkTest, NoCompressFonts) { + StdErrDiagnostics diag; + std::string content(500, 'a'); + const std::string compiled_files_dir = GetTestPath("compiled"); + ASSERT_TRUE(CompileFile(GetTestPath("res/raw/test.txt"), content, compiled_files_dir, &diag)); + WriteFile(GetTestPath("assets/fonts/myfont1.ttf"), content); + WriteFile(GetTestPath("assets/fonts/myfont2.ttf"), content); + + const std::string out_apk = GetTestPath("out.apk"); + std::vector<std::string> link_args = { + "--manifest", GetDefaultManifest(), + "-o", out_apk, + "--no-compress-fonts", + "-A", GetTestPath("assets") + }; + + ASSERT_TRUE(Link(link_args, compiled_files_dir, &diag)); + + std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(out_apk, &diag); + ASSERT_THAT(apk, Ne(nullptr)); + io::IFileCollection* zip = apk->GetFileCollection(); + ASSERT_THAT(zip, Ne(nullptr)); + + auto file = zip->FindFile("res/raw/test.txt"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_TRUE(file->WasCompressed()); + + file = zip->FindFile("assets/fonts/myfont1.ttf"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_FALSE(file->WasCompressed()); + + file = zip->FindFile("assets/fonts/myfont2.ttf"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_FALSE(file->WasCompressed()); +} + TEST_F(LinkTest, OverlayStyles) { StdErrDiagnostics diag; const std::string compiled_files_dir = GetTestPath("compiled"); diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md index 5c3dfdcadfec..6bdbaaed9858 100644 --- a/tools/aapt2/readme.md +++ b/tools/aapt2/readme.md @@ -3,6 +3,8 @@ ## Version 2.20 - Too many features, bug fixes, and improvements to list since the last minor version update in 2017. This README will be updated more frequently in the future. +- Added a new flag `--no-compress-fonts`. This can significantly speed up loading fonts from APK + assets, at the cost of increasing the storage size of the APK. ## Version 2.19 - Added navigation resource type. |