diff options
196 files changed, 4547 insertions, 771 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 251776e907d8..44e4999ccf44 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -5369,7 +5369,9 @@ public class AlarmManagerService extends SystemService { // to do any wakelock or stats tracking, so we have nothing // left to do here but go on to the next thing. mSendFinishCount++; - if (Flags.acquireWakelockBeforeSend()) { + if (Flags.acquireWakelockBeforeSend() && mBroadcastRefCount == 0) { + // No other alarms are in-flight and this dispatch failed. We will + // acquire the wakelock again before the next dispatch. mWakeLock.release(); } return; @@ -5409,7 +5411,9 @@ public class AlarmManagerService extends SystemService { // stats management to do. It threw before we posted the delayed // timeout message, so we're done here. mListenerFinishCount++; - if (Flags.acquireWakelockBeforeSend()) { + if (Flags.acquireWakelockBeforeSend() && mBroadcastRefCount == 0) { + // No other alarms are in-flight and this dispatch failed. We will + // acquire the wakelock again before the next dispatch. mWakeLock.release(); } return; diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index e193d2ddd5a7..0a2b1eaaad6b 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -4002,6 +4002,10 @@ public final class ActivityThread extends ClientTransactionHandler + (fromIpc ? " (from ipc" : "")); } } + if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { + Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, + "updateProcessState: processState=" + processState); + } } /** Converts a process state to a VM process state. */ diff --git a/core/java/android/app/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java index ea4646aa9eb9..3fd9d8b26611 100644 --- a/core/java/android/app/AppCompatTaskInfo.java +++ b/core/java/android/app/AppCompatTaskInfo.java @@ -104,6 +104,8 @@ public class AppCompatTaskInfo implements Parcelable { public static final int FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE = FLAG_BASE << 9; /** Top activity flag for whether restart menu is shown due to display move. */ private static final int FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE = FLAG_BASE << 10; + /** Top activity flag for whether activity opted out of edge to edge. */ + public static final int FLAG_OPT_OUT_EDGE_TO_EDGE = FLAG_BASE << 11; @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = { @@ -118,7 +120,8 @@ public class AppCompatTaskInfo implements Parcelable { FLAG_FULLSCREEN_OVERRIDE_SYSTEM, FLAG_FULLSCREEN_OVERRIDE_USER, FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE, - FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE + FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE, + FLAG_OPT_OUT_EDGE_TO_EDGE }) public @interface TopActivityFlag {} @@ -132,7 +135,8 @@ public class AppCompatTaskInfo implements Parcelable { @TopActivityFlag private static final int FLAGS_ORGANIZER_INTERESTED = FLAG_IS_FROM_LETTERBOX_DOUBLE_TAP | FLAG_ELIGIBLE_FOR_USER_ASPECT_RATIO_BUTTON | FLAG_FULLSCREEN_OVERRIDE_SYSTEM - | FLAG_FULLSCREEN_OVERRIDE_USER | FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE; + | FLAG_FULLSCREEN_OVERRIDE_USER | FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE + | FLAG_OPT_OUT_EDGE_TO_EDGE; @TopActivityFlag private static final int FLAGS_COMPAT_UI_INTERESTED = FLAGS_ORGANIZER_INTERESTED @@ -347,6 +351,20 @@ public class AppCompatTaskInfo implements Parcelable { setTopActivityFlag(FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE, enable); } + /** + * Sets the top activity flag for whether the activity has opted out of edge to edge. + */ + public void setOptOutEdgeToEdge(boolean enable) { + setTopActivityFlag(FLAG_OPT_OUT_EDGE_TO_EDGE, enable); + } + + /** + * @return {@code true} if the top activity has opted out of edge to edge. + */ + public boolean hasOptOutEdgeToEdge() { + return isTopActivityFlagEnabled(FLAG_OPT_OUT_EDGE_TO_EDGE); + } + /** Clear all top activity flags and set to false. */ public void clearTopActivityFlags() { mTopActivityFlags = FLAG_UNDEFINED; diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index 4afe75f7814c..3eaf2c40daca 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -63,6 +63,16 @@ flag { } flag { + name: "modes_ui_dnd_tile" + namespace: "systemui" + description: "Shows a dedicated tile for the DND mode; dependent on modes_ui" + bug: "401217520" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "modes_hsum" namespace: "systemui" description: "Fixes for modes (and DND/Zen in general) with HSUM or secondary users" diff --git a/core/java/android/os/LegacyMessageQueue/MessageQueue.java b/core/java/android/os/LegacyMessageQueue/MessageQueue.java index 132bdd1e56b8..1cf57de08d5f 100644 --- a/core/java/android/os/LegacyMessageQueue/MessageQueue.java +++ b/core/java/android/os/LegacyMessageQueue/MessageQueue.java @@ -786,8 +786,8 @@ public final class MessageQueue { } /** - * Get the timestamp of the next executable message in our priority queue. - * Returns null if there are no messages ready for delivery. + * Get the timestamp of the next message in our priority queue. + * Returns null if there are no messages in the queue. * * Caller must ensure that this doesn't race 'next' from the Looper thread. */ @@ -799,8 +799,8 @@ public final class MessageQueue { } /** - * Return the next executable message in our priority queue. - * Returns null if there are no messages ready for delivery + * Return the next message in our priority queue. + * Returns null if there are no messages in the queue. * * Caller must ensure that this doesn't race 'next' from the Looper thread. */ diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java index 1a54f4df58fb..204e3444c547 100644 --- a/core/java/android/os/TestLooperManager.java +++ b/core/java/android/os/TestLooperManager.java @@ -96,8 +96,8 @@ public class TestLooperManager { } /** - * Retrieves and removes the next message that should be executed by this queue. - * If the queue is empty or no messages are deliverable, returns null. + * Retrieves and removes the next message in this queue. + * If the queue is empty, returns null. * This method never blocks. * * <p>Callers should always call {@link #recycle(Message)} on the message when all interactions @@ -112,9 +112,9 @@ public class TestLooperManager { } /** - * Retrieves, but does not remove, the values of {@link Message#when} of next message that - * should be executed by this queue. - * If the queue is empty or no messages are deliverable, returns null. + * Retrieves, but does not remove, the values of {@link Message#when} of next message in the + * queue. + * If the queue is empty, returns null. * This method never blocks. */ @FlaggedApi(Flags.FLAG_MESSAGE_QUEUE_TESTABILITY) diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index ce31e1ea7e38..df3b8baa40c8 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -455,7 +455,7 @@ public class DreamService extends Service implements Window.Callback { // Simply wake up in the case the device is not locked. if (!keyguardManager.isKeyguardLocked()) { - wakeUp(); + wakeUp(false); return true; } @@ -477,11 +477,11 @@ public class DreamService extends Service implements Window.Callback { if (!mInteractive) { if (mDebug) Slog.v(mTag, "Waking up on keyEvent"); - wakeUp(); + wakeUp(false); return true; } else if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { if (mDebug) Slog.v(mTag, "Waking up on back key"); - wakeUp(); + wakeUp(false); return true; } return mWindow.superDispatchKeyEvent(event); @@ -492,7 +492,7 @@ public class DreamService extends Service implements Window.Callback { public boolean dispatchKeyShortcutEvent(KeyEvent event) { if (!mInteractive) { if (mDebug) Slog.v(mTag, "Waking up on keyShortcutEvent"); - wakeUp(); + wakeUp(false); return true; } return mWindow.superDispatchKeyShortcutEvent(event); @@ -505,7 +505,7 @@ public class DreamService extends Service implements Window.Callback { // but finish()es on any other kind of activity if (!mInteractive && event.getActionMasked() == MotionEvent.ACTION_UP) { if (mDebug) Slog.v(mTag, "Waking up on touchEvent"); - wakeUp(); + wakeUp(false); return true; } return mWindow.superDispatchTouchEvent(event); @@ -516,7 +516,7 @@ public class DreamService extends Service implements Window.Callback { public boolean dispatchTrackballEvent(MotionEvent event) { if (!mInteractive) { if (mDebug) Slog.v(mTag, "Waking up on trackballEvent"); - wakeUp(); + wakeUp(false); return true; } return mWindow.superDispatchTrackballEvent(event); @@ -527,7 +527,7 @@ public class DreamService extends Service implements Window.Callback { public boolean dispatchGenericMotionEvent(MotionEvent event) { if (!mInteractive) { if (mDebug) Slog.v(mTag, "Waking up on genericMotionEvent"); - wakeUp(); + wakeUp(false); return true; } return mWindow.superDispatchGenericMotionEvent(event); @@ -925,32 +925,37 @@ public class DreamService extends Service implements Window.Callback { } } - private synchronized void updateDoze() { - if (mDreamToken == null) { - Slog.w(mTag, "Updating doze without a dream token."); - return; - } + /** + * Updates doze state. Note that this must be called on the mHandler. + */ + private void updateDoze() { + mHandler.post(() -> { + if (mDreamToken == null) { + Slog.w(mTag, "Updating doze without a dream token."); + return; + } - if (mDozing) { - try { - Slog.v(mTag, "UpdateDoze mDozeScreenState=" + mDozeScreenState - + " mDozeScreenBrightness=" + mDozeScreenBrightness - + " mDozeScreenBrightnessFloat=" + mDozeScreenBrightnessFloat); - if (startAndStopDozingInBackground()) { - mDreamManager.startDozingOneway( - mDreamToken, mDozeScreenState, mDozeScreenStateReason, - mDozeScreenBrightnessFloat, mDozeScreenBrightness, - mUseNormalBrightnessForDoze); - } else { - mDreamManager.startDozing( - mDreamToken, mDozeScreenState, mDozeScreenStateReason, - mDozeScreenBrightnessFloat, mDozeScreenBrightness, - mUseNormalBrightnessForDoze); + if (mDozing) { + try { + Slog.v(mTag, "UpdateDoze mDozeScreenState=" + mDozeScreenState + + " mDozeScreenBrightness=" + mDozeScreenBrightness + + " mDozeScreenBrightnessFloat=" + mDozeScreenBrightnessFloat); + if (startAndStopDozingInBackground()) { + mDreamManager.startDozingOneway( + mDreamToken, mDozeScreenState, mDozeScreenStateReason, + mDozeScreenBrightnessFloat, mDozeScreenBrightness, + mUseNormalBrightnessForDoze); + } else { + mDreamManager.startDozing( + mDreamToken, mDozeScreenState, mDozeScreenStateReason, + mDozeScreenBrightnessFloat, mDozeScreenBrightness, + mUseNormalBrightnessForDoze); + } + } catch (RemoteException ex) { + // system server died } - } catch (RemoteException ex) { - // system server died } - } + }); } /** @@ -966,14 +971,20 @@ public class DreamService extends Service implements Window.Callback { */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) public void stopDozing() { - if (mDozing) { - mDozing = false; - try { - mDreamManager.stopDozing(mDreamToken); - } catch (RemoteException ex) { - // system server died + mHandler.post(() -> { + if (mDreamToken == null) { + return; } - } + + if (mDozing) { + mDozing = false; + try { + mDreamManager.stopDozing(mDreamToken); + } catch (RemoteException ex) { + // system server died + } + } + }); } /** @@ -1201,7 +1212,7 @@ public class DreamService extends Service implements Window.Callback { @Override public void onExitRequested() { // Simply finish dream when exit is requested. - mHandler.post(() -> finish()); + mHandler.post(() -> finishInternal()); } @Override @@ -1299,9 +1310,13 @@ public class DreamService extends Service implements Window.Callback { * </p> */ public final void finish() { + mHandler.post(this::finishInternal); + } + + private void finishInternal() { // If there is an active overlay connection, signal that the dream is ending before - // continuing. Note that the overlay cannot rely on the unbound state, since another dream - // might have bound to it in the meantime. + // continuing. Note that the overlay cannot rely on the unbound state, since another + // dream might have bound to it in the meantime. if (mOverlayConnection != null) { mOverlayConnection.addConsumer(overlay -> { try { @@ -1357,7 +1372,7 @@ public class DreamService extends Service implements Window.Callback { * </p> */ public final void wakeUp() { - wakeUp(false); + mHandler.post(()-> wakeUp(false)); } /** @@ -1559,7 +1574,7 @@ public class DreamService extends Service implements Window.Callback { if (mActivity != null && !mActivity.isFinishing()) { mActivity.finishAndRemoveTask(); } else { - finish(); + finishInternal(); } mDreamToken = null; @@ -1719,7 +1734,7 @@ public class DreamService extends Service implements Window.Callback { // the window reference in order to fully release the DreamActivity. mWindow = null; mActivity = null; - finish(); + finishInternal(); } if (mOverlayConnection != null && mDreamStartOverlayConsumer != null) { diff --git a/core/java/com/android/internal/policy/DesktopModeCompatUtils.java b/core/java/com/android/internal/policy/DesktopModeCompatUtils.java new file mode 100644 index 000000000000..d7cfbdfed99c --- /dev/null +++ b/core/java/com/android/internal/policy/DesktopModeCompatUtils.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.policy; + +import static android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED; +import static android.content.pm.ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS; + +import android.annotation.NonNull; +import android.content.pm.ActivityInfo; +import android.window.DesktopModeFlags; + +/** + * Utility functions for app compat in desktop windowing used by both window manager and System UI. + * @hide + */ +public final class DesktopModeCompatUtils { + + /** + * Whether the caption insets should be excluded from configuration for system to handle. + * The caller should ensure the activity is in or entering desktop view. + * + * <p> The treatment is enabled when all the of the following is true: + * <li> Any flags to forcibly consume caption insets are enabled. + * <li> Top activity have configuration coupled with insets. + * <li> Task is not resizeable or per-app override + * {@link ActivityInfo#OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS} is enabled. + */ + public static boolean shouldExcludeCaptionFromAppBounds(@NonNull ActivityInfo info, + boolean isResizeable, boolean optOutEdgeToEdge) { + return DesktopModeFlags.EXCLUDE_CAPTION_FROM_APP_BOUNDS.isTrue() + && isAnyForceConsumptionFlagsEnabled() + && !isConfigurationDecoupled(info, optOutEdgeToEdge) + && (!isResizeable + || info.isChangeEnabled(OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS)); + } + + private static boolean isConfigurationDecoupled(@NonNull ActivityInfo info, + boolean optOutEdgeToEdge) { + return info.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED) && !optOutEdgeToEdge; + } + + private static boolean isAnyForceConsumptionFlagsEnabled() { + return DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS.isTrue() + || DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isTrue(); + } +} diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java b/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java index d3a496db2ca9..f70f4cbceb70 100644 --- a/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java +++ b/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java @@ -179,7 +179,7 @@ public class CoreDocumentAccessibility implements RemoteComposeDocumentAccessibi * @return */ public boolean performClick(Component component) { - mDocument.performClick(mRemoteContext, component.getComponentId()); + mDocument.performClick(mRemoteContext, component.getComponentId(), ""); return true; } diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java index 3cc998576741..caf19e1ed34a 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java +++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java @@ -19,8 +19,10 @@ import android.annotation.NonNull; import android.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.widget.remotecompose.core.operations.BitmapData; import com.android.internal.widget.remotecompose.core.operations.ComponentValue; import com.android.internal.widget.remotecompose.core.operations.DrawContent; +import com.android.internal.widget.remotecompose.core.operations.FloatConstant; import com.android.internal.widget.remotecompose.core.operations.FloatExpression; import com.android.internal.widget.remotecompose.core.operations.Header; import com.android.internal.widget.remotecompose.core.operations.IntegerExpression; @@ -29,6 +31,7 @@ import com.android.internal.widget.remotecompose.core.operations.RootContentBeha import com.android.internal.widget.remotecompose.core.operations.ShaderData; import com.android.internal.widget.remotecompose.core.operations.TextData; import com.android.internal.widget.remotecompose.core.operations.Theme; +import com.android.internal.widget.remotecompose.core.operations.Utils; import com.android.internal.widget.remotecompose.core.operations.layout.CanvasOperations; import com.android.internal.widget.remotecompose.core.operations.layout.Component; import com.android.internal.widget.remotecompose.core.operations.layout.Container; @@ -42,6 +45,8 @@ import com.android.internal.widget.remotecompose.core.operations.utilities.IntMa import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer; import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; import com.android.internal.widget.remotecompose.core.serialize.Serializable; +import com.android.internal.widget.remotecompose.core.types.IntegerConstant; +import com.android.internal.widget.remotecompose.core.types.LongConstant; import java.util.ArrayList; import java.util.HashMap; @@ -68,7 +73,7 @@ public class CoreDocument implements Serializable { // We also keep a more fine-grained BUILD number, exposed as // ID_API_LEVEL = DOCUMENT_API_LEVEL + BUILD - static final float BUILD = 0.4f; + static final float BUILD = 0.5f; @NonNull ArrayList<Operation> mOperations = new ArrayList<>(); @@ -442,6 +447,94 @@ public class CoreDocument implements Serializable { return mDocProperties.get(key); } + /** + * Apply a collection of operations to the document + * + * @param delta the delta to apply + */ + public void applyUpdate(CoreDocument delta) { + HashMap<Integer, TextData> txtData = new HashMap<Integer, TextData>(); + HashMap<Integer, BitmapData> imgData = new HashMap<Integer, BitmapData>(); + HashMap<Integer, FloatConstant> fltData = new HashMap<Integer, FloatConstant>(); + HashMap<Integer, IntegerConstant> intData = new HashMap<Integer, IntegerConstant>(); + HashMap<Integer, LongConstant> longData = new HashMap<Integer, LongConstant>(); + recursiveTreverse( + mOperations, + (op) -> { + if (op instanceof TextData) { + TextData d = (TextData) op; + txtData.put(d.mTextId, d); + } else if (op instanceof BitmapData) { + BitmapData d = (BitmapData) op; + imgData.put(d.mImageId, d); + } else if (op instanceof FloatConstant) { + FloatConstant d = (FloatConstant) op; + fltData.put(d.mId, d); + } else if (op instanceof IntegerConstant) { + IntegerConstant d = (IntegerConstant) op; + intData.put(d.mId, d); + } else if (op instanceof LongConstant) { + LongConstant d = (LongConstant) op; + longData.put(d.mId, d); + } + }); + + recursiveTreverse( + delta.mOperations, + (op) -> { + if (op instanceof TextData) { + TextData t = (TextData) op; + TextData txtInDoc = txtData.get(t.mTextId); + if (txtInDoc != null) { + txtInDoc.update(t); + Utils.log("update" + t.mText); + txtInDoc.markDirty(); + } + } else if (op instanceof BitmapData) { + BitmapData b = (BitmapData) op; + BitmapData imgInDoc = imgData.get(b.mImageId); + if (imgInDoc != null) { + imgInDoc.update(b); + imgInDoc.markDirty(); + } + } else if (op instanceof FloatConstant) { + FloatConstant f = (FloatConstant) op; + FloatConstant fltInDoc = fltData.get(f.mId); + if (fltInDoc != null) { + fltInDoc.update(f); + fltInDoc.markDirty(); + } + } else if (op instanceof IntegerConstant) { + IntegerConstant ic = (IntegerConstant) op; + IntegerConstant intInDoc = intData.get(ic.mId); + if (intInDoc != null) { + intInDoc.update(ic); + intInDoc.markDirty(); + } + } else if (op instanceof LongConstant) { + LongConstant lc = (LongConstant) op; + LongConstant longInDoc = longData.get(lc.mId); + if (longInDoc != null) { + longInDoc.update(lc); + longInDoc.markDirty(); + } + } + }); + } + + private interface Visitor { + void visit(Operation op); + } + + private void recursiveTreverse(ArrayList<Operation> mOperations, Visitor visitor) { + for (Operation op : mOperations) { + if (op instanceof Container) { + recursiveTreverse(((Component) op).mList, visitor); + } + visitor.visit(op); + } + } + // ============== Haptic support ================== public interface HapticEngine { /** @@ -911,7 +1004,7 @@ public class CoreDocument implements Serializable { * * @param id the click area id */ - public void performClick(@NonNull RemoteContext context, int id) { + public void performClick(@NonNull RemoteContext context, int id, @NonNull String metadata) { for (ClickAreaRepresentation clickArea : mClickAreas) { if (clickArea.mId == id) { warnClickListeners(clickArea); @@ -920,7 +1013,7 @@ public class CoreDocument implements Serializable { } for (IdActionCallback listener : mIdActionListeners) { - listener.onAction(id, ""); + listener.onAction(id, metadata); } Component component = getComponent(id); diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operations.java b/core/java/com/android/internal/widget/remotecompose/core/Operations.java index 20897ba77de4..ac9f98bd6b15 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/Operations.java +++ b/core/java/com/android/internal/widget/remotecompose/core/Operations.java @@ -104,6 +104,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.managers import com.android.internal.widget.remotecompose.core.operations.layout.managers.CollapsibleRowLayout; import com.android.internal.widget.remotecompose.core.operations.layout.managers.ColumnLayout; import com.android.internal.widget.remotecompose.core.operations.layout.managers.FitBoxLayout; +import com.android.internal.widget.remotecompose.core.operations.layout.managers.ImageLayout; import com.android.internal.widget.remotecompose.core.operations.layout.managers.RowLayout; import com.android.internal.widget.remotecompose.core.operations.layout.managers.StateLayout; import com.android.internal.widget.remotecompose.core.operations.layout.managers.TextLayout; @@ -115,6 +116,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.modifier import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.GraphicsLayerModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightInModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightModifierOperation; +import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HostActionMetadataOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HostActionOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HostNamedActionOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.MarqueeModifierOperation; @@ -247,6 +249,7 @@ public class Operations { public static final int LAYOUT_CANVAS_CONTENT = 207; public static final int LAYOUT_TEXT = 208; public static final int LAYOUT_STATE = 217; + public static final int LAYOUT_IMAGE = 234; public static final int COMPONENT_START = 2; @@ -278,6 +281,7 @@ public class Operations { public static final int MODIFIER_VISIBILITY = 211; public static final int HOST_ACTION = 209; + public static final int HOST_METADATA_ACTION = 216; public static final int HOST_NAMED_ACTION = 210; public static final int VALUE_INTEGER_CHANGE_ACTION = 212; @@ -385,6 +389,7 @@ public class Operations { map.put(CONTAINER_END, ContainerEnd::read); map.put(HOST_ACTION, HostActionOperation::read); + map.put(HOST_METADATA_ACTION, HostActionMetadataOperation::read); map.put(HOST_NAMED_ACTION, HostNamedActionOperation::read); map.put(VALUE_INTEGER_CHANGE_ACTION, ValueIntegerChangeActionOperation::read); map.put( @@ -407,7 +412,7 @@ public class Operations { map.put(LAYOUT_CANVAS, CanvasLayout::read); map.put(LAYOUT_CANVAS_CONTENT, CanvasContent::read); map.put(LAYOUT_TEXT, TextLayout::read); - + map.put(LAYOUT_IMAGE, ImageLayout::read); map.put(LAYOUT_STATE, StateLayout::read); map.put(DRAW_CONTENT, DrawContent::read); diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java index 161a5f17ef23..1f026687680f 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java +++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java @@ -102,6 +102,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.managers import com.android.internal.widget.remotecompose.core.operations.layout.managers.CollapsibleRowLayout; import com.android.internal.widget.remotecompose.core.operations.layout.managers.ColumnLayout; import com.android.internal.widget.remotecompose.core.operations.layout.managers.FitBoxLayout; +import com.android.internal.widget.remotecompose.core.operations.layout.managers.ImageLayout; import com.android.internal.widget.remotecompose.core.operations.layout.managers.RowLayout; import com.android.internal.widget.remotecompose.core.operations.layout.managers.StateLayout; import com.android.internal.widget.remotecompose.core.operations.layout.managers.TextLayout; @@ -2094,6 +2095,19 @@ public class RemoteComposeBuffer { } /** + * Add an imagelayout command + * + * @param componentId component id + * @param animationId animation id + * @param bitmapId bitmap id + */ + public void addImage( + int componentId, int animationId, int bitmapId, int scaleType, float alpha) { + mLastComponentId = getComponentId(componentId); + ImageLayout.apply(mBuffer, componentId, animationId, bitmapId, scaleType, alpha); + } + + /** * Add a row start tag * * @param componentId component id diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java index 255d7a46e49e..90929e06ecd0 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java @@ -41,12 +41,12 @@ import java.util.List; public class BitmapData extends Operation implements SerializableToString, Serializable { private static final int OP_CODE = Operations.DATA_BITMAP; private static final String CLASS_NAME = "BitmapData"; - int mImageId; + public final int mImageId; int mImageWidth; int mImageHeight; short mType; short mEncoding; - @NonNull final byte[] mBitmap; + @NonNull byte[] mBitmap; /** The max size of width or height */ public static final int MAX_IMAGE_DIMENSION = 8000; @@ -91,6 +91,19 @@ public class BitmapData extends Operation implements SerializableToString, Seria } /** + * Update the bitmap data + * + * @param from the bitmap to copy + */ + public void update(BitmapData from) { + this.mImageWidth = from.mImageWidth; + this.mImageHeight = from.mImageHeight; + this.mBitmap = from.mBitmap; + this.mType = from.mType; + this.mEncoding = from.mEncoding; + } + + /** * The width of the image * * @return the width diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java index 233e246d3868..5096aaf03c9d 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java @@ -42,6 +42,15 @@ public class FloatConstant extends Operation implements Serializable { this.mValue = value; } + /** + * Copy the value from another operation + * + * @param from value to copy from + */ + public void update(FloatConstant from) { + mValue = from.mValue; + } + @Override public void write(@NonNull WireBuffer buffer) { apply(buffer, mId, mValue); diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java index 67773d1d6187..d8ef4cbba4d6 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java @@ -37,7 +37,7 @@ public class TextData extends Operation implements SerializableToString, Seriali private static final int OP_CODE = Operations.DATA_TEXT; private static final String CLASS_NAME = "TextData"; public final int mTextId; - @NonNull public final String mText; + @NonNull public String mText; public static final int MAX_STRING_SIZE = 4000; public TextData(int textId, @NonNull String text) { @@ -45,6 +45,15 @@ public class TextData extends Operation implements SerializableToString, Seriali this.mText = text; } + /** + * Copy the data from another text data + * + * @param from source to copy from + */ + public void update(TextData from) { + mText = from.mText; + } + @Override public void write(@NonNull WireBuffer buffer) { apply(buffer, mTextId, mText); diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java index 76bb96d1b61a..f1158d91f94b 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java @@ -27,6 +27,7 @@ import com.android.internal.widget.remotecompose.core.SerializableToString; import com.android.internal.widget.remotecompose.core.TouchListener; import com.android.internal.widget.remotecompose.core.VariableSupport; import com.android.internal.widget.remotecompose.core.WireBuffer; +import com.android.internal.widget.remotecompose.core.operations.BitmapData; import com.android.internal.widget.remotecompose.core.operations.ComponentValue; import com.android.internal.widget.remotecompose.core.operations.TextData; import com.android.internal.widget.remotecompose.core.operations.TouchExpression; @@ -1131,14 +1132,14 @@ public class Component extends PaintOperation } /** - * Extract child TextData elements + * Extract child Data elements * - * @param data an ArrayList that will be populated with the TextData elements (if any) + * @param data an ArrayList that will be populated with the Data elements (if any) */ - public void getData(@NonNull ArrayList<TextData> data) { + public void getData(@NonNull ArrayList<Operation> data) { for (Operation op : mList) { - if (op instanceof TextData) { - data.add((TextData) op); + if (op instanceof TextData || op instanceof BitmapData) { + data.add(op); } } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java index e57438662012..6163d8099b8c 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java @@ -30,7 +30,6 @@ import com.android.internal.widget.remotecompose.core.operations.MatrixRestore; import com.android.internal.widget.remotecompose.core.operations.MatrixSave; import com.android.internal.widget.remotecompose.core.operations.MatrixTranslate; import com.android.internal.widget.remotecompose.core.operations.PaintData; -import com.android.internal.widget.remotecompose.core.operations.TextData; import com.android.internal.widget.remotecompose.core.operations.TouchExpression; import com.android.internal.widget.remotecompose.core.operations.layout.animation.AnimationSpec; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentModifiers; @@ -138,7 +137,7 @@ public class LayoutComponent extends Component { @Override public void inflate() { - ArrayList<TextData> data = new ArrayList<>(); + ArrayList<Operation> data = new ArrayList<>(); ArrayList<Operation> supportedOperations = new ArrayList<>(); for (Operation op : mList) { @@ -186,8 +185,6 @@ public class LayoutComponent extends Component { ((ScrollModifierOperation) op).inflate(this); } mComponentModifiers.add((ModifierOperation) op); - } else if (op instanceof TextData) { - data.add((TextData) op); } else if (op instanceof TouchExpression || (op instanceof PaintData) || (op instanceof FloatExpression)) { diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ImageLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ImageLayout.java new file mode 100644 index 000000000000..a4ed0c1319d4 --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ImageLayout.java @@ -0,0 +1,314 @@ +/* + * 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.internal.widget.remotecompose.core.operations.layout.managers; + +import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.FLOAT; +import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.widget.remotecompose.core.Operation; +import com.android.internal.widget.remotecompose.core.Operations; +import com.android.internal.widget.remotecompose.core.PaintContext; +import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.VariableSupport; +import com.android.internal.widget.remotecompose.core.WireBuffer; +import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; +import com.android.internal.widget.remotecompose.core.operations.BitmapData; +import com.android.internal.widget.remotecompose.core.operations.layout.Component; +import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure; +import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass; +import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size; +import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle; +import com.android.internal.widget.remotecompose.core.operations.utilities.ImageScaling; +import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer; + +import java.util.List; + +public class ImageLayout extends LayoutManager implements VariableSupport { + + private static final boolean DEBUG = false; + private int mBitmapId = -1; + private int mScaleType; + private float mAlpha = 1f; + + @NonNull ImageScaling mScaling = new ImageScaling(); + @NonNull PaintBundle mPaint = new PaintBundle(); + + @Override + public void registerListening(@NonNull RemoteContext context) { + if (mBitmapId != -1) { + context.listensTo(mBitmapId, this); + } + } + + public ImageLayout( + @Nullable Component parent, + int componentId, + int animationId, + int bitmapId, + float x, + float y, + float width, + float height, + int scaleType, + float alpha) { + super(parent, componentId, animationId, x, y, width, height); + mBitmapId = bitmapId; + mScaleType = scaleType & 0xFF; + mAlpha = alpha; + } + + public ImageLayout( + @Nullable Component parent, + int componentId, + int animationId, + int bitmapId, + int scaleType, + float alpha) { + this(parent, componentId, animationId, bitmapId, 0, 0, 0, 0, scaleType, alpha); + } + + @Override + public void computeWrapSize( + @NonNull PaintContext context, + float maxWidth, + float maxHeight, + boolean horizontalWrap, + boolean verticalWrap, + @NonNull MeasurePass measure, + @NonNull Size size) { + + BitmapData bitmapData = (BitmapData) context.getContext().getObject(mBitmapId); + if (bitmapData != null) { + size.setWidth(bitmapData.getWidth()); + size.setHeight(bitmapData.getHeight()); + } + } + + @Override + public void computeSize( + @NonNull PaintContext context, + float minWidth, + float maxWidth, + float minHeight, + float maxHeight, + @NonNull MeasurePass measure) { + float modifiersWidth = computeModifierDefinedWidth(context.getContext()); + float modifiersHeight = computeModifierDefinedHeight(context.getContext()); + ComponentMeasure m = measure.get(this); + m.setW(modifiersWidth); + m.setH(modifiersHeight); + } + + @Override + public void paintingComponent(@NonNull PaintContext context) { + context.save(); + context.translate(mX, mY); + mComponentModifiers.paint(context); + float tx = mPaddingLeft; + float ty = mPaddingTop; + context.translate(tx, ty); + float w = mWidth - mPaddingLeft - mPaddingRight; + float h = mHeight - mPaddingTop - mPaddingBottom; + context.clipRect(0f, 0f, w, h); + + BitmapData bitmapData = (BitmapData) context.getContext().getObject(mBitmapId); + if (bitmapData != null) { + mScaling.setup( + 0f, + 0f, + bitmapData.getWidth(), + bitmapData.getHeight(), + 0f, + 0f, + w, + h, + mScaleType, + 1f); + + context.savePaint(); + if (mAlpha == 1f) { + context.drawBitmap( + mBitmapId, + (int) 0f, + (int) 0f, + (int) bitmapData.getWidth(), + (int) bitmapData.getHeight(), + (int) mScaling.mFinalDstLeft, + (int) mScaling.mFinalDstTop, + (int) mScaling.mFinalDstRight, + (int) mScaling.mFinalDstBottom, + -1); + } else { + context.savePaint(); + mPaint.reset(); + mPaint.setColor(0f, 0f, 0f, mAlpha); + context.applyPaint(mPaint); + context.drawBitmap( + mBitmapId, + (int) 0f, + (int) 0f, + (int) bitmapData.getWidth(), + (int) bitmapData.getHeight(), + (int) mScaling.mFinalDstLeft, + (int) mScaling.mFinalDstTop, + (int) mScaling.mFinalDstRight, + (int) mScaling.mFinalDstBottom, + -1); + context.restorePaint(); + } + context.restorePaint(); + } + + // debugBox(this, context); + context.translate(-tx, -ty); + context.restore(); + } + + @NonNull + @Override + public String toString() { + return "IMAGE_LAYOUT [" + + mComponentId + + ":" + + mAnimationId + + "] (" + + mX + + ", " + + mY + + " - " + + mWidth + + " x " + + mHeight + + ") " + + mVisibility; + } + + @NonNull + @Override + protected String getSerializedName() { + return "IMAGE_LAYOUT"; + } + + @Override + public void serializeToString(int indent, @NonNull StringSerializer serializer) { + serializer.append( + indent, + getSerializedName() + + " [" + + mComponentId + + ":" + + mAnimationId + + "] = " + + "[" + + mX + + ", " + + mY + + ", " + + mWidth + + ", " + + mHeight + + "] " + + mVisibility + + " (" + + mBitmapId + + "\")"); + } + + /** + * The name of the class + * + * @return the name + */ + @NonNull + public static String name() { + return "ImageLayout"; + } + + /** + * The OP_CODE for this command + * + * @return the opcode + */ + public static int id() { + return Operations.LAYOUT_IMAGE; + } + + /** + * Write the operation to the buffer + * + * @param buffer + * @param componentId + * @param animationId + * @param bitmapId + * @param scaleType + * @param alpha + */ + public static void apply( + @NonNull WireBuffer buffer, + int componentId, + int animationId, + int bitmapId, + int scaleType, + float alpha) { + buffer.start(id()); + buffer.writeInt(componentId); + buffer.writeInt(animationId); + buffer.writeInt(bitmapId); + buffer.writeInt(scaleType); + buffer.writeFloat(alpha); + } + + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ + public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { + int componentId = buffer.readInt(); + int animationId = buffer.readInt(); + int bitmapId = buffer.readInt(); + int scaleType = buffer.readInt(); + float alpha = buffer.readFloat(); + operations.add(new ImageLayout(null, componentId, animationId, bitmapId, scaleType, alpha)); + } + + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ + public static void documentation(@NonNull DocumentationBuilder doc) { + doc.operation("Layout Operations", id(), name()) + .description("Image layout implementation.\n\n") + .field(INT, "COMPONENT_ID", "unique id for this component") + .field( + INT, + "ANIMATION_ID", + "id used to match components," + " for animation purposes") + .field(INT, "BITMAP_ID", "bitmap id") + .field(INT, "SCALE_TYPE", "scale type") + .field(FLOAT, "ALPHA", "alpha"); + } + + @Override + public void write(@NonNull WireBuffer buffer) { + apply(buffer, mComponentId, mAnimationId, mBitmapId, mScaleType, mAlpha); + } +} diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java index 656a3c0fca68..b3d76b765143 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java @@ -250,7 +250,7 @@ public class BorderModifierOperation extends DecoratorModifierOperation { context.savePaint(); paint.reset(); paint.setColor(mR, mG, mB, mA); - paint.setStrokeWidth(mBorderWidth); + paint.setStrokeWidth(mBorderWidth * context.getContext().getDensity()); paint.setStyle(PaintBundle.STYLE_STROKE); context.replacePaint(paint); if (mShapeType == ShapeType.RECTANGLE) { diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostActionMetadataOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostActionMetadataOperation.java new file mode 100644 index 000000000000..2170efd68a64 --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostActionMetadataOperation.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.widget.remotecompose.core.operations.layout.modifiers; + +import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT; + +import android.annotation.NonNull; + +import com.android.internal.widget.remotecompose.core.CoreDocument; +import com.android.internal.widget.remotecompose.core.Operation; +import com.android.internal.widget.remotecompose.core.Operations; +import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.SerializableToString; +import com.android.internal.widget.remotecompose.core.WireBuffer; +import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; +import com.android.internal.widget.remotecompose.core.operations.layout.ActionOperation; +import com.android.internal.widget.remotecompose.core.operations.layout.Component; +import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; +import com.android.internal.widget.remotecompose.core.serialize.SerializeTags; + +import java.util.List; + +/** Capture a host action information. This can be triggered on eg. a click. */ +public class HostActionMetadataOperation extends Operation + implements ActionOperation, SerializableToString, Serializable { + private static final int OP_CODE = Operations.HOST_METADATA_ACTION; + + int mActionId = -1; + int mMetadataId = -1; + + public HostActionMetadataOperation(int id, int metadataId) { + mActionId = id; + mMetadataId = metadataId; + } + + @NonNull + @Override + public String toString() { + return "HostMetadataActionOperation(" + mActionId + ":" + mMetadataId + ")"; + } + + public int getActionId() { + return mActionId; + } + + /** + * Returns the serialized name for this operation + * + * @return the serialized name + */ + @NonNull + public String serializedName() { + return "HOST_METADATA_ACTION"; + } + + @Override + public void serializeToString(int indent, @NonNull StringSerializer serializer) { + serializer.append(indent, serializedName() + " = " + mActionId + ", " + mMetadataId); + } + + @Override + public void apply(@NonNull RemoteContext context) {} + + @NonNull + @Override + public String deepToString(@NonNull String indent) { + return (indent != null ? indent : "") + toString(); + } + + @Override + public void write(@NonNull WireBuffer buffer) {} + + @Override + public void runAction( + @NonNull RemoteContext context, + @NonNull CoreDocument document, + @NonNull Component component, + float x, + float y) { + String metadata = context.getText(mMetadataId); + if (metadata == null) { + metadata = ""; + } + context.runAction(mActionId, metadata); + } + + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param actionId the action id + */ + public static void apply(@NonNull WireBuffer buffer, int actionId, int metadataId) { + buffer.start(OP_CODE); + buffer.writeInt(actionId); + buffer.writeInt(metadataId); + } + + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ + public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { + int actionId = buffer.readInt(); + int metadataId = buffer.readInt(); + operations.add(new HostActionMetadataOperation(actionId, metadataId)); + } + + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ + public static void documentation(@NonNull DocumentationBuilder doc) { + doc.operation("Layout Operations", OP_CODE, "HostAction") + .description("Host action. This operation represents a host action") + .field(INT, "ACTION_ID", "Host Action ID") + .field(INT, "METADATA", "Host Action Text Metadata ID"); + } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .addTags(SerializeTags.MODIFIER) + .addType("HostActionOperation") + .add("id", mActionId) + .add("metadata", mMetadataId); + } +} diff --git a/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java b/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java index bdc765968387..25dcb67fe9e2 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java +++ b/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java @@ -34,14 +34,23 @@ import java.util.List; public class IntegerConstant extends Operation implements Serializable { private static final String CLASS_NAME = "IntegerConstant"; - private final int mValue; - private final int mId; + private int mValue; + public final int mId; IntegerConstant(int id, int value) { mId = id; mValue = value; } + /** + * Updates the value of the integer constant + * + * @param ic the integer constant to copy + */ + public void update(IntegerConstant ic) { + mValue = ic.mValue; + } + @Override public void write(@NonNull WireBuffer buffer) { apply(buffer, mId, mValue); diff --git a/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java b/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java index d071e0a21d22..ab0f7352182a 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java +++ b/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java @@ -36,7 +36,7 @@ public class LongConstant extends Operation implements Serializable { private static final int OP_CODE = Operations.DATA_LONG; private long mValue; - private final int mId; + public final int mId; /** * @param id the id of the constant @@ -48,6 +48,15 @@ public class LongConstant extends Operation implements Serializable { } /** + * Copy the value from another longConstant + * + * @param from the constant to copy from + */ + public void update(LongConstant from) { + mValue = from.mValue; + } + + /** * Get the value of the long constant * * @return the value of the long diff --git a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java index 1f9a27429067..f5b2cca15e43 100644 --- a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java +++ b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java @@ -41,6 +41,7 @@ import com.android.internal.widget.remotecompose.core.RemoteContext; import com.android.internal.widget.remotecompose.core.RemoteContextAware; import com.android.internal.widget.remotecompose.core.operations.NamedVariable; import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior; +import com.android.internal.widget.remotecompose.player.platform.AndroidRemoteContext; import com.android.internal.widget.remotecompose.player.platform.RemoteComposeCanvas; /** A view to to display and play RemoteCompose documents */ @@ -114,6 +115,23 @@ public class RemoteComposePlayer extends FrameLayout implements RemoteContextAwa return mInner.getDocument(); } + /** + * This will update values in the already loaded document. + * + * @param value the document to update variables in the current document width + */ + public void updateDocument(RemoteComposeDocument value) { + RemoteComposeDocument document = value; + AndroidRemoteContext tmpContext = new AndroidRemoteContext(); + document.initializeContext(tmpContext); + float density = getContext().getResources().getDisplayMetrics().density; + tmpContext.setAnimationEnabled(true); + tmpContext.setDensity(density); + tmpContext.setUseChoreographer(false); + mInner.getDocument().mDocument.applyUpdate(document.mDocument); + mInner.invalidate(); + } + public void setDocument(RemoteComposeDocument value) { if (value != null) { if (value.canBeDisplayed( @@ -312,7 +330,8 @@ public class RemoteComposePlayer extends FrameLayout implements RemoteContextAwa } /** - * Add a callback for handling id actions events on the document + * Add a callback for handling id actions events on the document. Can only be added after the + * document has been loaded. * * @param callback the callback lambda that will be used when a action is executed * <p>The parameter of the callback are: diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java index ad2414974780..e1f2924021a4 100644 --- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java +++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java @@ -197,7 +197,7 @@ public class AndroidRemoteContext extends RemoteContext { @Override public void runAction(int id, @NonNull String metadata) { - mDocument.performClick(this, id); + mDocument.performClick(this, id, metadata); } @Override diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java index 29cd40def562..0bc99abc17bc 100644 --- a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java +++ b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java @@ -154,7 +154,11 @@ public class RemoteComposeCanvas extends FrameLayout implements View.OnAttachSta param.leftMargin = (int) area.getLeft(); param.topMargin = (int) area.getTop(); viewArea.setOnClickListener( - view1 -> mDocument.getDocument().performClick(mARContext, area.getId())); + view1 -> + mDocument + .getDocument() + .performClick( + mARContext, area.getId(), area.getMetadata())); addView(viewArea, param); } if (!clickAreas.isEmpty()) { diff --git a/core/jni/android_view_InputChannel.cpp b/core/jni/android_view_InputChannel.cpp index e874163943b6..a6a748c3deb7 100644 --- a/core/jni/android_view_InputChannel.cpp +++ b/core/jni/android_view_InputChannel.cpp @@ -55,41 +55,25 @@ public: inline std::shared_ptr<InputChannel> getInputChannel() { return mInputChannel; } - void setDisposeCallback(InputChannelObjDisposeCallback callback, void* data); void dispose(JNIEnv* env, jobject obj); private: std::shared_ptr<InputChannel> mInputChannel; - InputChannelObjDisposeCallback mDisposeCallback; - void* mDisposeData; }; // ---------------------------------------------------------------------------- NativeInputChannel::NativeInputChannel(std::unique_ptr<InputChannel> inputChannel) - : mInputChannel(std::move(inputChannel)), mDisposeCallback(nullptr) {} + : mInputChannel(std::move(inputChannel)) {} NativeInputChannel::~NativeInputChannel() { } -void NativeInputChannel::setDisposeCallback(InputChannelObjDisposeCallback callback, void* data) { - if (input_flags::remove_input_channel_from_windowstate()) { - return; - } - mDisposeCallback = callback; - mDisposeData = data; -} - void NativeInputChannel::dispose(JNIEnv* env, jobject obj) { if (!mInputChannel) { return; } - if (mDisposeCallback) { - mDisposeCallback(env, obj, mInputChannel, mDisposeData); - mDisposeCallback = nullptr; - mDisposeData = nullptr; - } mInputChannel.reset(); } @@ -108,17 +92,6 @@ std::shared_ptr<InputChannel> android_view_InputChannel_getInputChannel(JNIEnv* return nativeInputChannel != nullptr ? nativeInputChannel->getInputChannel() : nullptr; } -void android_view_InputChannel_setDisposeCallback(JNIEnv* env, jobject inputChannelObj, - InputChannelObjDisposeCallback callback, void* data) { - NativeInputChannel* nativeInputChannel = - android_view_InputChannel_getNativeInputChannel(env, inputChannelObj); - if (!nativeInputChannel || !nativeInputChannel->getInputChannel()) { - ALOGW("Cannot set dispose callback because input channel object has not been initialized."); - } else { - nativeInputChannel->setDisposeCallback(callback, data); - } -} - static jlong android_view_InputChannel_createInputChannel( JNIEnv* env, std::unique_ptr<InputChannel> inputChannel) { std::unique_ptr<NativeInputChannel> nativeInputChannel = diff --git a/libs/WindowManager/Shell/aconfig/OWNERS b/libs/WindowManager/Shell/aconfig/OWNERS index 9eba0f2dea7b..eacadeacab36 100644 --- a/libs/WindowManager/Shell/aconfig/OWNERS +++ b/libs/WindowManager/Shell/aconfig/OWNERS @@ -1,3 +1,4 @@ # Owners for flag changes madym@google.com -hwwang@google.com
\ No newline at end of file +hwwang@google.com +sqsun@google.com
\ No newline at end of file diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index ab2804626361..592c7cdd070c 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -203,4 +203,11 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + +flag { + name: "enable_magnetic_split_divider" + namespace: "multitasking" + description: "Makes the split divider snap 'magnetically' to available snap points during drag" + bug: "383631946" +} diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubject.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubject.kt index 2d6df43f67e0..3597ce0041d4 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubject.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubject.kt @@ -22,14 +22,14 @@ import com.google.common.truth.Subject import com.google.common.truth.Truth /** Subclass of [Subject] to simplify verifying [FakeUiEvent] data */ -class UiEventSubject(metadata: FailureMetadata, private val actual: FakeUiEvent) : +class UiEventSubject(metadata: FailureMetadata, private val actual: FakeUiEvent?) : Subject(metadata, actual) { /** Check that [FakeUiEvent] contains the expected data from the [bubble] passed id */ fun hasBubbleInfo(bubble: Bubble) { - check("uid").that(actual.uid).isEqualTo(bubble.appUid) - check("packageName").that(actual.packageName).isEqualTo(bubble.packageName) - check("instanceId").that(actual.instanceId).isEqualTo(bubble.instanceId) + check("uid").that(actual?.uid).isEqualTo(bubble.appUid) + check("packageName").that(actual?.packageName).isEqualTo(bubble.packageName) + check("instanceId").that(actual?.instanceId).isEqualTo(bubble.instanceId) } companion object { diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_header_ic_minimize.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_header_ic_minimize.xml index b35dc022e210..56e5dc717a7b 100644 --- a/libs/WindowManager/Shell/res/drawable/desktop_mode_header_ic_minimize.xml +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_header_ic_minimize.xml @@ -18,9 +18,9 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" - android:viewportHeight="24" - android:viewportWidth="24"> + android:viewportWidth="960" + android:viewportHeight="960"> <path android:fillColor="#FF000000" - android:pathData="M6,21V19H18V21Z"/> + android:pathData="M160,800L160,720L800,720L800,800L160,800Z"/> </vector> diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index 16e098b39004..e11babe5cb0e 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -39,8 +39,8 @@ <ImageView android:id="@+id/application_icon" - android:layout_width="@dimen/desktop_mode_caption_icon_radius" - android:layout_height="@dimen/desktop_mode_caption_icon_radius" + android:layout_width="@dimen/desktop_mode_handle_menu_icon_radius" + android:layout_height="@dimen/desktop_mode_handle_menu_icon_radius" android:layout_marginStart="10dp" android:layout_marginEnd="12dp" android:contentDescription="@string/app_icon_text" diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index b8f0e3d40f08..9ebbf71138b0 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -570,7 +570,10 @@ <dimen name="desktop_mode_handle_menu_corner_radius">26dp</dimen> <!-- The radius of the caption menu icon. --> - <dimen name="desktop_mode_caption_icon_radius">32dp</dimen> + <dimen name="desktop_mode_caption_icon_radius">24dp</dimen> + + <!-- The radius of the icon in the header menu's app info pill. --> + <dimen name="desktop_mode_handle_menu_icon_radius">32dp</dimen> <!-- The radius of the caption menu shadow. --> <dimen name="desktop_mode_handle_menu_shadow_radius">2dp</dimen> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt index 9ea0532f9450..b87c2054bea6 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt @@ -19,13 +19,10 @@ package com.android.wm.shell.shared.desktopmode import android.Manifest.permission.SYSTEM_ALERT_WINDOW import android.app.TaskInfo import android.content.Context -import android.content.pm.ActivityInfo -import android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED -import android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION -import android.content.pm.ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS import android.content.pm.PackageManager import android.window.DesktopModeFlags import com.android.internal.R +import com.android.internal.policy.DesktopModeCompatUtils /** * Class to decide whether to apply app compat policies in desktop mode. @@ -60,22 +57,11 @@ class DesktopModeCompatPolicy(private val context: Context) { hasFullscreenTransparentPermission(packageName))) && !isTopActivityNoDisplay) - /** - * Whether the caption insets should be excluded from configuration for system to handle. - * - * The treatment is enabled when all the of the following is true: - * * Any flags to forcibly consume caption insets are enabled. - * * Top activity have configuration coupled with insets. - * * Task is not resizeable or [ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS] - * is enabled. - */ + /** @see DesktopModeCompatUtils.shouldExcludeCaptionFromAppBounds */ fun shouldExcludeCaptionFromAppBounds(taskInfo: TaskInfo): Boolean = - DesktopModeFlags.EXCLUDE_CAPTION_FROM_APP_BOUNDS.isTrue - && isAnyForceConsumptionFlagsEnabled() - && taskInfo.topActivityInfo?.let { - isInsetsCoupledWithConfiguration(it) && (!taskInfo.isResizeable || it.isChangeEnabled( - OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS - )) + taskInfo.topActivityInfo?.let { + DesktopModeCompatUtils.shouldExcludeCaptionFromAppBounds(it, taskInfo.isResizeable, + taskInfo.appCompatTaskInfo.hasOptOutEdgeToEdge()) } ?: false /** @@ -118,12 +104,4 @@ class DesktopModeCompatPolicy(private val context: Context) { */ private fun isPartOfDefaultHomePackageOrNoHomeAvailable(packageName: String?) = defaultHomePackage == null || (packageName != null && packageName == defaultHomePackage) - - private fun isAnyForceConsumptionFlagsEnabled(): Boolean = - DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS.isTrue - || DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isTrue - - private fun isInsetsCoupledWithConfiguration(info: ActivityInfo): Boolean = - !(info.isChangeEnabled(OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION) - || info.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED)) } 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 bc2ed3f35b45..179f03b8a975 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 @@ -98,6 +98,7 @@ import com.android.wm.shell.desktopmode.DesktopModeKeyGestureHandler; import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; import com.android.wm.shell.desktopmode.DesktopModeMoveToDisplayTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger; +import com.android.wm.shell.desktopmode.DesktopPipTransitionObserver; import com.android.wm.shell.desktopmode.DesktopTaskChangeListener; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopTasksLimiter; @@ -780,6 +781,7 @@ public abstract class WMShellModule { OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver, DesksOrganizer desksOrganizer, Optional<DesksTransitionObserver> desksTransitionObserver, + Optional<DesktopPipTransitionObserver> desktopPipTransitionObserver, UserProfileContexts userProfileContexts, DesktopModeCompatPolicy desktopModeCompatPolicy, DragToDisplayTransitionHandler dragToDisplayTransitionHandler, @@ -823,6 +825,7 @@ public abstract class WMShellModule { overviewToDesktopTransitionObserver, desksOrganizer, desksTransitionObserver.get(), + desktopPipTransitionObserver.get(), userProfileContexts, desktopModeCompatPolicy, dragToDisplayTransitionHandler, @@ -1225,6 +1228,7 @@ public abstract class WMShellModule { Transitions transitions, ShellTaskOrganizer shellTaskOrganizer, Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler, + Optional<DesktopPipTransitionObserver> desktopPipTransitionObserver, Optional<BackAnimationController> backAnimationController, DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, ShellInit shellInit) { @@ -1237,6 +1241,7 @@ public abstract class WMShellModule { transitions, shellTaskOrganizer, desktopMixedTransitionHandler.get(), + desktopPipTransitionObserver.get(), backAnimationController.get(), desktopWallpaperActivityTokenProvider, shellInit))); @@ -1258,6 +1263,19 @@ public abstract class WMShellModule { @WMSingleton @Provides + static Optional<DesktopPipTransitionObserver> provideDesktopPipTransitionObserver( + Context context + ) { + if (DesktopModeStatus.canEnterDesktopMode(context) + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue()) { + return Optional.of( + new DesktopPipTransitionObserver()); + } + return Optional.empty(); + } + + @WMSingleton + @Provides static Optional<DesktopMixedTransitionHandler> provideDesktopMixedTransitionHandler( Context context, Transitions transitions, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserver.kt new file mode 100644 index 000000000000..efd3866e1bc4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserver.kt @@ -0,0 +1,81 @@ +/* + * 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.WINDOWING_MODE_PINNED +import android.os.IBinder +import android.window.DesktopModeFlags +import android.window.TransitionInfo +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE + +/** + * Observer of PiP in Desktop Mode transitions. At the moment, this is specifically tracking a PiP + * transition for a task that is entering PiP via the minimize button on the caption bar. + */ +class DesktopPipTransitionObserver { + private val pendingPipTransitions = mutableMapOf<IBinder, PendingPipTransition>() + + /** Adds a pending PiP transition to be tracked. */ + fun addPendingPipTransition(transition: PendingPipTransition) { + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue) return + pendingPipTransitions[transition.token] = transition + } + + /** + * Called when any transition is ready, which may include transitions not tracked by this + * observer. + */ + fun onTransitionReady(transition: IBinder, info: TransitionInfo) { + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue) return + val pipTransition = pendingPipTransitions.remove(transition) ?: return + + logD("Desktop PiP transition ready: %s", transition) + for (change in info.changes) { + val taskInfo = change.taskInfo + if (taskInfo == null || taskInfo.taskId == -1) { + continue + } + + if ( + taskInfo.taskId == pipTransition.taskId && + taskInfo.windowingMode == WINDOWING_MODE_PINNED + ) { + logD("Desktop PiP transition was successful") + pipTransition.onSuccess() + return + } + } + logD("Change with PiP task not found in Desktop PiP transition; likely failed") + } + + /** + * Data tracked for a pending PiP transition. + * + * @property token the PiP transition that is started. + * @property taskId task id of the task entering PiP. + * @property onSuccess callback to be invoked if the PiP transition is successful. + */ + data class PendingPipTransition(val token: IBinder, val taskId: Int, val onSuccess: () -> Unit) + + private fun logD(msg: String, vararg arguments: Any?) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + private companion object { + private const val TAG = "DesktopPipTransitionObserver" + } +} 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 8636bc1f56c2..0c2ee4648a43 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 @@ -68,7 +68,6 @@ class DesktopRepository( * @property topTransparentFullscreenTaskId the task id of any current top transparent * fullscreen task launched on top of the desk. Cleared when the transparent task is closed or * sent to back. (top is at index 0). - * @property pipTaskId the task id of PiP task entered while in Desktop Mode. */ private data class Desk( val deskId: Int, @@ -81,7 +80,6 @@ class DesktopRepository( val freeformTasksInZOrder: ArrayList<Int> = ArrayList(), var fullImmersiveTaskId: Int? = null, var topTransparentFullscreenTaskId: Int? = null, - var pipTaskId: Int? = null, ) { fun deepCopy(): Desk = Desk( @@ -94,7 +92,6 @@ class DesktopRepository( freeformTasksInZOrder = ArrayList(freeformTasksInZOrder), fullImmersiveTaskId = fullImmersiveTaskId, topTransparentFullscreenTaskId = topTransparentFullscreenTaskId, - pipTaskId = pipTaskId, ) // TODO: b/362720497 - remove when multi-desktops is enabled where instances aren't @@ -107,7 +104,6 @@ class DesktopRepository( freeformTasksInZOrder.clear() fullImmersiveTaskId = null topTransparentFullscreenTaskId = null - pipTaskId = null } } @@ -127,9 +123,6 @@ class DesktopRepository( /* Tracks last bounds of task before toggled to immersive state. */ private val boundsBeforeFullImmersiveByTaskId = SparseArray<Rect>() - /* Callback for when a pending PiP transition has been aborted. */ - private var onPipAbortedCallback: ((Int, Int) -> Unit)? = null - private var desktopGestureExclusionListener: Consumer<Region>? = null private var desktopGestureExclusionExecutor: Executor? = null @@ -611,57 +604,6 @@ class DesktopRepository( } /** - * Set whether the given task is the Desktop-entered PiP task in this display's active desk. - * - * TODO: b/389960283 - add explicit [deskId] argument. - */ - fun setTaskInPip(displayId: Int, taskId: Int, enterPip: Boolean) { - val activeDesk = - desktopData.getActiveDesk(displayId) - ?: error("Expected active desk in display: $displayId") - if (enterPip) { - activeDesk.pipTaskId = taskId - } else { - activeDesk.pipTaskId = - if (activeDesk.pipTaskId == taskId) null - else { - logW( - "setTaskInPip: taskId=%d did not match saved taskId=%d", - taskId, - activeDesk.pipTaskId, - ) - activeDesk.pipTaskId - } - } - } - - /** - * Returns whether the given task is the Desktop-entered PiP task in this display's active desk. - * - * TODO: b/389960283 - add explicit [deskId] argument. - */ - fun isTaskMinimizedPipInDisplay(displayId: Int, taskId: Int): Boolean = - desktopData.getActiveDesk(displayId)?.pipTaskId == taskId - - /** - * Saves callback to handle a pending PiP transition being aborted. - * - * TODO: b/389960283 - add explicit [deskId] argument. - */ - fun setOnPipAbortedCallback(callbackIfPipAborted: ((displayId: Int, pipTaskId: Int) -> Unit)?) { - onPipAbortedCallback = callbackIfPipAborted - } - - /** - * Invokes callback to handle a pending PiP transition with the given task id being aborted. - * - * TODO: b/389960283 - add explicit [deskId] argument. - */ - fun onPipAborted(displayId: Int, pipTaskId: Int) { - onPipAbortedCallback?.invoke(displayId, pipTaskId) - } - - /** * Set whether the given task is the full-immersive task in this display's active desk. * * TODO: b/389960283 - consider forcing callers to use [setTaskInFullImmersiveStateInDesk] with diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index c28fdcb44f5b..1f0774c24143 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -215,6 +215,7 @@ class DesktopTasksController( private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver, private val desksOrganizer: DesksOrganizer, private val desksTransitionObserver: DesksTransitionObserver, + private val desktopPipTransitionObserver: DesktopPipTransitionObserver, private val userProfileContexts: UserProfileContexts, private val desktopModeCompatPolicy: DesktopModeCompatPolicy, private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler, @@ -788,10 +789,30 @@ class DesktopTasksController( fun minimizeTask(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) { val wct = WindowContainerTransaction() - + val taskId = taskInfo.taskId + val displayId = taskInfo.displayId + val deskId = + taskRepository.getDeskIdForTask(taskInfo.taskId) + ?: if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + logW("minimizeTask: desk not found for task: ${taskInfo.taskId}") + return + } else { + getDefaultDeskId(taskInfo.displayId) + } + val isLastTask = + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + taskRepository.isOnlyVisibleNonClosingTaskInDesk( + taskId = taskId, + deskId = checkNotNull(deskId) { "Expected non-null deskId" }, + displayId = displayId, + ) + } else { + taskRepository.isOnlyVisibleNonClosingTask(taskId = taskId, displayId = displayId) + } val isMinimizingToPip = DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue && - (taskInfo.pictureInPictureParams?.isAutoEnterEnabled() ?: false) + (taskInfo.pictureInPictureParams?.isAutoEnterEnabled ?: false) + // If task is going to PiP, start a PiP transition instead of a minimize transition if (isMinimizingToPip) { val requestInfo = @@ -805,75 +826,60 @@ class DesktopTasksController( ) val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null) wct.merge(requestRes.second, true) - freeformTaskTransitionStarter.startPipTransition(wct) - taskRepository.setTaskInPip(taskInfo.displayId, taskInfo.taskId, enterPip = true) - taskRepository.setOnPipAbortedCallback { displayId, taskId -> - minimizeTaskInner(shellTaskOrganizer.getRunningTaskInfo(taskId)!!, minimizeReason) - taskRepository.setTaskInPip(displayId, taskId, enterPip = false) - } - return - } - - minimizeTaskInner(taskInfo, minimizeReason) - } - private fun minimizeTaskInner(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) { - val taskId = taskInfo.taskId - val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) - if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { - logW("minimizeTaskInner: desk not found for task: ${taskInfo.taskId}") - return - } - val displayId = taskInfo.displayId - val wct = WindowContainerTransaction() - - snapEventHandler.removeTaskIfTiled(displayId, taskId) - val willExitDesktop = willExitDesktop(taskId, displayId, forceExitDesktop = false) - val desktopExitRunnable = - performDesktopExitCleanUp( - wct = wct, - deskId = deskId, - displayId = displayId, - willExitDesktop = willExitDesktop, - ) - // Notify immersive handler as it might need to exit immersive state. - val exitResult = - desktopImmersiveController.exitImmersiveIfApplicable( - wct = wct, - taskInfo = taskInfo, - reason = DesktopImmersiveController.ExitReason.MINIMIZED, - ) - if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { - desksOrganizer.minimizeTask( - wct = wct, - deskId = checkNotNull(deskId) { "Expected non-null deskId" }, - task = taskInfo, + desktopPipTransitionObserver.addPendingPipTransition( + DesktopPipTransitionObserver.PendingPipTransition( + token = freeformTaskTransitionStarter.startPipTransition(wct), + taskId = taskInfo.taskId, + onSuccess = { + onDesktopTaskEnteredPip( + taskId = taskId, + deskId = deskId, + displayId = taskInfo.displayId, + taskIsLastVisibleTaskBeforePip = isLastTask, + ) + }, + ) ) } else { - wct.reorder(taskInfo.token, /* onTop= */ false) - } - val isLastTask = + snapEventHandler.removeTaskIfTiled(displayId, taskId) + val willExitDesktop = willExitDesktop(taskId, displayId, forceExitDesktop = false) + val desktopExitRunnable = + performDesktopExitCleanUp( + wct = wct, + deskId = deskId, + displayId = displayId, + willExitDesktop = willExitDesktop, + ) + // Notify immersive handler as it might need to exit immersive state. + val exitResult = + desktopImmersiveController.exitImmersiveIfApplicable( + wct = wct, + taskInfo = taskInfo, + reason = DesktopImmersiveController.ExitReason.MINIMIZED, + ) if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { - taskRepository.isOnlyVisibleNonClosingTaskInDesk( - taskId = taskId, + desksOrganizer.minimizeTask( + wct = wct, deskId = checkNotNull(deskId) { "Expected non-null deskId" }, - displayId = displayId, + task = taskInfo, ) } else { - taskRepository.isOnlyVisibleNonClosingTask(taskId = taskId, displayId = displayId) + wct.reorder(taskInfo.token, /* onTop= */ false) } - val transition = - freeformTaskTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask) - desktopTasksLimiter.ifPresent { - it.addPendingMinimizeChange( - transition = transition, - displayId = displayId, - taskId = taskId, - minimizeReason = minimizeReason, - ) + val transition = + freeformTaskTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask) + desktopTasksLimiter.ifPresent { + it.addPendingMinimizeChange( + transition = transition, + displayId = displayId, + taskId = taskId, + minimizeReason = minimizeReason, + ) + } + exitResult.asExit()?.runOnTransitionStart?.invoke(transition) + desktopExitRunnable?.invoke(transition) } - exitResult.asExit()?.runOnTransitionStart?.invoke(transition) - desktopExitRunnable?.invoke(transition) } /** Move a task with given `taskId` to fullscreen */ @@ -1841,7 +1847,11 @@ class DesktopTasksController( displayId: Int, forceExitDesktop: Boolean, ): Boolean { - if (forceExitDesktop && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + if ( + forceExitDesktop && + (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue || + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue) + ) { // |forceExitDesktop| is true when the callers knows we'll exit desktop, such as when // explicitly going fullscreen, so there's no point in checking the desktop state. return true @@ -1858,6 +1868,33 @@ class DesktopTasksController( return true } + /** Potentially perform Desktop cleanup after a task successfully enters PiP. */ + @VisibleForTesting + fun onDesktopTaskEnteredPip( + taskId: Int, + deskId: Int, + displayId: Int, + taskIsLastVisibleTaskBeforePip: Boolean, + ) { + if ( + !willExitDesktop(taskId, displayId, forceExitDesktop = taskIsLastVisibleTaskBeforePip) + ) { + return + } + + val wct = WindowContainerTransaction() + val desktopExitRunnable = + performDesktopExitCleanUp( + wct = wct, + deskId = deskId, + displayId = displayId, + willExitDesktop = true, + ) + + val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) + desktopExitRunnable?.invoke(transition) + } + private fun performDesktopExitCleanupIfNeeded( taskId: Int, deskId: Int? = null, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index 7dabeb7c9d15..c670ac3c4488 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -23,7 +23,6 @@ import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_OPEN -import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_BACK import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags @@ -41,8 +40,6 @@ import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions -import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP -import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP /** * A [Transitions.TransitionObserver] that observes shell transitions and updates the @@ -55,6 +52,7 @@ class DesktopTasksTransitionObserver( private val transitions: Transitions, private val shellTaskOrganizer: ShellTaskOrganizer, private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler, + private val desktopPipTransitionObserver: DesktopPipTransitionObserver, private val backAnimationController: BackAnimationController, private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, shellInit: ShellInit, @@ -63,8 +61,6 @@ class DesktopTasksTransitionObserver( data class CloseWallpaperTransition(val transition: IBinder, val displayId: Int) private var transitionToCloseWallpaper: CloseWallpaperTransition? = null - /* Pending PiP transition and its associated display id and task id. */ - private var pendingPipTransitionAndPipTask: Triple<IBinder, Int, Int>? = null private var currentProfileId: Int init { @@ -98,33 +94,7 @@ class DesktopTasksTransitionObserver( removeTaskIfNeeded(info) } removeWallpaperOnLastTaskClosingIfNeeded(transition, info) - - val desktopRepository = desktopUserRepositories.getProfile(currentProfileId) - info.changes.forEach { change -> - change.taskInfo?.let { taskInfo -> - if ( - DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue && - desktopRepository.isTaskMinimizedPipInDisplay( - taskInfo.displayId, - taskInfo.taskId, - ) - ) { - when (info.type) { - TRANSIT_PIP -> - pendingPipTransitionAndPipTask = - Triple(transition, taskInfo.displayId, taskInfo.taskId) - - TRANSIT_EXIT_PIP, - TRANSIT_REMOVE_PIP -> - desktopRepository.setTaskInPip( - taskInfo.displayId, - taskInfo.taskId, - enterPip = false, - ) - } - } - } - } + desktopPipTransitionObserver.onTransitionReady(transition, info) } private fun removeTaskIfNeeded(info: TransitionInfo) { @@ -299,18 +269,6 @@ class DesktopTasksTransitionObserver( } } transitionToCloseWallpaper = null - } else if (pendingPipTransitionAndPipTask?.first == transition) { - val desktopRepository = desktopUserRepositories.getProfile(currentProfileId) - if (aborted) { - pendingPipTransitionAndPipTask?.let { - desktopRepository.onPipAborted( - /*displayId=*/ it.second, - /* taskId=*/ it.third, - ) - } - } - desktopRepository.setOnPipAbortedCallback(null) - pendingPipTransitionAndPipTask = null } } 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 275d7b73a112..2aebcdcc3bf5 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 @@ -61,10 +61,10 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var displayController: DisplayController @Mock private lateinit var mockDesktopUserRepositories: DesktopUserRepositories - @Mock private lateinit var mockDesktopRepositoryInitializer: DesktopRepositoryInitializer @Mock private lateinit var mockDesktopRepository: DesktopRepository @Mock private lateinit var mockDesktopTasksController: DesktopTasksController @Mock private lateinit var desktopDisplayModeController: DesktopDisplayModeController + private val desktopRepositoryInitializer = FakeDesktopRepositoryInitializer() private val testScope = TestScope() private lateinit var mockitoSession: StaticMockitoSession @@ -90,7 +90,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { shellInit, testScope.backgroundScope, displayController, - mockDesktopRepositoryInitializer, + desktopRepositoryInitializer, mockDesktopUserRepositories, mockDesktopTasksController, desktopDisplayModeController, @@ -109,12 +109,10 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { @Test fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_createsDesk() = testScope.runTest { - val stateFlow = MutableStateFlow(false) - whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow) whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) - stateFlow.emit(true) + desktopRepositoryInitializer.initialize(mockDesktopUserRepositories) runCurrent() verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY) @@ -123,8 +121,6 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { @Test fun testDisplayAdded_supportsDesks_desktopRepositoryNotInitialized_doesNotCreateDesk() = testScope.runTest { - val stateFlow = MutableStateFlow(false) - whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow) whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) @@ -136,13 +132,11 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { @Test fun testDisplayAdded_supportsDesks_desktopRepositoryInitializedTwice_createsDeskOnce() = testScope.runTest { - val stateFlow = MutableStateFlow(false) - whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow) whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) - stateFlow.emit(true) - stateFlow.emit(true) + desktopRepositoryInitializer.initialize(mockDesktopUserRepositories) + desktopRepositoryInitializer.initialize(mockDesktopUserRepositories) runCurrent() verify(mockDesktopTasksController, times(1)).createDesk(DEFAULT_DISPLAY) @@ -151,13 +145,11 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { @Test fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_deskExists_doesNotCreateDesk() = testScope.runTest { - val stateFlow = MutableStateFlow(false) - whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow) whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(1) onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) - stateFlow.emit(true) + desktopRepositoryInitializer.initialize(mockDesktopUserRepositories) runCurrent() verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY) @@ -203,4 +195,15 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { onDisplaysChangedListenerCaptor.lastValue.onDisplayRemoved(externalDisplayId) verify(desktopDisplayModeController).refreshDisplayWindowingMode() } + + private class FakeDesktopRepositoryInitializer : DesktopRepositoryInitializer { + override var deskRecreationFactory: DesktopRepositoryInitializer.DeskRecreationFactory = + DesktopRepositoryInitializer.DeskRecreationFactory { _, _, deskId -> deskId } + + override val isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false) + + override fun initialize(userRepositories: DesktopUserRepositories) { + isInitialized.value = true + } + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserverTest.kt new file mode 100644 index 000000000000..ef394d81cc57 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserverTest.kt @@ -0,0 +1,98 @@ +/* + * 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.WINDOWING_MODE_PINNED +import android.os.Binder +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.AndroidTestingRunner +import android.view.WindowManager.TRANSIT_PIP +import android.window.TransitionInfo +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +/** + * Tests for [DesktopPipTransitionObserver]. + * + * Build/Install/Run: atest WMShellUnitTests:DesktopPipTransitionObserverTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopPipTransitionObserverTest : ShellTestCase() { + + @JvmField @Rule val setFlagsRule = SetFlagsRule() + + private lateinit var observer: DesktopPipTransitionObserver + + private val transition = Binder() + private var onSuccessInvokedCount = 0 + + @Before + fun setUp() { + observer = DesktopPipTransitionObserver() + + onSuccessInvokedCount = 0 + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun onTransitionReady_taskInPinnedWindowingMode_onSuccessInvoked() { + val taskId = 1 + val pipTransition = createPendingPipTransition(taskId) + val successfulChange = createChange(taskId, WINDOWING_MODE_PINNED) + observer.addPendingPipTransition(pipTransition) + + observer.onTransitionReady( + transition = transition, + info = TransitionInfo( + TRANSIT_PIP, /* flags= */ + 0 + ).apply { addChange(successfulChange) }, + ) + + assertThat(onSuccessInvokedCount).isEqualTo(1) + } + + private fun createPendingPipTransition( + taskId: Int + ): DesktopPipTransitionObserver.PendingPipTransition { + return DesktopPipTransitionObserver.PendingPipTransition( + token = transition, + taskId = taskId, + onSuccess = { onSuccessInvokedCount += 1 }, + ) + } + + private fun createChange(taskId: Int, windowingMode: Int): TransitionInfo.Change { + return TransitionInfo.Change(mock(), mock()).apply { + taskInfo = + TestRunningTaskInfoBuilder() + .setTaskId(taskId) + .setWindowingMode(windowingMode) + .build() + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index f84a1a38bdfc..3813f752cae8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -1225,36 +1225,6 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { } @Test - fun setTaskInPip_savedAsMinimizedPipInDisplay() { - assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse() - - repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) - - assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue() - } - - @Test - fun removeTaskInPip_removedAsMinimizedPipInDisplay() { - repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) - assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue() - - repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = false) - - assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse() - } - - @Test - fun setTaskInPip_multipleDisplays_bothAreInPip() { - repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) - repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) - repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) - repo.setTaskInPip(SECOND_DISPLAY, taskId = 2, enterPip = true) - - assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue() - assertThat(repo.isTaskMinimizedPipInDisplay(SECOND_DISPLAY, taskId = 2)).isTrue() - } - - @Test @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun addTask_deskDoesNotExists_createsDesk() { repo.addTask(displayId = 999, taskId = 6, isVisible = true) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index b2553b0bd30f..14af57372ed6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -264,6 +264,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Mock private lateinit var desksOrganizer: DesksOrganizer @Mock private lateinit var userProfileContexts: UserProfileContexts @Mock private lateinit var desksTransitionsObserver: DesksTransitionObserver + @Mock private lateinit var desktopPipTransitionObserver: DesktopPipTransitionObserver @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var mockDisplayContext: Context @Mock private lateinit var dragToDisplayTransitionHandler: DragToDisplayTransitionHandler @@ -392,6 +393,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken) whenever(userProfileContexts[anyInt()]).thenReturn(context) whenever(userProfileContexts.getOrCreate(anyInt())).thenReturn(context) + whenever(freeformTaskTransitionStarter.startPipTransition(any())).thenReturn(Binder()) controller = createController() controller.setSplitScreenController(splitScreenController) @@ -456,6 +458,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() overviewToDesktopTransitionObserver, desksOrganizer, desksTransitionsObserver, + desktopPipTransitionObserver, userProfileContexts, desktopModeCompatPolicy, dragToDisplayTransitionHandler, @@ -3470,6 +3473,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP) fun onPipTaskMinimize_autoEnterEnabled_startPipTransition() { val task = setUpPipTask(autoEnterEnabled = true) val handler = mock(TransitionHandler::class.java) @@ -3484,6 +3488,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP) fun onPipTaskMinimize_autoEnterDisabled_startMinimizeTransition() { val task = setUpPipTask(autoEnterEnabled = false) whenever( @@ -3503,6 +3508,90 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP, + ) + fun onDesktopTaskEnteredPip_pipIsLastTask_removesWallpaper() { + val task = setUpPipTask(autoEnterEnabled = true) + + controller.onDesktopTaskEnteredPip( + taskId = task.taskId, + deskId = DEFAULT_DISPLAY, + displayId = task.displayId, + taskIsLastVisibleTaskBeforePip = true, + ) + + // Wallpaper is moved to the back + val wct = getLatestTransition() + wct.assertReorder(wallpaperToken, /* toTop= */ false) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun onDesktopTaskEnteredPip_pipIsLastTask_deactivatesDesk() { + val deskId = DEFAULT_DISPLAY + val task = setUpPipTask(autoEnterEnabled = true, deskId = deskId) + val transition = Binder() + whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition) + + controller.onDesktopTaskEnteredPip( + taskId = task.taskId, + deskId = deskId, + displayId = task.displayId, + taskIsLastVisibleTaskBeforePip = true, + ) + + verify(desksOrganizer).deactivateDesk(any(), eq(deskId)) + verify(desksTransitionsObserver) + .addPendingTransition(DeskTransition.DeactivateDesk(transition, deskId)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun onDesktopTaskEnteredPip_pipIsLastTask_launchesHome() { + val task = setUpPipTask(autoEnterEnabled = true) + + controller.onDesktopTaskEnteredPip( + taskId = task.taskId, + deskId = DEFAULT_DISPLAY, + displayId = task.displayId, + taskIsLastVisibleTaskBeforePip = true, + ) + + val wct = getLatestTransition() + wct.assertPendingIntent(launchHomeIntent(DEFAULT_DISPLAY)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun onDesktopTaskEnteredPip_pipIsNotLastTask_doesntExitDesktopMode() { + val task = setUpPipTask(autoEnterEnabled = true) + val deskId = DEFAULT_DISPLAY + setUpFreeformTask(deskId = deskId) // launch another freeform task + val transition = Binder() + whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition) + + controller.onDesktopTaskEnteredPip( + taskId = task.taskId, + deskId = deskId, + displayId = task.displayId, + taskIsLastVisibleTaskBeforePip = false, + ) + + // No transition to exit Desktop mode is started + verifyWCTNotExecuted() + verify(desktopModeEnterExitTransitionListener, never()) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + verify(desksOrganizer, never()).deactivateDesk(any(), eq(deskId)) + verify(desksTransitionsObserver, never()) + .addPendingTransition(DeskTransition.DeactivateDesk(transition, deskId)) + } + + @Test fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() { val task = setUpFreeformTask(active = true) val transition = Binder() @@ -7615,8 +7704,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() return task } - private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo = - setUpFreeformTask().apply { + private fun setUpPipTask( + autoEnterEnabled: Boolean, + displayId: Int = DEFAULT_DISPLAY, + deskId: Int = DEFAULT_DISPLAY, + ): RunningTaskInfo = + setUpFreeformTask(displayId = displayId, deskId = deskId).apply { pictureInPictureParams = PictureInPictureParams.Builder().setAutoEnterEnabled(autoEnterEnabled).build() } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt index a7dc706eb6c9..ec64c2fa2337 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -22,7 +22,6 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.content.ComponentName import android.content.Context import android.content.Intent -import android.os.Binder import android.os.IBinder import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags @@ -30,7 +29,6 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.WindowManager import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_OPEN -import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.IWindowContainerToken @@ -41,7 +39,6 @@ import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER import com.android.modules.utils.testing.ExtendedMockitoRule import com.android.window.flags.Flags -import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP import com.android.wm.shell.MockToken import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.back.BackAnimationController @@ -51,8 +48,6 @@ import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpape import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions -import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP -import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import org.junit.Before @@ -90,6 +85,7 @@ class DesktopTasksTransitionObserverTest { private val userRepositories = mock<DesktopUserRepositories>() private val taskRepository = mock<DesktopRepository>() private val mixedHandler = mock<DesktopMixedTransitionHandler>() + private val pipTransitionObserver = mock<DesktopPipTransitionObserver>() private val backAnimationController = mock<BackAnimationController>() private val desktopWallpaperActivityTokenProvider = mock<DesktopWallpaperActivityTokenProvider>() @@ -114,6 +110,7 @@ class DesktopTasksTransitionObserverTest { transitions, shellTaskOrganizer, mixedHandler, + pipTransitionObserver, backAnimationController, desktopWallpaperActivityTokenProvider, shellInit, @@ -349,56 +346,6 @@ class DesktopTasksTransitionObserverTest { verify(desktopWallpaperActivityTokenProvider).removeToken(wallpaperTask.displayId) } - @Test - @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) - fun pendingPipTransitionAborted_taskRepositoryOnPipAbortedInvoked() { - val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM) - val pipTransition = Binder() - whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true) - - transitionObserver.onTransitionReady( - transition = pipTransition, - info = createOpenChangeTransition(task, type = TRANSIT_PIP), - startTransaction = mock(), - finishTransaction = mock(), - ) - transitionObserver.onTransitionFinished(transition = pipTransition, aborted = true) - - verify(taskRepository).onPipAborted(task.displayId, task.taskId) - } - - @Test - @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) - fun exitPipTransition_taskRepositoryClearTaskInPip() { - val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM) - whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true) - - transitionObserver.onTransitionReady( - transition = mock(), - info = createOpenChangeTransition(task, type = TRANSIT_EXIT_PIP), - startTransaction = mock(), - finishTransaction = mock(), - ) - - verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false) - } - - @Test - @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) - fun removePipTransition_taskRepositoryClearTaskInPip() { - val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM) - whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true) - - transitionObserver.onTransitionReady( - transition = mock(), - info = createOpenChangeTransition(task, type = TRANSIT_REMOVE_PIP), - startTransaction = mock(), - finishTransaction = mock(), - ) - - verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false) - } - private fun createBackNavigationTransition( task: RunningTaskInfo?, type: Int = TRANSIT_TO_BACK, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index e246329446dc..5dff21860ef4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -211,11 +211,19 @@ public class StageCoordinatorTests extends ShellTestCase { when(mSplitLayout.getDividerLeash()).thenReturn(dividerLeash); mRootTask = new TestRunningTaskInfoBuilder().build(); - SurfaceControl rootLeash = new SurfaceControl.Builder().setName("test").build(); + SurfaceControl rootLeash = new SurfaceControl.Builder().setName("splitRoot").build(); mStageCoordinator.onTaskAppeared(mRootTask, rootLeash); mSideStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build(); mMainStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build(); + SurfaceControl mainRootLeash = new SurfaceControl.Builder().setName("mainRoot").build(); + SurfaceControl sideRootLeash = new SurfaceControl.Builder().setName("sideRoot").build(); + mMainStage.mRootLeash = mainRootLeash; + mSideStage.mRootLeash = sideRootLeash; + SurfaceControl mainDimLayer = new SurfaceControl.Builder().setName("mainDim").build(); + SurfaceControl sideDimLayer = new SurfaceControl.Builder().setName("sideDim").build(); + mMainStage.mDimLayer = mainDimLayer; + mSideStage.mDimLayer = sideDimLayer; doReturn(mock(SplitDecorManager.class)).when(mMainStage).getSplitDecorManager(); doReturn(mock(SplitDecorManager.class)).when(mSideStage).getSplitDecorManager(); diff --git a/location/java/android/location/GnssClock.java b/location/java/android/location/GnssClock.java index 62f50b57520c..6930f365adc1 100644 --- a/location/java/android/location/GnssClock.java +++ b/location/java/android/location/GnssClock.java @@ -349,7 +349,7 @@ public final class GnssClock implements Parcelable { * Gets the clock's Drift in nanoseconds per second. * * <p>This value is the instantaneous time-derivative of the value provided by - * {@link #getBiasNanos()}. + * the sum of {@link #getFullBiasNanos()} and {@link #getBiasNanos()}. * * <p>A positive value indicates that the frequency is higher than the nominal (e.g. GPS master * clock) frequency. The error estimate for this reported drift is diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index e39a0aa8717e..48e2f4e15238 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -242,3 +242,13 @@ flag { description: "Fallbacks to the default handling for volume adjustment when media session has fixed volume handling and its app is in the foreground and setting a media controller." bug: "293743975" } + +flag { + name: "fix_output_media_item_list_index_out_of_bounds_exception" + namespace: "media_better_together" + description: "Fixes a bug of causing IndexOutOfBoundsException when building media item list." + bug: "398246089" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/media/java/android/media/quality/Android.bp b/media/java/android/media/quality/Android.bp index 080d5266ccb7..f620144e2880 100644 --- a/media/java/android/media/quality/Android.bp +++ b/media/java/android/media/quality/Android.bp @@ -15,6 +15,30 @@ filegroup { path: "aidl", } +cc_library_headers { + name: "media_quality_headers", + export_include_dirs: ["include"], +} + +cc_library_shared { + name: "libmedia_quality_include", + + export_include_dirs: ["include"], + cflags: [ + "-Wno-unused-variable", + "-Wunused-parameter", + ], + + shared_libs: [ + "libbinder", + "libutils", + ], + + srcs: [ + ":framework-media-quality-sources-aidl", + ], +} + aidl_interface { name: "media_quality_aidl_interface", unstable: true, @@ -24,7 +48,8 @@ aidl_interface { enabled: true, }, cpp: { - enabled: false, + additional_shared_libraries: ["libmedia_quality_include"], + enabled: true, }, ndk: { enabled: false, diff --git a/media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl b/media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl index 2851306f6e4d..d2cf140632ab 100644 --- a/media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl @@ -16,4 +16,4 @@ package android.media.quality; -parcelable ActiveProcessingPicture;
\ No newline at end of file +parcelable ActiveProcessingPicture cpp_header "quality/MediaQualityManager.h";
\ No newline at end of file diff --git a/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl index 174cd461e846..d53860fdf9ad 100644 --- a/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl @@ -16,4 +16,4 @@ package android.media.quality; -parcelable AmbientBacklightEvent; +parcelable AmbientBacklightEvent cpp_header "quality/MediaQualityManager.h"; diff --git a/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl index b95a474fbf90..a935b49b5d23 100644 --- a/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl @@ -16,4 +16,4 @@ package android.media.quality; -parcelable AmbientBacklightMetadata;
\ No newline at end of file +parcelable AmbientBacklightMetadata cpp_header "quality/MediaQualityManager.h";
\ No newline at end of file diff --git a/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl index e2cdd03194cd..051aef80b948 100644 --- a/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl @@ -16,4 +16,4 @@ package android.media.quality; -parcelable AmbientBacklightSettings; +parcelable AmbientBacklightSettings cpp_header "quality/MediaQualityManager.h"; diff --git a/media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl b/media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl index eb2ac97916f3..ea848576e026 100644 --- a/media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl @@ -16,4 +16,4 @@ package android.media.quality; -parcelable ParameterCapability; +parcelable ParameterCapability cpp_header "quality/MediaQualityManager.h"; diff --git a/media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl b/media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl index 41d018b12f33..b0fe3f5538f4 100644 --- a/media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl @@ -16,4 +16,4 @@ package android.media.quality; -parcelable PictureProfile; +parcelable PictureProfile cpp_header "quality/MediaQualityManager.h"; diff --git a/media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl b/media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl index 5d14631dbb73..0582938b6ea7 100644 --- a/media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl @@ -16,4 +16,4 @@ package android.media.quality; -parcelable PictureProfileHandle; +parcelable PictureProfileHandle cpp_header "quality/MediaQualityManager.h"; diff --git a/media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl b/media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl index e79fcaac97be..d93231fbf7e0 100644 --- a/media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl @@ -16,4 +16,4 @@ package android.media.quality; -parcelable SoundProfile; +parcelable SoundProfile cpp_header "quality/MediaQualityManager.h"; diff --git a/media/java/android/media/quality/include/quality/MediaQualityManager.h b/media/java/android/media/quality/include/quality/MediaQualityManager.h new file mode 100644 index 000000000000..8c31667077c3 --- /dev/null +++ b/media/java/android/media/quality/include/quality/MediaQualityManager.h @@ -0,0 +1,127 @@ +#ifndef ANDROID_MEDIA_QUALITY_MANAGER_H +#define ANDROID_MEDIA_QUALITY_MANAGER_H + + +namespace android { +namespace media { +namespace quality { + +// TODO: implement writeToParcel and readFromParcel + +class PictureProfileHandle : public Parcelable { + public: + PictureProfileHandle() {} + status_t writeToParcel(android::Parcel*) const override { + return 0; + } + status_t readFromParcel(const android::Parcel*) override { + return 0; + } + std::string toString() const { + return ""; + } +}; + +class SoundProfile : public Parcelable { + public: + SoundProfile() {} + status_t writeToParcel(android::Parcel*) const override { + return 0; + } + status_t readFromParcel(const android::Parcel*) override { + return 0; + } + std::string toString() const { + return ""; + } +}; + +class PictureProfile : public Parcelable { + public: + PictureProfile() {} + status_t writeToParcel(android::Parcel*) const override { + return 0; + } + status_t readFromParcel(const android::Parcel*) override { + return 0; + } + std::string toString() const { + return ""; + } +}; + +class ActiveProcessingPicture : public Parcelable { + public: + ActiveProcessingPicture() {} + status_t writeToParcel(android::Parcel*) const override { + return 0; + } + status_t readFromParcel(const android::Parcel*) override { + return 0; + } + std::string toString() const { + return ""; + } +}; + +class AmbientBacklightEvent : public Parcelable { + public: + AmbientBacklightEvent() {} + status_t writeToParcel(android::Parcel*) const override { + return 0; + } + status_t readFromParcel(const android::Parcel*) override { + return 0; + } + std::string toString() const { + return ""; + } +}; + +class AmbientBacklightMetadata : public Parcelable { + public: + AmbientBacklightMetadata() {} + status_t writeToParcel(android::Parcel*) const override { + return 0; + } + status_t readFromParcel(const android::Parcel*) override { + return 0; + } + std::string toString() const { + return ""; + } +}; + +class AmbientBacklightSettings : public Parcelable { + public: + AmbientBacklightSettings() {} + status_t writeToParcel(android::Parcel*) const override { + return 0; + } + status_t readFromParcel(const android::Parcel*) override { + return 0; + } + std::string toString() const { + return ""; + } +}; + +class ParameterCapability : public Parcelable { + public: + ParameterCapability() {} + status_t writeToParcel(android::Parcel*) const override { + return 0; + } + status_t readFromParcel(const android::Parcel*) override { + return 0; + } + std::string toString() const { + return ""; + } +}; + +} // namespace quality +} // namespace media +} // namespace android + +#endif diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt index 8ad96a5bcb37..62b134279267 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt @@ -77,6 +77,16 @@ internal constructor( list.apply { add(toIndex, removeAt(fromIndex)) } } + /** Swap the two items in the list with the given indices. */ + fun swapItems(index1: Int, index2: Int) { + list.apply { + val item1 = get(index1) + val item2 = get(index2) + set(index2, item1) + set(index1, item2) + } + } + /** Remove widget from the list and the database. */ fun onRemove(indexToRemove: Int) { if (list[indexToRemove].isWidgetContent()) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt index 0aef7f2c7063..dda388aeeac6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt @@ -18,8 +18,10 @@ package com.android.systemui.communal.ui.compose import android.content.ClipDescription import android.view.DragEvent +import androidx.compose.animation.core.tween import androidx.compose.foundation.draganddrop.dragAndDropTarget import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.runtime.Composable @@ -37,6 +39,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import com.android.systemui.Flags.communalWidgetResizing +import com.android.systemui.Flags.glanceableHubV2 import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset import com.android.systemui.communal.util.WidgetPickerIntentUtils @@ -51,13 +54,14 @@ import kotlinx.coroutines.launch * @see dragAndDropTarget */ @Composable -internal fun rememberDragAndDropTargetState( +fun rememberDragAndDropTargetState( gridState: LazyGridState, contentOffset: Offset, contentListState: ContentListState, ): DragAndDropTargetState { val scope = rememberCoroutineScope() val autoScrollThreshold = with(LocalDensity.current) { 60.dp.toPx() } + val state = remember(gridState, contentOffset, contentListState, autoScrollThreshold, scope) { DragAndDropTargetState( @@ -68,11 +72,9 @@ internal fun rememberDragAndDropTargetState( scope = scope, ) } - LaunchedEffect(state) { - for (diff in state.scrollChannel) { - gridState.scrollBy(diff) - } - } + + LaunchedEffect(state) { state.processScrollRequests(scope) } + return state } @@ -83,7 +85,7 @@ internal fun rememberDragAndDropTargetState( * @see DragEvent */ @Composable -internal fun Modifier.dragAndDropTarget(dragDropTargetState: DragAndDropTargetState): Modifier { +fun Modifier.dragAndDropTarget(dragDropTargetState: DragAndDropTargetState): Modifier { val state by rememberUpdatedState(dragDropTargetState) return this then @@ -132,13 +134,79 @@ internal fun Modifier.dragAndDropTarget(dragDropTargetState: DragAndDropTargetSt * other activities. [GridDragDropState] on the other hand, handles dragging of existing items in * the communal hub grid. */ -internal class DragAndDropTargetState( +class DragAndDropTargetState( + state: LazyGridState, + contentOffset: Offset, + contentListState: ContentListState, + autoScrollThreshold: Float, + scope: CoroutineScope, +) { + private val dragDropState: DragAndDropTargetStateInternal = + if (glanceableHubV2()) { + DragAndDropTargetStateV2( + state = state, + contentListState = contentListState, + scope = scope, + autoScrollThreshold = autoScrollThreshold, + contentOffset = contentOffset, + ) + } else { + DragAndDropTargetStateV1( + state = state, + contentListState = contentListState, + scope = scope, + autoScrollThreshold = autoScrollThreshold, + contentOffset = contentOffset, + ) + } + + fun onStarted() = dragDropState.onStarted() + + fun onMoved(event: DragAndDropEvent) = dragDropState.onMoved(event) + + fun onDrop(event: DragAndDropEvent) = dragDropState.onDrop(event) + + fun onEnded() = dragDropState.onEnded() + + fun onExited() = dragDropState.onExited() + + suspend fun processScrollRequests(coroutineScope: CoroutineScope) = + dragDropState.processScrollRequests(coroutineScope) +} + +/** + * A private interface defining the API for handling drag-and-drop operations. There will be two + * implementations of this interface: V1 for devices that do not have the glanceable_hub_v2 flag + * enabled, and V2 for devices that do have that flag enabled. + * + * TODO(b/400789179): Remove this interface and the V1 implementation once glanceable_hub_v2 has + * shipped. + */ +private interface DragAndDropTargetStateInternal { + fun onStarted() = Unit + + fun onMoved(event: DragAndDropEvent) = Unit + + fun onDrop(event: DragAndDropEvent): Boolean = false + + fun onEnded() = Unit + + fun onExited() = Unit + + suspend fun processScrollRequests(coroutineScope: CoroutineScope) = Unit +} + +/** + * The V1 implementation of DragAndDropTargetStateInternal to be used when the glanceable_hub_v2 + * flag is disabled. + */ +private class DragAndDropTargetStateV1( private val state: LazyGridState, private val contentOffset: Offset, private val contentListState: ContentListState, private val autoScrollThreshold: Float, private val scope: CoroutineScope, -) { +) : DragAndDropTargetStateInternal { /** * The placeholder item that is treated as if it is being dragged across the grid. It is added * to grid once drag and drop event is started and removed when event ends. @@ -147,15 +215,21 @@ internal class DragAndDropTargetState( private var placeHolderIndex: Int? = null private var previousTargetItemKey: Any? = null - internal val scrollChannel = Channel<Float>() + private val scrollChannel = Channel<Float>() - fun onStarted() { + override suspend fun processScrollRequests(coroutineScope: CoroutineScope) { + for (diff in scrollChannel) { + state.scrollBy(diff) + } + } + + override fun onStarted() { // assume item will be added to the end. contentListState.list.add(placeHolder) placeHolderIndex = contentListState.list.size - 1 } - fun onMoved(event: DragAndDropEvent) { + override fun onMoved(event: DragAndDropEvent) { val dragOffset = event.toOffset() val targetItem = @@ -201,7 +275,7 @@ internal class DragAndDropTargetState( } } - fun onDrop(event: DragAndDropEvent): Boolean { + override fun onDrop(event: DragAndDropEvent): Boolean { return placeHolderIndex?.let { dropIndex -> val widgetExtra = event.maybeWidgetExtra() ?: return false val (componentName, user) = widgetExtra @@ -219,13 +293,13 @@ internal class DragAndDropTargetState( } ?: false } - fun onEnded() { + override fun onEnded() { placeHolderIndex = null previousTargetItemKey = null contentListState.list.remove(placeHolder) } - fun onExited() { + override fun onExited() { onEnded() } @@ -257,16 +331,186 @@ internal class DragAndDropTargetState( contentListState.onMove(currentIndex, index) } } +} +/** + * The V2 implementation of DragAndDropTargetStateInternal to be used when the glanceable_hub_v2 + * flag is enabled. + */ +private class DragAndDropTargetStateV2( + private val state: LazyGridState, + private val contentOffset: Offset, + private val contentListState: ContentListState, + private val autoScrollThreshold: Float, + private val scope: CoroutineScope, +) : DragAndDropTargetStateInternal { /** - * Parses and returns the intent extra associated with the widget that is dropped into the grid. - * - * Returns null if the drop event didn't include intent information. + * The placeholder item that is treated as if it is being dragged across the grid. It is added + * to grid once drag and drop event is started and removed when event ends. */ - private fun DragAndDropEvent.maybeWidgetExtra(): WidgetPickerIntentUtils.WidgetExtra? { - val clipData = this.toAndroidDragEvent().clipData.takeIf { it.itemCount != 0 } - return clipData?.getItemAt(0)?.intent?.let { intent -> getWidgetExtraFromIntent(intent) } + private var placeHolder = CommunalContentModel.WidgetPlaceholder() + private var placeHolderIndex: Int? = null + private var previousTargetItemKey: Any? = null + private var dragOffset = Offset.Zero + private var columnWidth = 0 + + private val scrollChannel = Channel<Float>() + + override suspend fun processScrollRequests(coroutineScope: CoroutineScope) { + while (true) { + val amount = scrollChannel.receive() + + if (state.isScrollInProgress) { + // Ignore overscrolling if a scroll is already in progress (but we still want to + // consume the scroll event so that we don't end up processing a bunch of old + // events after scrolling has finished). + continue + } + + // Perform the rest of the drag operation after scrolling has finished (or immediately + // if there will be no scrolling). + if (amount != 0f) { + scope.launch { + state.animateScrollBy(amount, tween(delayMillis = 250, durationMillis = 1000)) + performDragAction() + } + } else { + performDragAction() + } + } + } + + override fun onStarted() { + // assume item will be added to the end. + contentListState.list.add(placeHolder) + placeHolderIndex = contentListState.list.size - 1 + + // Use the width of the first item as the column width. + columnWidth = + state.layoutInfo.visibleItemsInfo.first().size.width + + state.layoutInfo.beforeContentPadding + + state.layoutInfo.afterContentPadding } - private fun DragAndDropEvent.toOffset() = this.toAndroidDragEvent().run { Offset(x, y) } + override fun onMoved(event: DragAndDropEvent) { + dragOffset = event.toOffset() + scrollChannel.trySend(computeAutoscroll(dragOffset)) + } + + override fun onDrop(event: DragAndDropEvent): Boolean { + return placeHolderIndex?.let { dropIndex -> + val widgetExtra = event.maybeWidgetExtra() ?: return false + val (componentName, user) = widgetExtra + if (componentName != null && user != null) { + // Placeholder isn't removed yet to allow the setting the right rank for items + // before adding in the new item. + contentListState.onSaveList( + newItemComponentName = componentName, + newItemUser = user, + newItemIndex = dropIndex, + ) + return@let true + } + return false + } ?: false + } + + override fun onEnded() { + placeHolderIndex = null + previousTargetItemKey = null + contentListState.list.remove(placeHolder) + } + + override fun onExited() { + onEnded() + } + + private fun performDragAction() { + val targetItem = + state.layoutInfo.visibleItemsInfo + .asSequence() + .filter { item -> contentListState.isItemEditable(item.index) } + .firstItemAtOffset(dragOffset - contentOffset) + + if ( + targetItem != null && + (!communalWidgetResizing() || targetItem.key != previousTargetItemKey) + ) { + if (communalWidgetResizing()) { + // Keep track of the previous target item, to avoid rapidly oscillating between + // items if the target item doesn't visually move as a result of the index change. + // In this case, even after the index changes, we'd still be colliding with the + // element, so it would be selected as the target item the next time this function + // runs again, which would trigger us to revert the index change we recently made. + previousTargetItemKey = targetItem.key + } + + val scrollToIndex = + if (targetItem.index == state.firstVisibleItemIndex) { + placeHolderIndex + } else if (placeHolderIndex == state.firstVisibleItemIndex) { + targetItem.index + } else { + null + } + + if (scrollToIndex != null) { + scope.launch { + state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset) + movePlaceholderTo(targetItem.index) + } + } else { + movePlaceholderTo(targetItem.index) + } + + placeHolderIndex = targetItem.index + } else if (targetItem == null) { + previousTargetItemKey = null + } + } + + private fun computeAutoscroll(dragOffset: Offset): Float { + val orientation = state.layoutInfo.orientation + val distanceFromStart = + if (orientation == Orientation.Horizontal) { + dragOffset.x + } else { + dragOffset.y + } + val distanceFromEnd = + if (orientation == Orientation.Horizontal) { + state.layoutInfo.viewportEndOffset - dragOffset.x + } else { + state.layoutInfo.viewportEndOffset - dragOffset.y + } + + return when { + distanceFromEnd < autoScrollThreshold -> { + (columnWidth - state.layoutInfo.beforeContentPadding).toFloat() + } + distanceFromStart < autoScrollThreshold -> { + -(columnWidth - state.layoutInfo.afterContentPadding).toFloat() + } + else -> 0f + } + } + + private fun movePlaceholderTo(index: Int) { + val currentIndex = contentListState.list.indexOf(placeHolder) + if (currentIndex != index) { + contentListState.swapItems(currentIndex, index) + } + } } + +/** + * Parses and returns the intent extra associated with the widget that is dropped into the grid. + * + * Returns null if the drop event didn't include intent information. + */ +private fun DragAndDropEvent.maybeWidgetExtra(): WidgetPickerIntentUtils.WidgetExtra? { + val clipData = this.toAndroidDragEvent().clipData.takeIf { it.itemCount != 0 } + return clipData?.getItemAt(0)?.intent?.let { intent -> getWidgetExtraFromIntent(intent) } +} + +private fun DragAndDropEvent.toOffset() = this.toAndroidDragEvent().run { Offset(x, y) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt index c972d3e3cf15..2a5addeb4951 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt @@ -19,7 +19,10 @@ package com.android.systemui.communal.ui.compose import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.Box @@ -37,13 +40,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round import androidx.compose.ui.unit.toOffset import androidx.compose.ui.unit.toSize import com.android.systemui.Flags.communalWidgetResizing +import com.android.systemui.Flags.glanceableHubV2 import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset @@ -62,22 +68,22 @@ fun rememberGridDragDropState( contentListState: ContentListState, updateDragPositionForRemove: (boundingBox: IntRect) -> Boolean, ): GridDragDropState { - val scope = rememberCoroutineScope() + val coroutineScope = rememberCoroutineScope() + val autoScrollThreshold = with(LocalDensity.current) { 60.dp.toPx() } + val state = remember(gridState, contentListState, updateDragPositionForRemove) { GridDragDropState( - state = gridState, + gridState = gridState, contentListState = contentListState, - scope = scope, + coroutineScope = coroutineScope, + autoScrollThreshold = autoScrollThreshold, updateDragPositionForRemove = updateDragPositionForRemove, ) } - LaunchedEffect(state) { - while (true) { - val diff = state.scrollChannel.receive() - gridState.scrollBy(diff) - } - } + + LaunchedEffect(state) { state.processScrollRequests(coroutineScope) } + return state } @@ -89,36 +95,86 @@ fun rememberGridDragDropState( * to remove the dragged item if condition met and call [ContentListState.onSaveList] to persist any * change in ordering. */ -class GridDragDropState -internal constructor( - private val state: LazyGridState, - private val contentListState: ContentListState, - private val scope: CoroutineScope, +class GridDragDropState( + val gridState: LazyGridState, + contentListState: ContentListState, + coroutineScope: CoroutineScope, + autoScrollThreshold: Float, private val updateDragPositionForRemove: (draggingBoundingBox: IntRect) -> Boolean, ) { - var draggingItemKey by mutableStateOf<String?>(null) - private set + private val dragDropState: GridDragDropStateInternal = + if (glanceableHubV2()) { + GridDragDropStateV2( + gridState = gridState, + contentListState = contentListState, + scope = coroutineScope, + autoScrollThreshold = autoScrollThreshold, + updateDragPositionForRemove = updateDragPositionForRemove, + ) + } else { + GridDragDropStateV1( + gridState = gridState, + contentListState = contentListState, + scope = coroutineScope, + updateDragPositionForRemove = updateDragPositionForRemove, + ) + } - var isDraggingToRemove by mutableStateOf(false) - private set + val draggingItemKey: String? + get() = dragDropState.draggingItemKey - internal val scrollChannel = Channel<Float>() + val isDraggingToRemove: Boolean + get() = dragDropState.isDraggingToRemove - private var draggingItemDraggedDelta by mutableStateOf(Offset.Zero) - private var draggingItemInitialOffset by mutableStateOf(Offset.Zero) + val draggingItemOffset: Offset + get() = dragDropState.draggingItemOffset - private val spacer = CommunalContentModel.Spacer(CommunalContentSize.Responsive(1)) - private var spacerIndex: Int? = null + /** + * Called when dragging is initiated. + * + * @return {@code True} if dragging a grid item, {@code False} otherwise. + */ + fun onDragStart( + offset: Offset, + screenWidth: Int, + layoutDirection: LayoutDirection, + contentOffset: Offset, + ): Boolean = dragDropState.onDragStart(offset, screenWidth, layoutDirection, contentOffset) - private var previousTargetItemKey: Any? = null + fun onDragInterrupted() = dragDropState.onDragInterrupted() + + fun onDrag(offset: Offset, layoutDirection: LayoutDirection) = + dragDropState.onDrag(offset, layoutDirection) + + suspend fun processScrollRequests(coroutineScope: CoroutineScope) = + dragDropState.processScrollRequests(coroutineScope) +} + +/** + * A private base class defining the API for handling drag-and-drop operations. There will be two + * implementations of this class: V1 for devices that do not have the glanceable_hub_v2 flag + * enabled, and V2 for devices that do have that flag enabled. + * + * TODO(b/400789179): Remove this class and the V1 implementation once glanceable_hub_v2 has + * shipped. + */ +private open class GridDragDropStateInternal(protected val state: LazyGridState) { + var draggingItemKey by mutableStateOf<String?>(null) + protected set - internal val draggingItemOffset: Offset + var isDraggingToRemove by mutableStateOf(false) + protected set + + var draggingItemDraggedDelta by mutableStateOf(Offset.Zero) + var draggingItemInitialOffset by mutableStateOf(Offset.Zero) + + val draggingItemOffset: Offset get() = draggingItemLayoutInfo?.let { item -> draggingItemInitialOffset + draggingItemDraggedDelta - item.offset.toOffset() } ?: Offset.Zero - private val draggingItemLayoutInfo: LazyGridItemInfo? + val draggingItemLayoutInfo: LazyGridItemInfo? get() = state.layoutInfo.visibleItemsInfo.firstOrNull { it.key == draggingItemKey } /** @@ -126,7 +182,45 @@ internal constructor( * * @return {@code True} if dragging a grid item, {@code False} otherwise. */ - internal fun onDragStart( + open fun onDragStart( + offset: Offset, + screenWidth: Int, + layoutDirection: LayoutDirection, + contentOffset: Offset, + ): Boolean = false + + open fun onDragInterrupted() = Unit + + open fun onDrag(offset: Offset, layoutDirection: LayoutDirection) = Unit + + open suspend fun processScrollRequests(coroutineScope: CoroutineScope) = Unit +} + +/** + * The V1 implementation of GridDragDropStateInternal to be used when the glanceable_hub_v2 flag is + * disabled. + */ +private class GridDragDropStateV1( + val gridState: LazyGridState, + private val contentListState: ContentListState, + private val scope: CoroutineScope, + private val updateDragPositionForRemove: (draggingBoundingBox: IntRect) -> Boolean, +) : GridDragDropStateInternal(gridState) { + private val scrollChannel = Channel<Float>() + + private val spacer = CommunalContentModel.Spacer(CommunalContentSize.Responsive(1)) + private var spacerIndex: Int? = null + + private var previousTargetItemKey: Any? = null + + override suspend fun processScrollRequests(coroutineScope: CoroutineScope) { + while (true) { + val diff = scrollChannel.receive() + state.scrollBy(diff) + } + } + + override fun onDragStart( offset: Offset, screenWidth: Int, layoutDirection: LayoutDirection, @@ -162,7 +256,7 @@ internal constructor( return false } - internal fun onDragInterrupted() { + override fun onDragInterrupted() { draggingItemKey?.let { if (isDraggingToRemove) { contentListState.onRemove( @@ -185,7 +279,7 @@ internal constructor( } } - internal fun onDrag(offset: Offset, layoutDirection: LayoutDirection) { + override fun onDrag(offset: Offset, layoutDirection: LayoutDirection) { // Adjust offset to match the layout direction draggingItemDraggedDelta += Offset(offset.x.directional(LayoutDirection.Ltr, layoutDirection), offset.y) @@ -282,6 +376,249 @@ internal constructor( } } +/** + * The V2 implementation of GridDragDropStateInternal to be used when the glanceable_hub_v2 flag is + * enabled. + */ +private class GridDragDropStateV2( + val gridState: LazyGridState, + private val contentListState: ContentListState, + private val scope: CoroutineScope, + private val autoScrollThreshold: Float, + private val updateDragPositionForRemove: (draggingBoundingBox: IntRect) -> Boolean, +) : GridDragDropStateInternal(gridState) { + + private val scrollChannel = Channel<Float>(Channel.UNLIMITED) + + // Used to keep track of the dragging item during scrolling (because it might be off screen + // and no longer in the list of visible items). + private var draggingItemWhileScrolling: LazyGridItemInfo? by mutableStateOf(null) + + private val spacer = CommunalContentModel.Spacer(CommunalContentSize.Responsive(1)) + private var spacerIndex: Int? = null + + private var previousTargetItemKey: Any? = null + + // Basically, the location of the user's finger on the screen. + private var currentDragPositionOnScreen by mutableStateOf(Offset.Zero) + // The offset of the grid from the top of the screen. + private var contentOffset = Offset.Zero + + // The width of one column in the grid (needed in order to auto-scroll one column at a time). + private var columnWidth = 0 + + override suspend fun processScrollRequests(coroutineScope: CoroutineScope) { + while (true) { + val amount = scrollChannel.receive() + + if (state.isScrollInProgress) { + // Ignore overscrolling if a scroll is already in progress (but we still want to + // consume the scroll event so that we don't end up processing a bunch of old + // events after scrolling has finished). + continue + } + + // We perform the rest of the drag action after scrolling has finished (or immediately + // if there will be no scrolling). + if (amount != 0f) { + coroutineScope.launch { + state.animateScrollBy(amount, tween(delayMillis = 250, durationMillis = 1000)) + performDragAction() + } + } else { + performDragAction() + } + } + } + + override fun onDragStart( + offset: Offset, + screenWidth: Int, + layoutDirection: LayoutDirection, + contentOffset: Offset, + ): Boolean { + val normalizedOffset = + Offset( + if (layoutDirection == LayoutDirection.Ltr) offset.x else screenWidth - offset.x, + offset.y, + ) + + currentDragPositionOnScreen = normalizedOffset + this.contentOffset = contentOffset + + state.layoutInfo.visibleItemsInfo + .filter { item -> contentListState.isItemEditable(item.index) } + // grid item offset is based off grid content container so we need to deduct + // before content padding from the initial pointer position + .firstItemAtOffset(normalizedOffset - contentOffset) + ?.apply { + draggingItemKey = key as String + draggingItemWhileScrolling = this + draggingItemInitialOffset = this.offset.toOffset() + columnWidth = + this.size.width + + state.layoutInfo.beforeContentPadding + + state.layoutInfo.afterContentPadding + // Add a spacer after the last widget if it is larger than the dragging widget. + // This allows overscrolling, enabling the dragging widget to be placed beyond it. + val lastWidget = contentListState.list.lastOrNull { it.isWidgetContent() } + if ( + lastWidget != null && + draggingItemLayoutInfo != null && + lastWidget.size.span > draggingItemLayoutInfo!!.span + ) { + contentListState.list.add(spacer) + spacerIndex = contentListState.list.size - 1 + } + return true + } + + return false + } + + override fun onDragInterrupted() { + draggingItemKey?.let { + if (isDraggingToRemove) { + contentListState.onRemove( + contentListState.list.indexOfFirst { it.key == draggingItemKey } + ) + isDraggingToRemove = false + updateDragPositionForRemove(IntRect.Zero) + } + // persist list editing changes on dragging ends + contentListState.onSaveList() + draggingItemKey = null + } + previousTargetItemKey = null + draggingItemDraggedDelta = Offset.Zero + draggingItemInitialOffset = Offset.Zero + currentDragPositionOnScreen = Offset.Zero + draggingItemWhileScrolling = null + // Remove spacer, if any, when a drag gesture finishes. + spacerIndex?.let { + contentListState.list.removeAt(it) + spacerIndex = null + } + } + + override fun onDrag(offset: Offset, layoutDirection: LayoutDirection) { + // Adjust offset to match the layout direction + val delta = Offset(offset.x.directional(LayoutDirection.Ltr, layoutDirection), offset.y) + draggingItemDraggedDelta += delta + currentDragPositionOnScreen += delta + + scrollChannel.trySend(computeAutoscroll(currentDragPositionOnScreen)) + } + + fun performDragAction() { + val draggingItem = draggingItemLayoutInfo ?: draggingItemWhileScrolling + if (draggingItem == null) { + return + } + + val draggingBoundingBox = + IntRect(draggingItem.offset + draggingItemOffset.round(), draggingItem.size) + val curDragPositionInGrid = (currentDragPositionOnScreen - contentOffset) + + val targetItem = + if (communalWidgetResizing()) { + val lastVisibleItemIndex = state.layoutInfo.visibleItemsInfo.last().index + state.layoutInfo.visibleItemsInfo.findLast( + fun(item): Boolean { + val itemBoundingBox = IntRect(item.offset, item.size) + return draggingItemKey != item.key && + contentListState.isItemEditable(item.index) && + itemBoundingBox.contains(curDragPositionInGrid.round()) && + // If we swap with the last visible item, and that item doesn't fit + // in the gap created by moving the current item, then the current item + // will get placed after the last visible item. In this case, it gets + // placed outside of the viewport. We avoid this here, so the user + // has to scroll first before the swap can happen. + (item.index != lastVisibleItemIndex || item.span <= draggingItem.span) + } + ) + } else { + state.layoutInfo.visibleItemsInfo + .asSequence() + .filter { item -> contentListState.isItemEditable(item.index) } + .filter { item -> draggingItem.index != item.index } + .firstItemAtOffset(curDragPositionInGrid) + } + + if ( + targetItem != null && + (!communalWidgetResizing() || targetItem.key != previousTargetItemKey) + ) { + val scrollToIndex = + if (targetItem.index == state.firstVisibleItemIndex) { + draggingItem.index + } else if (draggingItem.index == state.firstVisibleItemIndex) { + targetItem.index + } else { + null + } + if (communalWidgetResizing()) { + // Keep track of the previous target item, to avoid rapidly oscillating between + // items if the target item doesn't visually move as a result of the index change. + // In this case, even after the index changes, we'd still be colliding with the + // element, so it would be selected as the target item the next time this function + // runs again, which would trigger us to revert the index change we recently made. + previousTargetItemKey = targetItem.key + } + if (scrollToIndex != null) { + scope.launch { + // this is needed to neutralize automatic keeping the first item first. + state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset) + contentListState.swapItems(draggingItem.index, targetItem.index) + } + } else { + contentListState.swapItems(draggingItem.index, targetItem.index) + } + draggingItemWhileScrolling = targetItem + isDraggingToRemove = false + } else if (targetItem == null) { + isDraggingToRemove = checkForRemove(draggingBoundingBox) + previousTargetItemKey = null + } + } + + /** Calculate the amount dragged out of bound on both sides. Returns 0f if not overscrolled. */ + private fun computeAutoscroll(dragOffset: Offset): Float { + val orientation = state.layoutInfo.orientation + val distanceFromStart = + if (orientation == Orientation.Horizontal) { + dragOffset.x + } else { + dragOffset.y + } + val distanceFromEnd = + if (orientation == Orientation.Horizontal) { + state.layoutInfo.viewportEndOffset - dragOffset.x + } else { + state.layoutInfo.viewportEndOffset - dragOffset.y + } + + return when { + distanceFromEnd < autoScrollThreshold -> { + (columnWidth - state.layoutInfo.beforeContentPadding).toFloat() + } + distanceFromStart < autoScrollThreshold -> { + -(columnWidth - state.layoutInfo.afterContentPadding).toFloat() + } + else -> 0f + } + } + + /** Calls the callback with the updated drag position and returns whether to remove the item. */ + private fun checkForRemove(draggingItemBoundingBox: IntRect): Boolean { + return if (draggingItemDraggedDelta.y < 0) { + updateDragPositionForRemove(draggingItemBoundingBox) + } else { + false + } + } +} + fun Modifier.dragContainer( dragDropState: GridDragDropState, layoutDirection: LayoutDirection, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt index 046d92d58978..2ab36501d87d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt @@ -443,4 +443,24 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Communal) assertThat(transitionRepository).noTransitionsStarted() } + + @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun testDoNotTransitionToGlanceableHub_onWakeUpFromAodDueToMotion() = + kosmos.runTest { + setCommunalV2Available(true) + + val currentScene by collectLastValue(communalSceneInteractor.currentScene) + fakeCommunalSceneRepository.changeScene(CommunalScenes.Blank) + + // Communal is not showing + Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Blank) + + powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LIFT) + testScope.advanceTimeBy(100) // account for debouncing + + Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Blank) + assertThat(transitionRepository) + .startedTransition(from = KeyguardState.AOD, to = KeyguardState.LOCKSCREEN) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt index 096c3dafd01c..c3d18a3d893c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt @@ -20,7 +20,6 @@ import android.os.PowerManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization -import android.provider.Settings import android.service.dream.dreamManager import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState @@ -30,8 +29,6 @@ import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR import com.android.systemui.Flags.FLAG_SCENE_CONTAINER import com.android.systemui.Flags.glanceableHubV2 import com.android.systemui.SysuiTestCase -import com.android.systemui.common.data.repository.batteryRepository -import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository import com.android.systemui.communal.data.repository.communalSceneRepository import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository @@ -61,8 +58,6 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos -import com.android.systemui.user.data.repository.fakeUserRepository -import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth import junit.framework.Assert.assertEquals import kotlinx.coroutines.flow.flowOf @@ -171,15 +166,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT fun testTransitionToLockscreen_onWake_canDream_ktfRefactor() = kosmos.runTest { setCommunalAvailable(true) - if (glanceableHubV2()) { - val user = fakeUserRepository.asMainUser() - fakeSettings.putIntForUser( - Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, - 1, - user.id, - ) - batteryRepository.fake.setDevicePluggedIn(true) - } else { + if (!glanceableHubV2()) { whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) } @@ -226,15 +213,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT fun testTransitionToGlanceableHub_onWakeup_ifAvailable() = kosmos.runTest { setCommunalAvailable(true) - if (glanceableHubV2()) { - val user = fakeUserRepository.asMainUser() - fakeSettings.putIntForUser( - Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, - 1, - user.id, - ) - batteryRepository.fake.setDevicePluggedIn(true) - } else { + if (!glanceableHubV2()) { whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) } @@ -250,6 +229,25 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT } @Test + @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR, FLAG_SCENE_CONTAINER) + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun testTransitionToLockscreen_onWakeupFromLift() = + kosmos.runTest { + setCommunalAvailable(true) + if (!glanceableHubV2()) { + whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + } + + // Device turns on. + powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LIFT) + testScope.advanceTimeBy(51L) + + // We transition to the lockscreen instead of the hub. + assertThat(transitionRepository) + .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.LOCKSCREEN) + } + + @Test @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testTransitionToOccluded_onWakeup_whenOccludingActivityOnTop() = kosmos.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesDndTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesDndTileTest.kt new file mode 100644 index 000000000000..1adba6fcd45d --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesDndTileTest.kt @@ -0,0 +1,211 @@ +/* + * 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.qs.tiles + +import android.app.Flags +import android.os.Handler +import android.platform.test.annotations.EnableFlags +import android.service.quicksettings.Tile +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.logging.MetricsLogger +import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.systemui.SysuiTestCase +import com.android.systemui.classifier.FalsingManagerFake +import com.android.systemui.kosmos.mainCoroutineContext +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.qs.QSTile.BooleanState +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.qs.QSHost +import com.android.systemui.qs.QsEventLogger +import com.android.systemui.qs.logging.QSLogger +import com.android.systemui.qs.shared.QSSettingsPackageRepository +import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler +import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesDndTileDataInteractor +import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesDndTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel +import com.android.systemui.qs.tiles.impl.modes.ui.ModesDndTileMapper +import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider +import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder +import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig +import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.data.repository.zenModeRepository +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor +import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate +import com.android.systemui.statusbar.policy.ui.dialog.modesDialogEventLogger +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any +import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.SecureSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.whenever + +@EnableFlags(Flags.FLAG_MODES_UI) +@SmallTest +@RunWith(AndroidJUnit4::class) +@RunWithLooper(setAsMainLooper = true) +class ModesDndTileTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val testDispatcher = kosmos.testDispatcher + + @Mock private lateinit var qsHost: QSHost + + @Mock private lateinit var metricsLogger: MetricsLogger + + @Mock private lateinit var statusBarStateController: StatusBarStateController + + @Mock private lateinit var activityStarter: ActivityStarter + + @Mock private lateinit var qsLogger: QSLogger + + @Mock private lateinit var uiEventLogger: QsEventLogger + + @Mock private lateinit var qsTileConfigProvider: QSTileConfigProvider + + @Mock private lateinit var dialogDelegate: ModesDialogDelegate + + @Mock private lateinit var settingsPackageRepository: QSSettingsPackageRepository + + private val inputHandler = FakeQSTileIntentUserInputHandler() + private val zenModeRepository = kosmos.zenModeRepository + private val tileDataInteractor = + ModesDndTileDataInteractor(context, kosmos.zenModeInteractor, testDispatcher) + private val mapper = ModesDndTileMapper(context.resources, context.theme) + + private lateinit var userActionInteractor: ModesDndTileUserActionInteractor + private lateinit var secureSettings: SecureSettings + private lateinit var testableLooper: TestableLooper + private lateinit var underTest: ModesDndTile + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testableLooper = TestableLooper.get(this) + secureSettings = FakeSettings() + + // Allow the tile to load resources + whenever(qsHost.context).thenReturn(context) + whenever(qsHost.userContext).thenReturn(context) + + whenever(qsTileConfigProvider.getConfig(any())) + .thenReturn( + QSTileConfigTestBuilder.build { + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.qs_dnd_icon_off, + labelRes = R.string.quick_settings_dnd_label, + ) + } + ) + + userActionInteractor = + ModesDndTileUserActionInteractor( + kosmos.mainCoroutineContext, + inputHandler, + dialogDelegate, + kosmos.zenModeInteractor, + kosmos.modesDialogEventLogger, + settingsPackageRepository, + ) + + underTest = + ModesDndTile( + qsHost, + uiEventLogger, + testableLooper.looper, + Handler(testableLooper.looper), + FalsingManagerFake(), + metricsLogger, + statusBarStateController, + activityStarter, + qsLogger, + qsTileConfigProvider, + tileDataInteractor, + mapper, + userActionInteractor, + ) + + underTest.initialize() + underTest.setListening(Object(), true) + + testableLooper.processAllMessages() + } + + @After + fun tearDown() { + underTest.destroy() + testableLooper.processAllMessages() + } + + @Test + fun stateUpdatesOnChange() = + testScope.runTest { + assertThat(underTest.state.state).isEqualTo(Tile.STATE_INACTIVE) + + zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND) + runCurrent() + testableLooper.processAllMessages() + + assertThat(underTest.state.state).isEqualTo(Tile.STATE_ACTIVE) + } + + @Test + fun handleUpdateState_withModel_updatesState() = + testScope.runTest { + val tileState = + BooleanState().apply { + state = Tile.STATE_INACTIVE + secondaryLabel = "Old secondary label" + } + val model = ModesDndTileModel(isActivated = true) + + underTest.handleUpdateState(tileState, model) + + assertThat(tileState.state).isEqualTo(Tile.STATE_ACTIVE) + assertThat(tileState.secondaryLabel).isEqualTo("On") + } + + @Test + fun handleUpdateState_withNull_updatesState() = + testScope.runTest { + val tileState = + BooleanState().apply { + state = Tile.STATE_INACTIVE + secondaryLabel = "Old secondary label" + } + zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND) + runCurrent() + + underTest.handleUpdateState(tileState, null) + + assertThat(tileState.state).isEqualTo(Tile.STATE_ACTIVE) + assertThat(tileState.secondaryLabel).isEqualTo("On") + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileDataInteractorTest.kt new file mode 100644 index 000000000000..23d7b86df875 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileDataInteractorTest.kt @@ -0,0 +1,118 @@ +/* + * 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.qs.tiles.impl.modes.domain.interactor + +import android.app.Flags +import android.os.UserHandle +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger +import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toCollection +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@EnableFlags(Flags.FLAG_MODES_UI) +@RunWith(AndroidJUnit4::class) +class ModesDndTileDataInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val dispatcher = kosmos.testDispatcher + private val zenModeRepository = kosmos.fakeZenModeRepository + + private val underTest by lazy { + ModesDndTileDataInteractor(context, kosmos.zenModeInteractor, dispatcher) + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI_DND_TILE) + fun availability_flagOn_isTrue() = + testScope.runTest { + val availability = underTest.availability(TEST_USER).toCollection(mutableListOf()) + + assertThat(availability).containsExactly(true) + } + + @Test + @DisableFlags(Flags.FLAG_MODES_UI_DND_TILE) + fun availability_flagOff_isFalse() = + testScope.runTest { + val availability = underTest.availability(TEST_USER).toCollection(mutableListOf()) + + assertThat(availability).containsExactly(false) + } + + @Test + fun tileData_dndChanges_updateActivated() = + testScope.runTest { + val model by + collectLastValue( + underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)) + ) + + runCurrent() + assertThat(model!!.isActivated).isFalse() + + zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND) + runCurrent() + assertThat(model!!.isActivated).isTrue() + + zenModeRepository.deactivateMode(TestModeBuilder.MANUAL_DND) + runCurrent() + assertThat(model!!.isActivated).isFalse() + } + + @Test + fun tileData_otherModeChanges_notActivated() = + testScope.runTest { + val model by + collectLastValue( + underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)) + ) + + runCurrent() + assertThat(model!!.isActivated).isFalse() + + zenModeRepository.addMode("Other mode") + runCurrent() + assertThat(model!!.isActivated).isFalse() + + zenModeRepository.activateMode("Other mode") + runCurrent() + assertThat(model!!.isActivated).isFalse() + } + + private companion object { + val TEST_USER = UserHandle.of(1)!! + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileUserActionInteractorTest.kt new file mode 100644 index 000000000000..0a35b428bbc9 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileUserActionInteractorTest.kt @@ -0,0 +1,134 @@ +/* + * 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.qs.tiles.impl.modes.domain.interactor + +import android.platform.test.annotations.EnableFlags +import android.provider.Settings +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.mainCoroutineContext +import com.android.systemui.kosmos.testScope +import com.android.systemui.qs.shared.QSSettingsPackageRepository +import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject +import com.android.systemui.qs.tiles.base.actions.qsTileIntentUserInputHandler +import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel +import com.android.systemui.statusbar.policy.data.repository.zenModeRepository +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor +import com.android.systemui.statusbar.policy.ui.dialog.mockModesDialogDelegate +import com.android.systemui.statusbar.policy.ui.dialog.modesDialogEventLogger +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags(android.app.Flags.FLAG_MODES_UI) +class ModesDndTileUserActionInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val inputHandler = kosmos.qsTileIntentUserInputHandler + private val mockDialogDelegate = kosmos.mockModesDialogDelegate + private val zenModeRepository = kosmos.zenModeRepository + private val zenModeInteractor = kosmos.zenModeInteractor + private val settingsPackageRepository = mock<QSSettingsPackageRepository>() + + private val underTest = + ModesDndTileUserActionInteractor( + kosmos.mainCoroutineContext, + inputHandler, + mockDialogDelegate, + zenModeInteractor, + kosmos.modesDialogEventLogger, + settingsPackageRepository, + ) + + @Before + fun setUp() { + whenever(settingsPackageRepository.getSettingsPackageName()).thenReturn(SETTINGS_PACKAGE) + } + + @Test + fun handleClick_dndActive_deactivatesDnd() = + testScope.runTest { + val dndMode by collectLastValue(zenModeInteractor.dndMode) + zenModeRepository.activateMode(MANUAL_DND) + assertThat(dndMode?.isActive).isTrue() + + underTest.handleInput(QSTileInputTestKtx.click(data = ModesDndTileModel(true))) + + assertThat(dndMode?.isActive).isFalse() + } + + @Test + fun handleClick_dndInactive_activatesDnd() = + testScope.runTest { + val dndMode by collectLastValue(zenModeInteractor.dndMode) + assertThat(dndMode?.isActive).isFalse() + + underTest.handleInput(QSTileInputTestKtx.click(data = ModesDndTileModel(false))) + + assertThat(dndMode?.isActive).isTrue() + } + + @Test + fun handleLongClick_active_opensSettings() = + testScope.runTest { + zenModeRepository.activateMode(MANUAL_DND) + runCurrent() + + underTest.handleInput(QSTileInputTestKtx.longClick(ModesDndTileModel(true))) + + QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { + assertThat(it.intent.`package`).isEqualTo(SETTINGS_PACKAGE) + assertThat(it.intent.action).isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS) + assertThat(it.intent.getStringExtra(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID)) + .isEqualTo(MANUAL_DND.id) + } + } + + @Test + fun handleLongClick_inactive_opensSettings() = + testScope.runTest { + zenModeRepository.activateMode(MANUAL_DND) + zenModeRepository.deactivateMode(MANUAL_DND) + runCurrent() + + underTest.handleInput(QSTileInputTestKtx.longClick(ModesDndTileModel(false))) + + QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { + assertThat(it.intent.`package`).isEqualTo(SETTINGS_PACKAGE) + assertThat(it.intent.action).isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS) + assertThat(it.intent.getStringExtra(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID)) + .isEqualTo(MANUAL_DND.id) + } + } + + companion object { + private const val SETTINGS_PACKAGE = "the.settings.package" + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesDndTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesDndTileMapperTest.kt new file mode 100644 index 000000000000..29f642a4325d --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesDndTileMapperTest.kt @@ -0,0 +1,80 @@ +/* + * 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.qs.tiles.impl.modes.ui + +import android.app.Flags +import android.graphics.drawable.TestStubDrawable +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig +import com.android.systemui.res.R +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags(Flags.FLAG_MODES_UI) +class ModesDndTileMapperTest : SysuiTestCase() { + val config = + QSTileConfigTestBuilder.build { + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.qs_dnd_icon_off, + labelRes = R.string.quick_settings_modes_label, + ) + } + + val underTest = + ModesDndTileMapper( + context.orCreateTestableResources + .apply { + addOverride(R.drawable.qs_dnd_icon_on, TestStubDrawable()) + addOverride(R.drawable.qs_dnd_icon_off, TestStubDrawable()) + } + .resources, + context.theme, + ) + + @Test + fun map_inactiveState() { + val model = ModesDndTileModel(isActivated = false) + + val state = underTest.map(config, model) + + assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.INACTIVE) + assertThat((state.icon as Icon.Loaded).res).isEqualTo(R.drawable.qs_dnd_icon_off) + assertThat(state.secondaryLabel).isEqualTo("Off") + } + + @Test + fun map_activeState() { + val model = ModesDndTileModel(isActivated = true) + + val state = underTest.map(config, model) + + assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.ACTIVE) + assertThat((state.icon as Icon.Loaded).res).isEqualTo(R.drawable.qs_dnd_icon_on) + assertThat(state.secondaryLabel).isEqualTo("On") + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index 72b003f9f463..998a7ea805a2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -33,7 +33,7 @@ import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC; -import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_OTP; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; @@ -693,7 +693,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mLockscreenUserManager.mConnectedToWifi.set(false); // Sensitive Content notifications are always redacted - assertEquals(REDACTION_TYPE_SENSITIVE_CONTENT, + assertEquals(REDACTION_TYPE_OTP, mLockscreenUserManager.getRedactionType(mSensitiveContentNotif)); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java index 65763a359c0f..f9405af3f85d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java @@ -16,7 +16,9 @@ package com.android.systemui.statusbar.notification.collection.coordinator; +import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; +import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST; @@ -340,6 +342,13 @@ public class RankingCoordinatorTest extends SysuiTestCase { } @Test + public void testAlertingSectioner_rejectsBundle() { + for (String id : SYSTEM_RESERVED_IDS) { + assertFalse(mAlertingSectioner.isInSection(makeClassifiedNotifEntry(id))); + } + } + + @Test public void statusBarStateCallbackTest() { mStatusBarStateCallback.onDozeAmountChanged(1f, 1f); verify(mInvalidationListener, times(1)) @@ -392,4 +401,11 @@ public class RankingCoordinatorTest extends SysuiTestCase { .build()); assertEquals(ambient, mEntry.getRanking().isAmbient()); } + + private NotificationEntry makeClassifiedNotifEntry(String channelId) { + NotificationChannel channel = new NotificationChannel(channelId, channelId, IMPORTANCE_LOW); + return new NotificationEntryBuilder() + .updateRanking((rankingBuilder -> rankingBuilder.setChannel(channel))) + .build(); + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index 9f35d631bd45..99f2596dbf1d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.notification.row; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC; -import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_OTP; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL; @@ -472,7 +472,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { com.android.systemui.res.R.drawable.ic_person).setStyle(messagingStyle).build(); ExpandableNotificationRow row = mHelper.createRow(messageNotif); inflateAndWait(false, mNotificationInflater, FLAG_CONTENT_VIEW_PUBLIC, - REDACTION_TYPE_SENSITIVE_CONTENT, row); + REDACTION_TYPE_OTP, row); NotificationContentView publicView = row.getPublicLayout(); assertNotNull(publicView); // The display name should be included, but not the content or message text @@ -493,7 +493,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { .build(); ExpandableNotificationRow row = mHelper.createRow(notif); inflateAndWait(false, mNotificationInflater, FLAG_CONTENT_VIEW_PUBLIC, - REDACTION_TYPE_SENSITIVE_CONTENT, row); + REDACTION_TYPE_OTP, row); NotificationContentView publicView = row.getPublicLayout(); assertNotNull(publicView); assertFalse(hasText(publicView, contentText)); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt index 31413b06e5fd..063a04ab9f37 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt @@ -36,8 +36,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.res.R import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_OTP import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC -import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.ConversationNotificationProcessor @@ -538,7 +538,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { false, notificationInflater, FLAG_CONTENT_VIEW_PUBLIC, - REDACTION_TYPE_SENSITIVE_CONTENT, + REDACTION_TYPE_OTP, newRow, ) // The display name should be included, but not the content or message text @@ -566,7 +566,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { false, notificationInflater, FLAG_CONTENT_VIEW_PUBLIC, - REDACTION_TYPE_SENSITIVE_CONTENT, + REDACTION_TYPE_OTP, newRow, ) var publicView = newRow.publicLayout diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt index 531b30b9547a..0fb0548582ce 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt @@ -18,27 +18,27 @@ package com.android.systemui.statusbar.notification.shared import com.google.common.truth.Correspondence val byKey: Correspondence<ActiveNotificationModel, String> = - Correspondence.transforming({ it?.key }, "has a key of") + Correspondence.transforming({ it.key }, "has a key of") val byIsAmbient: Correspondence<ActiveNotificationModel, Boolean> = - Correspondence.transforming({ it?.isAmbient }, "has an isAmbient value of") + Correspondence.transforming({ it.isAmbient }, "has an isAmbient value of") val byIsSuppressedFromStatusBar: Correspondence<ActiveNotificationModel, Boolean> = Correspondence.transforming( - { it?.isSuppressedFromStatusBar }, + { it.isSuppressedFromStatusBar }, "has an isSuppressedFromStatusBar value of", ) val byIsSilent: Correspondence<ActiveNotificationModel, Boolean> = - Correspondence.transforming({ it?.isSilent }, "has an isSilent value of") + Correspondence.transforming({ it.isSilent }, "has an isSilent value of") val byIsRowDismissed: Correspondence<ActiveNotificationModel, Boolean> = - Correspondence.transforming({ it?.isRowDismissed }, "has an isRowDismissed value of") + Correspondence.transforming({ it.isRowDismissed }, "has an isRowDismissed value of") val byIsLastMessageFromReply: Correspondence<ActiveNotificationModel, Boolean> = Correspondence.transforming( - { it?.isLastMessageFromReply }, + { it.isLastMessageFromReply }, "has an isLastMessageFromReply value of", ) val byIsPulsing: Correspondence<ActiveNotificationModel, Boolean> = - Correspondence.transforming({ it?.isPulsing }, "has an isPulsing value of") + Correspondence.transforming({ it.isPulsing }, "has an isPulsing value of") val byIsPromoted: Correspondence<ActiveNotificationModel, Boolean> = Correspondence.transforming( - { it?.promotedContent != null }, + { it.promotedContent != null }, "has (or doesn't have) a promoted content model", ) 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 08ecbac1582c..41cca19346f0 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 @@ -38,6 +38,7 @@ import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy import com.android.systemui.testKosmos import com.google.common.truth.Expect import com.google.common.truth.Truth.assertThat +import kotlin.math.roundToInt import org.junit.Assume import org.junit.Before import org.junit.Rule @@ -1572,7 +1573,11 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() { fullStackHeight: Float = 3000f, ) { ambientState.headsUpTop = headsUpTop - ambientState.headsUpBottom = headsUpBottom + if (NotificationsHunSharedAnimationValues.isEnabled) { + headsUpAnimator.headsUpAppearHeightBottom = headsUpBottom.roundToInt() + } else { + ambientState.headsUpBottom = headsUpBottom + } ambientState.stackTop = stackTop ambientState.stackCutoff = stackCutoff diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 78e719f6289a..549fdefd8f7a 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -115,7 +115,7 @@ <!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" --> <string name="quick_settings_tiles_stock" translatable="false"> - internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects + internet,bt,flashlight,dnd,modes_dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects </string> <!-- The tiles to display in QuickSettings --> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 8c1fd65d96d4..cd94a265aa80 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3181,8 +3181,6 @@ <string name="controls_media_active_session">The current media session cannot be hidden.</string> <!-- Label for a button that will hide media controls [CHAR_LIMIT=30] --> <string name="controls_media_dismiss_button">Hide</string> - <!-- Label for button to resume media playback [CHAR_LIMIT=NONE] --> - <string name="controls_media_resume">Resume</string> <!-- Label for button to go to media control settings screen [CHAR_LIMIT=30] --> <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]--> diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml index faf06f3d39f0..bcd49b91d894 100644 --- a/packages/SystemUI/res/values/tiles_states_strings.xml +++ b/packages/SystemUI/res/values/tiles_states_strings.xml @@ -85,6 +85,16 @@ <item>On</item> </string-array> + <!-- State names for dnd (Do not disturb) mode tile: unavailable, off, on. + This subtitle is shown when the tile is in that particular state but does not set its own + subtitle, so some of these may never appear on screen. They should still be translated as + if they could appear. [CHAR LIMIT=32] --> + <string-array name="tile_states_modes_dnd"> + <item>Unavailable</item> + <item>Off</item> + <item>On</item> + </string-array> + <!-- State names for flashlight tile: unavailable, off, on. This subtitle is shown when the tile is in that particular state but does not set its own subtitle, so some of these may never appear on screen. They should still be translated as diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt index 39f55803bb73..c4e1ccf6b62e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt @@ -31,7 +31,7 @@ import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.biometrics.shared.model.toSensorStrength import com.android.systemui.biometrics.shared.model.toSensorType import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt index 230b30bc548e..cce33fdf16c1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt @@ -21,7 +21,7 @@ import android.util.Log import com.android.systemui.biometrics.AuthController import com.android.systemui.biometrics.shared.model.PromptKind import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt index 40313e3158aa..6484116233ca 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt @@ -21,7 +21,7 @@ import android.content.res.Configuration import com.android.systemui.biometrics.data.repository.DisplayStateRepository import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.display.data.repository.DisplayRepository diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepository.kt index 7f1cb5da474d..dea3c472a476 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepository.kt @@ -21,7 +21,7 @@ import android.util.Log import com.android.settingslib.bluetooth.BluetoothCallback import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt index 55d4d3efbe27..9e0f10277197 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt @@ -22,7 +22,7 @@ import android.bluetooth.BluetoothAdapter.STATE_ON import com.android.settingslib.bluetooth.BluetoothCallback import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt index b606c19b3503..e458b8092cda 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt @@ -24,7 +24,7 @@ import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.volume.domain.interactor.AudioModeInteractor import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt index b9e1c55fbade..89208364178d 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt @@ -28,7 +28,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.bouncer.data.model.SimBouncerModel import com.android.systemui.bouncer.data.model.SimPukInputModel import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt b/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt index e6d6293733d4..636b3ab66dd5 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt @@ -24,7 +24,7 @@ import com.android.systemui.brightness.shared.model.BrightnessLog import com.android.systemui.brightness.shared.model.LinearBrightness import com.android.systemui.brightness.shared.model.formatBrightness import com.android.systemui.brightness.shared.model.logDiffForTable -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt index 183a3cc26b95..724670d955dd 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt @@ -32,7 +32,7 @@ import com.android.internal.annotations.VisibleForTesting import com.android.systemui.Dumpable import com.android.systemui.broadcast.logging.BroadcastDispatcherLogger import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.BroadcastRunning import com.android.systemui.dagger.qualifiers.Main diff --git a/packages/SystemUI/src/com/android/systemui/camera/data/repository/CameraSensorPrivacyRepository.kt b/packages/SystemUI/src/com/android/systemui/camera/data/repository/CameraSensorPrivacyRepository.kt index 7816a1487c01..dac5b7efaade 100644 --- a/packages/SystemUI/src/com/android/systemui/camera/data/repository/CameraSensorPrivacyRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/camera/data/repository/CameraSensorPrivacyRepository.kt @@ -19,7 +19,7 @@ package com.android.systemui.camera.data.repository import android.hardware.SensorPrivacyManager import android.hardware.SensorPrivacyManager.Sensors.CAMERA import android.os.UserHandle -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt index ae89b39175c1..0d7a2d9707d7 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt @@ -17,7 +17,7 @@ package com.android.systemui.communal.domain.interactor import android.content.pm.UserInfo -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN import com.android.systemui.communal.data.model.FEATURE_ENABLED import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN diff --git a/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt index 6f579a3986c8..d7ffbb2e76b8 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt @@ -18,7 +18,7 @@ package com.android.systemui.controls.settings import android.provider.Settings -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt index 69378b475938..449a995b782a 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt @@ -27,7 +27,7 @@ import com.android.systemui.Dumpable import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt index 675f00a89d23..b7315cc994a8 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt @@ -1,7 +1,7 @@ package com.android.systemui.deviceentry.data.repository import com.android.internal.widget.LockPatternUtils -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt index 29044d017d2d..f4db2cc71b38 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt @@ -27,7 +27,7 @@ import android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_HARDWARE_CONFI import android.hardware.devicestate.DeviceStateManager import android.hardware.devicestate.feature.flags.Flags as DeviceStateManagerFlags import com.android.internal.R -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState import java.util.concurrent.Executor diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt index cef45dcae43e..3c554b9ff66b 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt @@ -19,7 +19,7 @@ package com.android.systemui.display.data.repository import android.content.Context import android.content.res.Configuration import android.util.DisplayMetrics -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer diff --git a/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt b/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt index dc08570447a5..e5920924a4be 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt @@ -16,7 +16,7 @@ package com.android.systemui.flags -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.statusbar.policy.BatteryController import dagger.Lazy import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt index 922bc15c0633..4e7164ff12d7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt @@ -21,7 +21,7 @@ import android.hardware.input.InputManager.StickyModifierStateListener import android.hardware.input.StickyModifierState import android.provider.Settings import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyboard.stickykeys.StickyKeysLogger diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt index 07ed194dd68f..40861929add7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt @@ -30,7 +30,7 @@ import com.android.settingslib.notification.modes.EnableDndDialogFactory import com.android.settingslib.notification.modes.EnableDndDialogMetricsLogger import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt index e2642a0964c1..683c11a88b89 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt @@ -20,7 +20,7 @@ package com.android.systemui.keyguard.data.quickaffordance import android.content.Context import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt index 3555f06ce96f..a1dafb1438ca 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt @@ -24,7 +24,7 @@ import androidx.annotation.DrawableRes import com.android.systemui.res.R import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.controls.ControlsServiceInfo diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt index ad79177fdd76..01ff0e1344c6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt @@ -23,7 +23,7 @@ import android.content.SharedPreferences import com.android.systemui.backup.BackupHelper import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R import com.android.systemui.settings.UserFileManager diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt index 1c9bc9f39663..10fc4c2a02ff 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt @@ -23,7 +23,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.animation.Expandable -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt index d12c42a754f0..7c33e29bf25a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt @@ -21,7 +21,7 @@ import android.content.Context import com.android.systemui.res.R import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt index 760adbf58d93..56ea26e88b23 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt @@ -26,7 +26,7 @@ import android.service.quickaccesswallet.WalletCard import android.util.Log import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt index 0f5f31302670..30476b991baf 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt @@ -35,7 +35,7 @@ import com.android.systemui.biometrics.data.repository.FingerprintPropertyReposi import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt index 4d999df69588..396f60645f00 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt @@ -24,7 +24,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.biometrics.AuthController import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DevicePostureRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DevicePostureRepository.kt index 7c430920cb46..59e6a08c4511 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DevicePostureRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DevicePostureRepository.kt @@ -17,7 +17,7 @@ package com.android.systemui.keyguard.data.repository import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.shared.model.DevicePosture diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index affcd33b7170..cd0efdae337d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -24,7 +24,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.biometrics.AuthController import com.android.systemui.biometrics.data.repository.FacePropertyRepository import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt index c5a6fa199c58..63a0286832d0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt @@ -20,7 +20,7 @@ import android.app.trust.TrustManager import com.android.keyguard.TrustGrantFlags import com.android.keyguard.logging.TrustRepositoryLogger import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index 54af8f5b9806..f53421d539fe 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -101,14 +101,14 @@ constructor( ) .collect { ( - _, + detailedWakefulness, startedStep, canWakeDirectlyToGone, ) -> val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value val biometricUnlockMode = keyguardInteractor.biometricUnlockState.value.mode val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value - val shouldShowCommunal = communalSettingsInteractor.autoOpenEnabled.value + val autoOpenCommunal = communalSettingsInteractor.autoOpenEnabled.value if (!maybeHandleInsecurePowerGesture()) { val shouldTransitionToLockscreen = @@ -135,8 +135,12 @@ constructor( (!KeyguardWmStateRefactor.isEnabled && canDismissLockscreen()) || (KeyguardWmStateRefactor.isEnabled && canWakeDirectlyToGone) + // Avoid transitioning to communal automatically if the device is waking + // up due to motion. val shouldTransitionToCommunal = - communalSettingsInteractor.isV2FlagEnabled() && shouldShowCommunal + communalSettingsInteractor.isV2FlagEnabled() && + autoOpenCommunal && + !detailedWakefulness.isAwakeFromMotionOrLift() if (shouldTransitionToGone) { // TODO(b/360368320): Adapt for scene framework 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 1fc41085f772..4aaa1fab4c65 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 @@ -34,8 +34,9 @@ import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositor import com.android.systemui.keyguard.shared.model.BiometricUnlockMode.Companion.isWakeAndUnlock import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.WakefulnessModel import com.android.systemui.scene.shared.flag.SceneContainerFlag -import com.android.systemui.util.kotlin.Utils.Companion.sample +import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -121,9 +122,10 @@ constructor( private fun shouldTransitionToCommunal( shouldShowCommunal: Boolean, isCommunalAvailable: Boolean, + wakefulness: WakefulnessModel, ) = if (communalSettingsInteractor.isV2FlagEnabled()) { - shouldShowCommunal + shouldShowCommunal && !wakefulness.isAwakeFromMotionOrLift() } else { isCommunalAvailable && dreamManager.canStartDreaming(false) } @@ -148,14 +150,14 @@ constructor( } scope.launch { - powerInteractor.isAwake + powerInteractor.detailedWakefulness .debounce(50L) - .filterRelevantKeyguardStateAnd { isAwake -> isAwake } - .sample( + .filterRelevantKeyguardStateAnd { wakefulness -> wakefulness.isAwake() } + .sampleCombine( communalInteractor.isCommunalAvailable, communalSettingsInteractor.autoOpenEnabled, ) - .collect { (_, isCommunalAvailable, shouldShowCommunal) -> + .collect { (detailedWakefulness, isCommunalAvailable, shouldShowCommunal) -> val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value val isKeyguardGoingAway = keyguardInteractor.isKeyguardGoingAway.value @@ -186,7 +188,11 @@ constructor( } else if (isKeyguardOccludedLegacy) { startTransitionTo(KeyguardState.OCCLUDED) } else if ( - shouldTransitionToCommunal(shouldShowCommunal, isCommunalAvailable) + shouldTransitionToCommunal( + shouldShowCommunal, + isCommunalAvailable, + detailedWakefulness, + ) ) { if (!SceneContainerFlag.isEnabled) { transitionToGlanceableHub() @@ -208,7 +214,7 @@ constructor( scope.launch { powerInteractor.detailedWakefulness .filterRelevantKeyguardStateAnd { it.isAwake() } - .sample( + .sampleCombine( communalSettingsInteractor.autoOpenEnabled, communalInteractor.isCommunalAvailable, keyguardInteractor.biometricUnlockState, @@ -217,7 +223,7 @@ constructor( ) .collect { ( - _, + detailedWakefulness, shouldShowCommunal, isCommunalAvailable, biometricUnlockState, @@ -245,7 +251,11 @@ constructor( ) } } else if ( - shouldTransitionToCommunal(shouldShowCommunal, isCommunalAvailable) + shouldTransitionToCommunal( + shouldShowCommunal, + isCommunalAvailable, + detailedWakefulness, + ) ) { if (!SceneContainerFlag.isEnabled) { transitionToGlanceableHub() diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt index 309b6751176c..c2efc7559487 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt @@ -1256,7 +1256,7 @@ class LegacyMediaDataManagerImpl( return MediaAction( Icon.createWithResource(context, iconId).setTint(themeText).loadDrawable(context), action, - context.getString(R.string.controls_media_resume), + context.getString(R.string.controls_media_button_play), if (Flags.mediaControlsUiUpdate()) { context.getDrawable(R.drawable.ic_media_play_button_container) } else { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt index dbd2250a75b0..a7c5a36b804a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt @@ -521,7 +521,7 @@ constructor( return MediaAction( Icon.createWithResource(context, iconId).setTint(themeText).loadDrawable(context), action, - context.getString(R.string.controls_media_resume), + context.getString(R.string.controls_media_button_play), if (Flags.mediaControlsUiUpdate()) { context.getDrawable(R.drawable.ic_media_play_button_container) } else { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt index 94df4b353c94..ca4a65953cba 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt @@ -1193,7 +1193,7 @@ class MediaDataProcessor( return MediaAction( Icon.createWithResource(context, iconId).setTint(themeText).loadDrawable(context), action, - context.getString(R.string.controls_media_resume), + context.getString(R.string.controls_media_button_play), if (Flags.mediaControlsUiUpdate()) { context.getDrawable(R.drawable.ic_media_play_button_container) } else { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java index f79693138e24..d0c6a3e6a3ef 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java @@ -66,6 +66,7 @@ import androidx.annotation.VisibleForTesting; import androidx.core.graphics.drawable.IconCompat; import com.android.internal.annotations.GuardedBy; +import com.android.media.flags.Flags; import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.Utils; import com.android.settingslib.bluetooth.BluetoothUtils; @@ -78,7 +79,6 @@ import com.android.settingslib.media.InputMediaDevice; import com.android.settingslib.media.InputRouteManager; import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; -import com.android.settingslib.media.flags.Flags; import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.animation.ActivityTransitionAnimator; import com.android.systemui.animation.DialogTransitionAnimator; @@ -226,7 +226,7 @@ public class MediaSwitchingController InfoMediaManager.createInstance(mContext, packageName, userHandle, lbm, token); mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName); mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName); - mOutputMediaItemListProxy = new OutputMediaItemListProxy(); + mOutputMediaItemListProxy = new OutputMediaItemListProxy(context); mDialogTransitionAnimator = dialogTransitionAnimator; mNearbyMediaDevicesManager = nearbyMediaDevicesManager; mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromSystemColors(mContext); @@ -308,7 +308,8 @@ public class MediaSwitchingController } private MediaController getMediaController() { - if (mToken != null && Flags.usePlaybackInfoForRoutingControls()) { + if (mToken != null + && com.android.settingslib.media.flags.Flags.usePlaybackInfoForRoutingControls()) { return new MediaController(mContext, mToken); } else { for (NotificationEntry entry : mNotifCollection.getAllNotifs()) { @@ -577,19 +578,35 @@ public class MediaSwitchingController private void buildMediaItems(List<MediaDevice> devices) { synchronized (mMediaDevicesLock) { - List<MediaItem> updatedMediaItems = - buildMediaItems(mOutputMediaItemListProxy.getOutputMediaItemList(), devices); - mOutputMediaItemListProxy.clearAndAddAll(updatedMediaItems); + if (!mLocalMediaManager.isPreferenceRouteListingExist()) { + attachRangeInfo(devices); + Collections.sort(devices, Comparator.naturalOrder()); + } + if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) { + // For the first time building list, to make sure the top device is the connected + // device. + boolean needToHandleMutingExpectedDevice = + hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote(); + final MediaDevice connectedMediaDevice = + needToHandleMutingExpectedDevice ? null : getCurrentConnectedMediaDevice(); + mOutputMediaItemListProxy.updateMediaDevices( + devices, + getSelectedMediaDevice(), + connectedMediaDevice, + needToHandleMutingExpectedDevice, + getConnectNewDeviceItem()); + } else { + List<MediaItem> updatedMediaItems = + buildMediaItems( + mOutputMediaItemListProxy.getOutputMediaItemList(), devices); + mOutputMediaItemListProxy.clearAndAddAll(updatedMediaItems); + } } } protected List<MediaItem> buildMediaItems( List<MediaItem> oldMediaItems, List<MediaDevice> devices) { synchronized (mMediaDevicesLock) { - if (!mLocalMediaManager.isPreferenceRouteListingExist()) { - attachRangeInfo(devices); - Collections.sort(devices, Comparator.naturalOrder()); - } // For the first time building list, to make sure the top device is the connected // device. boolean needToHandleMutingExpectedDevice = @@ -648,8 +665,7 @@ public class MediaSwitchingController .map(MediaItem::createDeviceMediaItem) .collect(Collectors.toList()); - boolean shouldAddFirstSeenSelectedDevice = - com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping(); + boolean shouldAddFirstSeenSelectedDevice = Flags.enableOutputSwitcherDeviceGrouping(); if (shouldAddFirstSeenSelectedDevice) { finalMediaItems.clear(); @@ -675,7 +691,7 @@ public class MediaSwitchingController } private boolean enableInputRouting() { - return com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl(); + return Flags.enableAudioInputDeviceRoutingAndVolumeControl(); } private void buildInputMediaItems(List<MediaDevice> devices) { @@ -703,8 +719,7 @@ public class MediaSwitchingController if (connectedMediaDevice != null) { selectedDevicesIds.add(connectedMediaDevice.getId()); } - boolean groupSelectedDevices = - com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping(); + boolean groupSelectedDevices = Flags.enableOutputSwitcherDeviceGrouping(); int nextSelectedItemIndex = 0; boolean suggestedDeviceAdded = false; boolean displayGroupAdded = false; @@ -879,6 +894,11 @@ public class MediaSwitchingController return mLocalMediaManager.getCurrentConnectedDevice(); } + @VisibleForTesting + void clearMediaItemList() { + mOutputMediaItemListProxy.clear(); + } + boolean addDeviceToPlayMedia(MediaDevice device) { mMetricLogger.logInteractionExpansion(device); return mLocalMediaManager.addDeviceToPlayMedia(device); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java index 1c9c0b102cb7..45ca2c6ee8e5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * 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. @@ -16,22 +16,175 @@ package com.android.systemui.media.dialog; +import android.content.Context; + +import androidx.annotation.Nullable; + +import com.android.media.flags.Flags; +import com.android.settingslib.media.MediaDevice; +import com.android.systemui.res.R; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; /** A proxy of holding the list of Output Switcher's output media items. */ public class OutputMediaItemListProxy { + private final Context mContext; private final List<MediaItem> mOutputMediaItemList; - public OutputMediaItemListProxy() { + // Use separated lists to hold different media items and create the list of output media items + // by using those separated lists and group dividers. + private final List<MediaItem> mSelectedMediaItems; + private final List<MediaItem> mSuggestedMediaItems; + private final List<MediaItem> mSpeakersAndDisplaysMediaItems; + @Nullable private MediaItem mConnectNewDeviceMediaItem; + + public OutputMediaItemListProxy(Context context) { + mContext = context; mOutputMediaItemList = new CopyOnWriteArrayList<>(); + mSelectedMediaItems = new CopyOnWriteArrayList<>(); + mSuggestedMediaItems = new CopyOnWriteArrayList<>(); + mSpeakersAndDisplaysMediaItems = new CopyOnWriteArrayList<>(); } /** Returns the list of output media items. */ public List<MediaItem> getOutputMediaItemList() { + if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) { + if (isEmpty() && !mOutputMediaItemList.isEmpty()) { + // Ensures mOutputMediaItemList is empty when all individual media item lists are + // empty, preventing unexpected state issues. + mOutputMediaItemList.clear(); + } else if (!isEmpty() && mOutputMediaItemList.isEmpty()) { + // When any individual media item list is modified, the cached mOutputMediaItemList + // is emptied. On the next request for the output media item list, a fresh list is + // created and stored in the cache. + mOutputMediaItemList.addAll(createOutputMediaItemList()); + } + } return mOutputMediaItemList; } + private List<MediaItem> createOutputMediaItemList() { + List<MediaItem> finalMediaItems = new CopyOnWriteArrayList<>(); + finalMediaItems.addAll(mSelectedMediaItems); + if (!mSuggestedMediaItems.isEmpty()) { + finalMediaItems.add( + MediaItem.createGroupDividerMediaItem( + mContext.getString( + R.string.media_output_group_title_suggested_device))); + finalMediaItems.addAll(mSuggestedMediaItems); + } + if (!mSpeakersAndDisplaysMediaItems.isEmpty()) { + finalMediaItems.add( + MediaItem.createGroupDividerMediaItem( + mContext.getString( + R.string.media_output_group_title_speakers_and_displays))); + finalMediaItems.addAll(mSpeakersAndDisplaysMediaItems); + } + if (mConnectNewDeviceMediaItem != null) { + finalMediaItems.add(mConnectNewDeviceMediaItem); + } + return finalMediaItems; + } + + /** Updates the list of output media items with a given list of media devices. */ + public void updateMediaDevices( + List<MediaDevice> devices, + List<MediaDevice> selectedDevices, + @Nullable MediaDevice connectedMediaDevice, + boolean needToHandleMutingExpectedDevice, + @Nullable MediaItem connectNewDeviceMediaItem) { + Set<String> selectedOrConnectedMediaDeviceIds = + selectedDevices.stream().map(MediaDevice::getId).collect(Collectors.toSet()); + if (connectedMediaDevice != null) { + selectedOrConnectedMediaDeviceIds.add(connectedMediaDevice.getId()); + } + + List<MediaItem> selectedMediaItems = new ArrayList<>(); + List<MediaItem> suggestedMediaItems = new ArrayList<>(); + List<MediaItem> speakersAndDisplaysMediaItems = new ArrayList<>(); + Map<String, MediaItem> deviceIdToMediaItemMap = new HashMap<>(); + buildMediaItems( + devices, + selectedOrConnectedMediaDeviceIds, + needToHandleMutingExpectedDevice, + selectedMediaItems, + suggestedMediaItems, + speakersAndDisplaysMediaItems, + deviceIdToMediaItemMap); + + List<MediaItem> updatedSelectedMediaItems = new CopyOnWriteArrayList<>(); + List<MediaItem> updatedSuggestedMediaItems = new CopyOnWriteArrayList<>(); + List<MediaItem> updatedSpeakersAndDisplaysMediaItems = new CopyOnWriteArrayList<>(); + if (isEmpty()) { + updatedSelectedMediaItems.addAll(selectedMediaItems); + updatedSuggestedMediaItems.addAll(suggestedMediaItems); + updatedSpeakersAndDisplaysMediaItems.addAll(speakersAndDisplaysMediaItems); + } else { + Set<String> updatedDeviceIds = new HashSet<>(); + // Preserve the existing media item order while updating with the latest device + // information. Some items may retain their original group (suggested, speakers and + // displays) to maintain this order. + updateMediaItems( + mSelectedMediaItems, + updatedSelectedMediaItems, + deviceIdToMediaItemMap, + updatedDeviceIds); + updateMediaItems( + mSuggestedMediaItems, + updatedSuggestedMediaItems, + deviceIdToMediaItemMap, + updatedDeviceIds); + updateMediaItems( + mSpeakersAndDisplaysMediaItems, + updatedSpeakersAndDisplaysMediaItems, + deviceIdToMediaItemMap, + updatedDeviceIds); + + // Append new media items that are not already in the existing lists to the output list. + List<MediaItem> remainingMediaItems = new ArrayList<>(); + remainingMediaItems.addAll( + getRemainingMediaItems(selectedMediaItems, updatedDeviceIds)); + remainingMediaItems.addAll( + getRemainingMediaItems(suggestedMediaItems, updatedDeviceIds)); + remainingMediaItems.addAll( + getRemainingMediaItems(speakersAndDisplaysMediaItems, updatedDeviceIds)); + updatedSpeakersAndDisplaysMediaItems.addAll(remainingMediaItems); + } + + if (Flags.enableOutputSwitcherDeviceGrouping() && !updatedSelectedMediaItems.isEmpty()) { + MediaItem selectedMediaItem = updatedSelectedMediaItems.get(0); + Optional<MediaDevice> mediaDeviceOptional = selectedMediaItem.getMediaDevice(); + if (mediaDeviceOptional.isPresent()) { + MediaItem updatedMediaItem = + MediaItem.createDeviceMediaItem( + mediaDeviceOptional.get(), /* isFirstDeviceInGroup= */ true); + updatedSelectedMediaItems.remove(0); + updatedSelectedMediaItems.add(0, updatedMediaItem); + } + } + + mSelectedMediaItems.clear(); + mSelectedMediaItems.addAll(updatedSelectedMediaItems); + mSuggestedMediaItems.clear(); + mSuggestedMediaItems.addAll(updatedSuggestedMediaItems); + mSpeakersAndDisplaysMediaItems.clear(); + mSpeakersAndDisplaysMediaItems.addAll(updatedSpeakersAndDisplaysMediaItems); + mConnectNewDeviceMediaItem = connectNewDeviceMediaItem; + + // The cached mOutputMediaItemList is cleared upon any update to individual media item + // lists. This ensures getOutputMediaItemList() computes and caches a fresh list on the next + // invocation. + mOutputMediaItemList.clear(); + } + /** Updates the list of output media items with the given list. */ public void clearAndAddAll(List<MediaItem> updatedMediaItems) { mOutputMediaItemList.clear(); @@ -40,16 +193,112 @@ public class OutputMediaItemListProxy { /** Removes the media items with muting expected devices. */ public void removeMutingExpectedDevices() { + if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) { + mSelectedMediaItems.removeIf((MediaItem::isMutingExpectedDevice)); + mSuggestedMediaItems.removeIf((MediaItem::isMutingExpectedDevice)); + mSpeakersAndDisplaysMediaItems.removeIf((MediaItem::isMutingExpectedDevice)); + if (mConnectNewDeviceMediaItem != null + && mConnectNewDeviceMediaItem.isMutingExpectedDevice()) { + mConnectNewDeviceMediaItem = null; + } + } mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice)); } /** Clears the output media item list. */ public void clear() { + if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) { + mSelectedMediaItems.clear(); + mSuggestedMediaItems.clear(); + mSpeakersAndDisplaysMediaItems.clear(); + mConnectNewDeviceMediaItem = null; + } mOutputMediaItemList.clear(); } /** Returns whether the output media item list is empty. */ public boolean isEmpty() { - return mOutputMediaItemList.isEmpty(); + if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) { + return mSelectedMediaItems.isEmpty() + && mSuggestedMediaItems.isEmpty() + && mSpeakersAndDisplaysMediaItems.isEmpty() + && (mConnectNewDeviceMediaItem == null); + } else { + return mOutputMediaItemList.isEmpty(); + } + } + + private void buildMediaItems( + List<MediaDevice> devices, + Set<String> selectedOrConnectedMediaDeviceIds, + boolean needToHandleMutingExpectedDevice, + List<MediaItem> selectedMediaItems, + List<MediaItem> suggestedMediaItems, + List<MediaItem> speakersAndDisplaysMediaItems, + Map<String, MediaItem> deviceIdToMediaItemMap) { + for (MediaDevice device : devices) { + String deviceId = device.getId(); + MediaItem mediaItem = MediaItem.createDeviceMediaItem(device); + if (needToHandleMutingExpectedDevice && device.isMutingExpectedDevice()) { + selectedMediaItems.add(0, mediaItem); + } else if (!needToHandleMutingExpectedDevice + && selectedOrConnectedMediaDeviceIds.contains(device.getId())) { + if (Flags.enableOutputSwitcherDeviceGrouping()) { + selectedMediaItems.add(mediaItem); + } else { + selectedMediaItems.add(0, mediaItem); + } + } else if (device.isSuggestedDevice()) { + suggestedMediaItems.add(mediaItem); + } else { + speakersAndDisplaysMediaItems.add(mediaItem); + } + deviceIdToMediaItemMap.put(deviceId, mediaItem); + } + } + + /** Returns a list of media items that remains the same order as the existing media items. */ + private void updateMediaItems( + List<MediaItem> existingMediaItems, + List<MediaItem> updatedMediaItems, + Map<String, MediaItem> deviceIdToMediaItemMap, + Set<String> updatedDeviceIds) { + List<String> existingDeviceIds = getDeviceIds(existingMediaItems); + for (String deviceId : existingDeviceIds) { + MediaItem mediaItem = deviceIdToMediaItemMap.get(deviceId); + if (mediaItem != null) { + updatedMediaItems.add(mediaItem); + updatedDeviceIds.add(deviceId); + } + } + } + + /** + * Returns media items from the input list that are not associated with the given device IDs. + */ + private List<MediaItem> getRemainingMediaItems( + List<MediaItem> mediaItems, Set<String> deviceIds) { + List<MediaItem> remainingMediaItems = new ArrayList<>(); + for (MediaItem item : mediaItems) { + Optional<MediaDevice> mediaDeviceOptional = item.getMediaDevice(); + if (mediaDeviceOptional.isPresent()) { + String deviceId = mediaDeviceOptional.get().getId(); + if (!deviceIds.contains(deviceId)) { + remainingMediaItems.add(item); + } + } + } + return remainingMediaItems; + } + + /** Returns a list of media device IDs for the given list of media items. */ + private List<String> getDeviceIds(List<MediaItem> mediaItems) { + List<String> deviceIds = new ArrayList<>(); + for (MediaItem item : mediaItems) { + if (item != null && item.getMediaDevice().isPresent()) { + deviceIds.add(item.getMediaDevice().get().getId()); + } + } + return deviceIds; } } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt index 4ff54d4eae65..42d27619f60f 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt @@ -26,7 +26,7 @@ import android.os.IBinder import android.util.Log import android.view.Display import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt b/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt index 43bd6aa37b5a..faa77e51ec24 100644 --- a/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt @@ -24,7 +24,7 @@ import android.content.IntentFilter import android.os.PowerManager import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.power.shared.model.DozeScreenStateModel diff --git a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt index 297c6af5a4a7..f368c53c5b39 100644 --- a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt +++ b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt @@ -61,6 +61,11 @@ data class WakefulnessModel( (lastWakeReason == WakeSleepReason.TAP || lastWakeReason == WakeSleepReason.GESTURE) } + fun isAwakeFromMotionOrLift(): Boolean { + return isAwake() && + (lastWakeReason == WakeSleepReason.MOTION || lastWakeReason == WakeSleepReason.LIFT) + } + override fun logDiffs(prevVal: WakefulnessModel, row: TableRowLogger) { row.logChange(columnName = "wakefulness", value = toString()) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/ForegroundServicesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/ForegroundServicesRepository.kt index bd9d70c13572..eb99fec7a0a8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/ForegroundServicesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/ForegroundServicesRepository.kt @@ -17,7 +17,7 @@ package com.android.systemui.qs.footer.data.repository import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.FgsManagerController import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/QSSettingsRestoredRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/QSSettingsRestoredRepository.kt index 1cd5d100ec00..e3a8ffd0f480 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/QSSettingsRestoredRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/QSSettingsRestoredRepository.kt @@ -6,7 +6,7 @@ import android.os.UserHandle import android.provider.Settings import android.util.Log import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.common.shared.model.PackageChangeModel.Empty.user import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.kt index 88a49ee109aa..53d2554f0e82 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.kt @@ -16,7 +16,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking import com.android.systemui.qs.pipeline.domain.model.AutoAddable diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.kt index 76bfad936116..a3b4c71c7641 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.kt @@ -16,7 +16,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt index e9c91ca0db12..66af6d8b3e18 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt @@ -19,7 +19,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable import android.content.Context import android.hardware.display.ColorDisplayManager import android.hardware.display.NightDisplayListener -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.NightDisplayListenerModule import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt index 88d7f06dfada..ff3fd3781181 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt @@ -21,7 +21,7 @@ import android.content.pm.PackageManager import android.content.res.Resources import android.text.TextUtils import com.android.systemui.res.R -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.kt index 3f619c08261d..c66c9dc9ba6a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.kt @@ -18,7 +18,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable import android.content.pm.UserInfo import android.os.UserHandle -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.pipeline.data.restoreprocessors.WorkTileRestoreProcessor import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt index 61a8fa3d2a6e..cd0b70e5e988 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt @@ -27,6 +27,7 @@ object SubtitleArrayMapping { subtitleIdsMap["cell"] = R.array.tile_states_cell subtitleIdsMap["battery"] = R.array.tile_states_battery subtitleIdsMap["dnd"] = R.array.tile_states_dnd + subtitleIdsMap["modes_dnd"] = R.array.tile_states_modes_dnd subtitleIdsMap["flashlight"] = R.array.tile_states_flashlight subtitleIdsMap["rotation"] = R.array.tile_states_rotation subtitleIdsMap["bt"] = R.array.tile_states_bt diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesDndTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesDndTile.kt new file mode 100644 index 000000000000..52b02066c35a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesDndTile.kt @@ -0,0 +1,135 @@ +/* + * 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.qs.tiles + +import android.content.Intent +import android.os.Handler +import android.os.Looper +import androidx.annotation.DrawableRes +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.coroutineScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.internal.logging.MetricsLogger +import com.android.systemui.animation.Expandable +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.modes.shared.ModesUi +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.plugins.qs.QSTile.BooleanState +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.qs.QSHost +import com.android.systemui.qs.QsEventLogger +import com.android.systemui.qs.asQSTileIcon +import com.android.systemui.qs.logging.QSLogger +import com.android.systemui.qs.tileimpl.QSTileImpl +import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesDndTileDataInteractor +import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesDndTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel +import com.android.systemui.qs.tiles.impl.modes.ui.ModesDndTileMapper +import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.res.R +import javax.inject.Inject +import kotlinx.coroutines.runBlocking + +/** + * Standalone tile used to control the DND Mode. Contrast to [ModesTile] (the tile that opens a + * dialog showing the list of all modes) and [DndTile] (the tile used to toggle interruption + * filtering in the pre-MODES_UI world). + */ +class ModesDndTile +@Inject +constructor( + host: QSHost, + uiEventLogger: QsEventLogger, + @Background backgroundLooper: Looper, + @Main mainHandler: Handler, + falsingManager: FalsingManager, + metricsLogger: MetricsLogger, + statusBarStateController: StatusBarStateController, + activityStarter: ActivityStarter, + qsLogger: QSLogger, + qsTileConfigProvider: QSTileConfigProvider, + private val dataInteractor: ModesDndTileDataInteractor, + private val tileMapper: ModesDndTileMapper, + private val userActionInteractor: ModesDndTileUserActionInteractor, +) : + QSTileImpl<BooleanState>( + host, + uiEventLogger, + backgroundLooper, + mainHandler, + falsingManager, + metricsLogger, + statusBarStateController, + activityStarter, + qsLogger, + ) { + + private lateinit var tileState: QSTileState + private val config = qsTileConfigProvider.getConfig(TILE_SPEC) + + init { + lifecycle.coroutineScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { + dataInteractor.tileData().collect { refreshState(it) } + } + } + } + + override fun isAvailable(): Boolean = ModesUi.isEnabled && android.app.Flags.modesUiDndTile() + + override fun getTileLabel(): CharSequence = + mContext.getString(R.string.quick_settings_dnd_label) + + override fun newTileState(): BooleanState = BooleanState() + + override fun handleClick(expandable: Expandable?) = runBlocking { + userActionInteractor.handleClick() + } + + override fun getLongClickIntent(): Intent? = userActionInteractor.getSettingsIntent() + + @VisibleForTesting + public override fun handleUpdateState(state: BooleanState?, arg: Any?) { + val model = arg as? ModesDndTileModel ?: dataInteractor.getCurrentTileModel() + + tileState = tileMapper.map(config, model) + state?.apply { + value = model.isActivated + this.state = tileState.activationState.legacyState + icon = + tileState.icon?.asQSTileIcon() + ?: maybeLoadResourceIcon(iconResId(model.isActivated)) + label = tileLabel + secondaryLabel = tileState.secondaryLabel + contentDescription = tileState.contentDescription + expandedAccessibilityClassName = tileState.expandedAccessibilityClassName + } + } + + @DrawableRes + private fun iconResId(activated: Boolean): Int = + if (activated) R.drawable.qs_dnd_icon_on else R.drawable.qs_dnd_icon_off + + companion object { + const val TILE_SPEC = "modes_dnd" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt index 1544804c3291..38eb5947bd71 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt @@ -17,7 +17,7 @@ package com.android.systemui.qs.tiles.impl.flashlight.domain.interactor import android.os.UserHandle -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor import com.android.systemui.qs.tiles.impl.flashlight.domain.model.FlashlightTileModel diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileDataInteractor.kt new file mode 100644 index 000000000000..b1ae3ba4381a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileDataInteractor.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.modes.domain.interactor + +import android.content.Context +import android.os.UserHandle +import com.android.app.tracing.coroutines.flow.flowName +import com.android.settingslib.notification.modes.ZenMode +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.modes.shared.ModesUi +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger +import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel +import com.android.systemui.shade.ShadeDisplayAware +import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +class ModesDndTileDataInteractor +@Inject +constructor( + @ShadeDisplayAware val context: Context, + val zenModeInteractor: ZenModeInteractor, + @Background val bgDispatcher: CoroutineDispatcher, +) : QSTileDataInteractor<ModesDndTileModel> { + + override fun tileData( + user: UserHandle, + triggers: Flow<DataUpdateTrigger>, + ): Flow<ModesDndTileModel> = tileData() + + /** + * An adapted version of the base class' [tileData] method for use in an old-style tile. + * + * TODO(b/299909989): Remove after the transition. + */ + fun tileData() = + zenModeInteractor.dndMode + .filterNotNull() + .map { dndMode -> buildTileData(dndMode) } + .flowName("tileData") + .flowOn(bgDispatcher) + .distinctUntilChanged() + + fun getCurrentTileModel() = buildTileData(zenModeInteractor.getDndMode()) + + private fun buildTileData(dndMode: ZenMode): ModesDndTileModel { + return ModesDndTileModel(isActivated = dndMode.isActive) + } + + override fun availability(user: UserHandle): Flow<Boolean> = + flowOf(ModesUi.isEnabled && android.app.Flags.modesUiDndTile()) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileUserActionInteractor.kt new file mode 100644 index 000000000000..e8fcea070ede --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileUserActionInteractor.kt @@ -0,0 +1,113 @@ +/* + * 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.qs.tiles.impl.modes.domain.interactor + +import android.content.Intent +import android.provider.Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS +import android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID +import android.util.Log +import com.android.systemui.animation.Expandable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.shared.QSSettingsPackageRepository +import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler +import com.android.systemui.qs.tiles.base.interactor.QSTileInput +import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction +import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate +import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogEventLogger +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.withContext + +@SysUISingleton +class ModesDndTileUserActionInteractor +@Inject +constructor( + @Main private val mainContext: CoroutineContext, + private val qsTileIntentUserInputHandler: QSTileIntentUserInputHandler, + // TODO(b/353896370): The domain layer should not have to depend on the UI layer. + private val dialogDelegate: ModesDialogDelegate, + private val zenModeInteractor: ZenModeInteractor, + private val dialogEventLogger: ModesDialogEventLogger, + private val settingsPackageRepository: QSSettingsPackageRepository, +) : QSTileUserActionInteractor<ModesDndTileModel> { + + override suspend fun handleInput(input: QSTileInput<ModesDndTileModel>) { + with(input) { + when (action) { + is QSTileUserAction.Click, + is QSTileUserAction.ToggleClick -> { + handleClick() + } + is QSTileUserAction.LongClick -> { + handleLongClick(action.expandable) + } + } + } + } + + suspend fun handleClick() { + val dnd = zenModeInteractor.dndMode.value + if (dnd == null) { + Log.wtf(TAG, "No DND!?") + return + } + + if (!dnd.isActive) { + if (zenModeInteractor.shouldAskForZenDuration(dnd)) { + dialogEventLogger.logOpenDurationDialog(dnd) + withContext(mainContext) { + // NOTE: The dialog handles turning on the mode itself. + val dialog = dialogDelegate.makeDndDurationDialog() + dialog.show() + } + } else { + dialogEventLogger.logModeOn(dnd) + zenModeInteractor.activateMode(dnd) + } + } else { + dialogEventLogger.logModeOff(dnd) + zenModeInteractor.deactivateMode(dnd) + } + } + + private fun handleLongClick(expandable: Expandable?) { + val intent = getSettingsIntent() + if (intent != null) { + qsTileIntentUserInputHandler.handle(expandable, intent) + } + } + + fun getSettingsIntent(): Intent? { + val dnd = zenModeInteractor.dndMode.value + if (dnd == null) { + Log.wtf(TAG, "No DND!?") + return null + } + + return Intent(ACTION_AUTOMATIC_ZEN_RULE_SETTINGS) + .putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, dnd.id) + .setPackage(settingsPackageRepository.getSettingsPackageName()) + } + + companion object { + const val TAG = "ModesDndTileUserActionInteractor" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesDndTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesDndTileModel.kt new file mode 100644 index 000000000000..eab798897aa3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesDndTileModel.kt @@ -0,0 +1,19 @@ +/* + * 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.qs.tiles.impl.modes.domain.model + +data class ModesDndTileModel(val isActivated: Boolean) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesDndTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesDndTileMapper.kt new file mode 100644 index 000000000000..4869b6f74554 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesDndTileMapper.kt @@ -0,0 +1,61 @@ +/* + * 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.qs.tiles.impl.modes.ui + +import android.content.res.Resources +import android.widget.Switch +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.res.R +import com.android.systemui.shade.ShadeDisplayAware +import javax.inject.Inject + +class ModesDndTileMapper +@Inject +constructor(@ShadeDisplayAware private val resources: Resources, val theme: Resources.Theme) : + QSTileDataToStateMapper<ModesDndTileModel> { + override fun map(config: QSTileConfig, data: ModesDndTileModel): QSTileState = + QSTileState.build(resources, theme, config.uiConfig) { + val iconResource = + if (data.isActivated) R.drawable.qs_dnd_icon_on else R.drawable.qs_dnd_icon_off + icon = + Icon.Loaded( + resources.getDrawable(iconResource, theme), + res = iconResource, + contentDescription = null, + ) + + activationState = + if (data.isActivated) { + QSTileState.ActivationState.ACTIVE + } else { + QSTileState.ActivationState.INACTIVE + } + label = resources.getString(R.string.quick_settings_dnd_label) + secondaryLabel = + resources.getString( + if (data.isActivated) R.string.zen_mode_on else R.string.zen_mode_off + ) + contentDescription = label + supportedActions = + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + expandedAccessibilityClass = Switch::class + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/SensorPrivacyToggleTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/SensorPrivacyToggleTileDataInteractor.kt index 7117629622e6..a8e9c5663f39 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/SensorPrivacyToggleTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/SensorPrivacyToggleTileDataInteractor.kt @@ -22,7 +22,7 @@ import android.hardware.SensorPrivacyManager.Sensors.Sensor import android.os.UserHandle import android.provider.DeviceConfig import android.util.Log -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 7bb831baec20..3ad0867192d3 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -33,7 +33,7 @@ import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor import com.android.systemui.bouncer.shared.logging.BouncerUiEvent import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorActual -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.DisplayId diff --git a/packages/SystemUI/src/com/android/systemui/security/data/repository/SecurityRepository.kt b/packages/SystemUI/src/com/android/systemui/security/data/repository/SecurityRepository.kt index 7e967f436ecb..0b039fecc19e 100644 --- a/packages/SystemUI/src/com/android/systemui/security/data/repository/SecurityRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/security/data/repository/SecurityRepository.kt @@ -17,7 +17,7 @@ package com.android.systemui.security.data.repository import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.security.data.model.SecurityModel diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/PrivacyChipRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/PrivacyChipRepository.kt index 91c92cc8cd2f..ce74cb7d54d2 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/PrivacyChipRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/PrivacyChipRepository.kt @@ -20,7 +20,7 @@ import android.content.IntentFilter import android.os.UserHandle import android.safetycenter.SafetyCenterManager import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 657c86b10f16..e66b21866902 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -1088,14 +1088,18 @@ public class KeyguardIndicationController { if (!TextUtils.equals(mTopIndicationView.getText(), newIndication)) { mWakeLock.setAcquired(true); + final KeyguardIndication.Builder builder = new KeyguardIndication.Builder() + .setMessage(newIndication) + .setTextColor(ColorStateList.valueOf( + useMisalignmentColor + ? mContext.getColor(R.color.misalignment_text_color) + : Color.WHITE)); + if (mBiometricMessage != null && newIndication == mBiometricMessage) { + builder.setForceAccessibilityLiveRegionAssertive(); + } + mTopIndicationView.switchIndication(newIndication, - new KeyguardIndication.Builder() - .setMessage(newIndication) - .setTextColor(ColorStateList.valueOf( - useMisalignmentColor - ? mContext.getColor(R.color.misalignment_text_color) - : Color.WHITE)) - .build(), + builder.build(), true, () -> mWakeLock.setAcquired(false)); } return; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java index 3180a06ae787..fdcd39d1d616 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java @@ -34,7 +34,7 @@ public interface NotificationLockscreenUserManager { value = { REDACTION_TYPE_NONE, REDACTION_TYPE_PUBLIC, - REDACTION_TYPE_SENSITIVE_CONTENT}) + REDACTION_TYPE_OTP}) @interface RedactionType {} /** @@ -52,7 +52,7 @@ public interface NotificationLockscreenUserManager { * Indicates that a notification should have its main content redacted, due to detected * sensitive content, such as a One-Time Password */ - int REDACTION_TYPE_SENSITIVE_CONTENT = 1 << 1; + int REDACTION_TYPE_OTP = 1 << 1; /** * @param userId user Id diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index ec04edb0f810..339f898be251 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -795,7 +795,7 @@ public class NotificationLockscreenUserManagerImpl implements } if (shouldShowSensitiveContentRedactedView(ent)) { - return REDACTION_TYPE_SENSITIVE_CONTENT; + return REDACTION_TYPE_OTP; } return REDACTION_TYPE_NONE; } @@ -920,7 +920,7 @@ public class NotificationLockscreenUserManagerImpl implements // notification's "when" time, or the notification entry creation time private long getEarliestNotificationTime(NotificationEntry notif) { long notifWhenWallClock = notif.getSbn().getNotification().getWhen(); - long creationTimeDelta = SystemClock.elapsedRealtime() - notif.getCreationTime(); + long creationTimeDelta = SystemClock.uptimeMillis() - notif.getCreationTime(); long creationTimeWallClock = System.currentTimeMillis() - creationTimeDelta; return Math.min(notifWhenWallClock, creationTimeWallClock); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerExt.kt index 6148b407d3bf..8fc95092be10 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerExt.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerExt.kt @@ -16,7 +16,7 @@ package com.android.systemui.statusbar -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.plugins.statusbar.StatusBarStateController import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt index aeeb0427d24b..e91e9777d48e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt @@ -14,7 +14,7 @@ package com.android.systemui.statusbar.disableflags.data.repository -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.DisplayId 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 3d8ba7f335bd..d031d831bf5a 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 @@ -29,8 +29,6 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICAT import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; -import static com.android.systemui.statusbar.notification.collection.BundleEntry.ROOT_BUNDLES; -import static com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY; import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_NOT_CANCELED; import static java.util.Objects.requireNonNull; @@ -41,7 +39,6 @@ import android.app.Notification; import android.app.Notification.MessagingStyle.Message; import android.app.NotificationChannel; import android.app.NotificationManager.Policy; -import android.app.PendingIntent; import android.app.Person; import android.app.RemoteInput; import android.app.RemoteInputHistoryItem; @@ -68,7 +65,6 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; -import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.headsup.PinnedStatus; import com.android.systemui.statusbar.notification.icon.IconPack; import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; @@ -81,14 +77,14 @@ import com.android.systemui.statusbar.notification.row.shared.NotificationRowCon import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.util.ListenerSet; -import kotlinx.coroutines.flow.MutableStateFlow; -import kotlinx.coroutines.flow.StateFlow; -import kotlinx.coroutines.flow.StateFlowKt; - import java.util.ArrayList; import java.util.List; import java.util.Objects; +import kotlinx.coroutines.flow.MutableStateFlow; +import kotlinx.coroutines.flow.StateFlow; +import kotlinx.coroutines.flow.StateFlowKt; + /** * Represents a notification that the system UI knows about * @@ -497,7 +493,8 @@ public final class NotificationEntry extends ListEntry { public @Nullable List<NotificationEntry> getAttachedNotifChildren() { if (NotificationBundleUi.isEnabled()) { if (isGroupSummary()) { - return ((GroupEntry) getParent()).getChildren(); + GroupEntry parent = (GroupEntry) getParent(); + return parent != null ? new ArrayList<>(parent.getChildren()) : null; } } else { if (row == null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java index 1f32b945ce7e..cda535de86c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.collection.coordinator; +import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS; + import android.annotation.NonNull; import android.annotation.Nullable; @@ -99,7 +101,11 @@ public class RankingCoordinator implements Coordinator { NotificationPriorityBucketKt.BUCKET_ALERTING) { @Override public boolean isInSection(PipelineEntry entry) { - return mHighPriorityProvider.isHighPriority(entry); + return mHighPriorityProvider.isHighPriority(entry) + && entry.getRepresentativeEntry() != null + && entry.getRepresentativeEntry().getChannel() != null + && !SYSTEM_RESERVED_IDS.contains( + entry.getRepresentativeEntry().getChannel().getId()); } @Nullable diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerExt.kt index 6525b6f1186b..376735025abd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerExt.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerExt.kt @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.notification.headsup -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.statusbar.notification.collection.NotificationEntry import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt index d02f636728fc..fe3a856e711e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt @@ -18,7 +18,9 @@ package com.android.systemui.statusbar.notification.row import android.animation.ValueAnimator import android.content.Context +import android.content.res.ColorStateList import android.graphics.Canvas +import android.graphics.Color import android.graphics.ColorFilter import android.graphics.Paint import android.graphics.Path @@ -31,9 +33,27 @@ import android.graphics.RectF import android.graphics.Shader import com.android.systemui.res.R import com.android.wm.shell.shared.animation.Interpolators +import android.graphics.drawable.RippleDrawable +import androidx.core.content.ContextCompat class MagicActionBackgroundDrawable( context: Context, +) : RippleDrawable( + ContextCompat.getColorStateList( + context, + R.color.notification_ripple_untinted_color + ) ?: ColorStateList.valueOf(Color.TRANSPARENT), + createBaseDrawable(context), null +) { + companion object { + private fun createBaseDrawable(context: Context): Drawable { + return BaseBackgroundDrawable(context) + } + } +} + +class BaseBackgroundDrawable( + context: Context, ) : Drawable() { private val cornerRadius = context.resources.getDimension(R.dimen.magic_action_button_corner_radius) @@ -42,6 +62,14 @@ class MagicActionBackgroundDrawable( private val buttonShape = Path() // Color and style + private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + val bgColor = + context.getColor( + com.android.internal.R.color.materialColorSurfaceContainerHigh + ) + color = bgColor + style = Paint.Style.FILL + } private val outlinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { val outlineColor = context.getColor( @@ -91,6 +119,7 @@ class MagicActionBackgroundDrawable( canvas.save() // Draw background canvas.clipPath(buttonShape) + canvas.drawPath(buttonShape, bgPaint) // Apply gradient to outline canvas.drawPath(buttonShape, outlinePaint) updateGradient(boundsF) @@ -119,11 +148,13 @@ class MagicActionBackgroundDrawable( } override fun setAlpha(alpha: Int) { + bgPaint.alpha = alpha outlinePaint.alpha = alpha invalidateSelf() } override fun setColorFilter(colorFilter: ColorFilter?) { + bgPaint.colorFilter = colorFilter outlinePaint.colorFilter = colorFilter invalidateSelf() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 3ffc052b5acc..ff4b835eb3c0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.notification.row; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; -import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_OTP; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; @@ -236,7 +236,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder ); } if (LockscreenOtpRedaction.isSingleLineViewEnabled()) { - if (bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { + if (bindParams.redactionType == REDACTION_TYPE_OTP) { result.mPublicInflatedSingleLineViewModel = SingleLineViewInflater.inflateSingleLineViewModel( entry.getSbn().getNotification(), @@ -469,7 +469,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { logger.logAsyncTaskProgress(row.getLoggingKey(), "creating public remote view"); if (LockscreenOtpRedaction.isEnabled() - && bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { + && bindParams.redactionType == REDACTION_TYPE_OTP) { result.newPublicView = createSensitiveContentMessageNotification( NotificationBundleUi.isEnabled() ? row.getEntryAdapter().getSbn().getNotification() @@ -1355,7 +1355,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder } if (LockscreenOtpRedaction.isSingleLineViewEnabled()) { - if (mBindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { + if (mBindParams.redactionType == REDACTION_TYPE_OTP) { result.mPublicInflatedSingleLineViewModel = SingleLineViewInflater.inflateSingleLineViewModel( mEntry.getSbn().getNotification(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index cd6e4979ce88..b3357d01ab7a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -1247,7 +1247,7 @@ public class NotificationContentView extends FrameLayout implements Notification final boolean isSingleLineViewPresent = mSingleLineView != null; if (shouldShowSingleLineView && !isSingleLineViewPresent) { - Log.wtf(TAG, "calculateVisibleType: SingleLineView is not available!"); + Log.e(TAG, "calculateVisibleType: SingleLineView is not available!"); } final int collapsedVisualType = shouldShowSingleLineView && isSingleLineViewPresent @@ -1274,7 +1274,7 @@ public class NotificationContentView extends FrameLayout implements Notification final boolean shouldShowSingleLineView = mIsChildInGroup && !isGroupExpanded(); final boolean isSingleLinePresent = mSingleLineView != null; if (shouldShowSingleLineView && !isSingleLinePresent) { - Log.wtf(TAG, "getVisualTypeForHeight: singleLineView is not available."); + Log.e(TAG, "getVisualTypeForHeight: singleLineView is not available."); } if (!mUserExpanding && shouldShowSingleLineView && isSingleLinePresent) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index b1c145e08777..2f94d3220dc8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.notification.row import android.annotation.SuppressLint -import android.app.Flags import android.app.Notification import android.app.Notification.EXTRA_SUMMARIZED_CONTENT import android.app.Notification.MessagingStyle @@ -45,7 +44,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.NotifInflation import com.android.systemui.res.R import com.android.systemui.statusbar.InflationTask -import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_OTP import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.notification.ConversationNotificationProcessor import com.android.systemui.statusbar.notification.InflationException @@ -758,7 +757,7 @@ constructor( entry.logKey, "inflating public single line view model", ) - if (bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { + if (bindParams.redactionType == REDACTION_TYPE_OTP) { SingleLineViewInflater.inflateSingleLineViewModel( notification = entry.sbn.notification, messagingStyle = messagingStyle, @@ -876,7 +875,7 @@ constructor( logger.logAsyncTaskProgress(row.loggingKey, "creating public remote view") if ( LockscreenOtpRedaction.isEnabled && - bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT + bindParams.redactionType == REDACTION_TYPE_OTP ) { createSensitiveContentMessageNotification( entry.sbn.notification, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java index 1e249520e8b3..abfb86244390 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java @@ -36,13 +36,14 @@ import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository; +import com.android.systemui.statusbar.notification.headsup.AvalancheController; +import com.android.systemui.statusbar.notification.headsup.NotificationsHunSharedAnimationValues; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.BypassController; import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.SectionProvider; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; -import com.android.systemui.statusbar.notification.headsup.AvalancheController; import java.io.PrintWriter; @@ -424,6 +425,7 @@ public class AmbientState implements Dumpable { /** the bottom-most y position where we can draw pinned HUNs */ public float getHeadsUpBottom() { if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f; + NotificationsHunSharedAnimationValues.assertInLegacyMode(); return mHeadsUpBottom; } 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 d23a4c6307fc..28218227506c 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 @@ -967,9 +967,12 @@ public class StackScrollAlgorithm { childState.setZTranslation(baseZ); } if (isTopEntry && row.isAboveShelf()) { + float headsUpBottom = NotificationsHunSharedAnimationValues.isEnabled() + ? mHeadsUpAnimator.getHeadsUpAppearHeightBottom() + : ambientState.getHeadsUpBottom(); clampHunToMaxTranslation( /* headsUpTop = */ headsUpTranslation, - /* headsUpBottom = */ ambientState.getHeadsUpBottom(), + /* headsUpBottom = */ headsUpBottom, /* viewState = */ childState ); updateCornerRoundnessForPinnedHun(row, ambientState.getStackTop()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManagerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManagerExt.kt index fbc6b9524a6d..372e91f88ae5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManagerExt.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManagerExt.kt @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.phone import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt index f82e681de76f..e2fd4bdc45a9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt @@ -19,7 +19,7 @@ package com.android.systemui.statusbar.pipeline.airplane.data.repository import android.net.ConnectivityManager import android.os.Handler import android.provider.Settings.Global -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.table.TableLogBuffer diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt index 732ea6ac6790..5127e8a14796 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt @@ -101,7 +101,13 @@ fun BatteryCanvas( for (glyph in glyphs) { // Move the glyph to the right spot val verticalOffset = (BatteryFrame.innerHeight - glyph.height) / 2 - inset(horizontalOffset, verticalOffset) { glyph.draw(this, colors) } + inset( + // Never try and inset more than half of the available size - see b/400246091. + minOf(horizontalOffset, size.width / 2), + minOf(verticalOffset, size.height / 2), + ) { + glyph.draw(this, colors) + } horizontalOffset += glyph.width + INTER_GLYPH_PADDING_PX } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairos.kt index 1f5b849c56cc..f4076ae34a4f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairos.kt @@ -24,7 +24,7 @@ import com.android.systemui.Flags import com.android.systemui.KairosActivatable import com.android.systemui.KairosBuilder import com.android.systemui.activated -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.demomode.DemoMode import com.android.systemui.demomode.DemoModeController diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt index 982f6ec36150..2a6ff3b98ae2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt @@ -28,7 +28,7 @@ import android.telephony.satellite.SatelliteModemStateCallback import android.telephony.satellite.SatelliteProvisionStateCallback import androidx.annotation.VisibleForTesting import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt index f9bba9d624f1..eaceb5e22535 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt @@ -26,7 +26,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import com.android.internal.annotations.VisibleForTesting import com.android.systemui.Flags.multiuserWifiPickerTrackerSupport -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt index 0a2bbe580b99..14cadd90db4e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt @@ -14,7 +14,7 @@ package com.android.systemui.statusbar.policy import android.content.res.Configuration -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt index 3cb7090ea6d4..a352982f58f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt @@ -32,6 +32,7 @@ import com.android.systemui.qs.tiles.DndTile import com.android.systemui.qs.tiles.FlashlightTile import com.android.systemui.qs.tiles.LocationTile import com.android.systemui.qs.tiles.MicrophoneToggleTile +import com.android.systemui.qs.tiles.ModesDndTile import com.android.systemui.qs.tiles.ModesTile import com.android.systemui.qs.tiles.UiModeNightTile import com.android.systemui.qs.tiles.WorkModeTile @@ -49,9 +50,13 @@ import com.android.systemui.qs.tiles.impl.location.domain.LocationTileMapper import com.android.systemui.qs.tiles.impl.location.domain.interactor.LocationTileDataInteractor import com.android.systemui.qs.tiles.impl.location.domain.interactor.LocationTileUserActionInteractor import com.android.systemui.qs.tiles.impl.location.domain.model.LocationTileModel +import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesDndTileDataInteractor +import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesDndTileUserActionInteractor import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileDataInteractor import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel +import com.android.systemui.qs.tiles.impl.modes.ui.ModesDndTileMapper import com.android.systemui.qs.tiles.impl.modes.ui.ModesTileMapper import com.android.systemui.qs.tiles.impl.sensorprivacy.SensorPrivacyToggleTileDataInteractor import com.android.systemui.qs.tiles.impl.sensorprivacy.domain.SensorPrivacyToggleTileUserActionInteractor @@ -132,6 +137,7 @@ interface PolicyModule { const val CAMERA_TOGGLE_TILE_SPEC = "cameratoggle" const val MIC_TOGGLE_TILE_SPEC = "mictoggle" const val DND_TILE_SPEC = "dnd" + const val MODES_DND_TILE_SPEC = "modes_dnd" /** Inject DndTile or ModesTile into tileMap in QSModule based on feature flag */ @Provides @@ -146,6 +152,12 @@ interface PolicyModule { return if (ModesUi.isEnabled) modesTile.get() else dndTile.get() } + /** Inject ModesDndTile into tileViewModelMap in QSModule */ + @Provides + @IntoMap + @StringKey(MODES_DND_TILE_SPEC) + fun bindDndModeTile(tile: ModesDndTile): QSTileImpl<*> = tile + /** Inject flashlight config */ @Provides @IntoMap @@ -449,6 +461,37 @@ interface PolicyModule { mapper, ) else StubQSTileViewModel + + @Provides + @IntoMap + @StringKey(MODES_DND_TILE_SPEC) + fun provideDndModeTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(MODES_DND_TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.qs_dnd_icon_off, + labelRes = R.string.quick_settings_dnd_label, + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.CONNECTIVITY, + ) + + @Provides + @IntoMap + @StringKey(MODES_DND_TILE_SPEC) + fun provideDndModeTileViewModel( + factory: QSTileViewModelFactory.Static<ModesDndTileModel>, + mapper: ModesDndTileMapper, + stateInteractor: ModesDndTileDataInteractor, + userActionInteractor: ModesDndTileUserActionInteractor, + ): QSTileViewModel = + factory.create( + TileSpec.create(MODES_DND_TILE_SPEC), + userActionInteractor, + stateInteractor, + mapper, + ) } /** Inject FlashlightTile into tileMap in QSModule */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt index 07bbca74e12e..2b9d39a54c44 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt @@ -15,7 +15,7 @@ */ package com.android.systemui.statusbar.policy.data.repository -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.statusbar.policy.DeviceProvisionedController import dagger.Binds import dagger.Module diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt index e8347df5653f..ed814c6b3785 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt @@ -58,7 +58,7 @@ import kotlinx.coroutines.flow.stateIn * An interactor that performs business logic related to the status and configuration of Zen Mode * (or Do Not Disturb/DND Mode). */ - @SysUISingleton +@SysUISingleton class ZenModeInteractor @Inject constructor( @@ -141,6 +141,18 @@ constructor( return field } + /** + * Returns the current state of the special "manual DND" mode. + * + * This should only be used when there is a strong reason to handle DND specifically (such as + * legacy UI pieces that haven't been updated to use modes more generally, or if the user + * explicitly wants a shortcut to DND). Please prefer using [modes] or [activeModes] in all + * other scenarios. + */ + fun getDndMode(): ZenMode { + return zenModeRepository.getModes().single { it.isManualDnd } + } + /** Flow returning the currently active mode(s), if any. */ val activeModes: Flow<ActiveZenModes> = modes diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt index 2dc17f40a380..c86e00de4246 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.ui.viewmodel -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt index b1b6014bfbde..9b88d439f2db 100644 --- a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt @@ -23,7 +23,7 @@ import android.content.pm.PackageManager import android.telecom.TelecomManager import android.telephony.Annotation import android.telephony.TelephonyCallback -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt index fbbd2b9c5de8..e47d74ec9412 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt @@ -16,7 +16,7 @@ package com.android.systemui.unfold.data.repository import androidx.annotation.FloatRange -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.unfold.UnfoldTransitionProgressProvider import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionInProgress diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index c960b5525d96..05b2e0d1423e 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -33,7 +33,7 @@ import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.statusbar.IStatusBarService import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt index bcbd679b35eb..412161cf98bc 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt @@ -23,7 +23,7 @@ import android.os.UserManager import android.provider.Settings.Global.USER_SWITCHER_ENABLED import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt index 31a8d864de95..9937eeb29151 100644 --- a/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt @@ -19,7 +19,7 @@ import android.content.ContentResolver import android.database.ContentObserver import android.os.Handler import android.provider.Settings -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider.Companion.areAnimationsEnabled import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt index 80ccd646f6be..d4eabb9264e6 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt @@ -16,7 +16,7 @@ package com.android.systemui.util.kotlin -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.statusbar.policy.BatteryController import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt index 7a2f9b24700f..837bbea9cc3c 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt @@ -16,7 +16,7 @@ package com.android.systemui.util.kotlin -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.statusbar.phone.ManagedProfileController import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt index ee00e8b04ef1..02012ede697b 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt @@ -16,7 +16,7 @@ package com.android.systemui.util.kotlin -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.qs.ReduceBrightColorsController import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/RotationLockControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/RotationLockControllerExt.kt index 22cc8dd7745d..a914c86da0e7 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/RotationLockControllerExt.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/RotationLockControllerExt.kt @@ -16,7 +16,7 @@ package com.android.systemui.util.kotlin -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.statusbar.policy.RotationLockController import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt index 594c5526dca9..7e9ebd218787 100644 --- a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt +++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt @@ -24,7 +24,7 @@ import android.service.quickaccesswallet.QuickAccessWalletClient import android.service.quickaccesswallet.WalletCard import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.flags.FeatureFlags diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java index 5c26dac5eb30..798aa428e73e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java @@ -60,13 +60,13 @@ import android.os.RemoteException; import android.os.UserHandle; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.FlagsParameterization; import android.service.notification.StatusBarNotification; import android.testing.TestableLooper; import android.text.TextUtils; import android.view.View; import androidx.core.graphics.drawable.IconCompat; -import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.media.flags.Flags; @@ -101,6 +101,9 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -108,7 +111,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) public class MediaSwitchingControllerTest extends SysuiTestCase { private static final String TEST_DEVICE_1_ID = "test_device_1_id"; @@ -201,6 +204,17 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { private MediaDescription mMediaDescription; private List<RoutingSessionInfo> mRoutingSessionInfos = new ArrayList<>(); + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return FlagsParameterization.allCombinationsOf( + Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION, + Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING); + } + + public MediaSwitchingControllerTest(FlagsParameterization flags) { + mSetFlagsRule.setFlagsParameterization(flags); + } + @Before public void setUp() { mPackageName = mContext.getPackageName(); @@ -260,7 +274,6 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { mMediaDevices.add(mMediaDevice1); mMediaDevices.add(mMediaDevice2); - when(mNearbyDevice1.getMediaRoute2Id()).thenReturn(TEST_DEVICE_1_ID); when(mNearbyDevice1.getRangeZone()).thenReturn(NearbyDevice.RANGE_FAR); when(mNearbyDevice2.getMediaRoute2Id()).thenReturn(TEST_DEVICE_2_ID); @@ -689,7 +702,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { mMediaSwitchingController.start(mCb); reset(mCb); - mMediaSwitchingController.getMediaItemList().clear(); + mMediaSwitchingController.clearMediaItemList(); mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); final List<MediaDevice> devices = new ArrayList<>(); int dividerSize = 0; @@ -1528,7 +1541,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { .getSelectedMediaDevice(); mMediaSwitchingController.start(mCb); reset(mCb); - mMediaSwitchingController.getMediaItemList().clear(); + mMediaSwitchingController.clearMediaItemList(); mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); @@ -1546,7 +1559,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { .getSelectedMediaDevice(); mMediaSwitchingController.start(mCb); reset(mCb); - mMediaSwitchingController.getMediaItemList().clear(); + mMediaSwitchingController.clearMediaItemList(); mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); @@ -1564,7 +1577,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { .getSelectedMediaDevice(); mMediaSwitchingController.start(mCb); reset(mCb); - mMediaSwitchingController.getMediaItemList().clear(); + mMediaSwitchingController.clearMediaItemList(); mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); @@ -1582,7 +1595,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { .getSelectedMediaDevice(); mMediaSwitchingController.start(mCb); reset(mCb); - mMediaSwitchingController.getMediaItemList().clear(); + mMediaSwitchingController.clearMediaItemList(); mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); mMediaDevices.clear(); mMediaDevices.add(mMediaDevice2); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java new file mode 100644 index 000000000000..f6edd49f142f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java @@ -0,0 +1,383 @@ +/* + * 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.media.dialog; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.FlagsParameterization; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.media.flags.Flags; +import com.android.settingslib.media.MediaDevice; +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + +import java.util.List; +import java.util.stream.Collectors; + +@SmallTest +@RunWith(ParameterizedAndroidJunit4.class) +@TestableLooper.RunWithLooper +public class OutputMediaItemListProxyTest extends SysuiTestCase { + private static final String DEVICE_ID_1 = "device_id_1"; + private static final String DEVICE_ID_2 = "device_id_2"; + private static final String DEVICE_ID_3 = "device_id_3"; + private static final String DEVICE_ID_4 = "device_id_4"; + @Mock private MediaDevice mMediaDevice1; + @Mock private MediaDevice mMediaDevice2; + @Mock private MediaDevice mMediaDevice3; + @Mock private MediaDevice mMediaDevice4; + + private MediaItem mMediaItem1; + private MediaItem mMediaItem2; + private MediaItem mConnectNewDeviceMediaItem; + private OutputMediaItemListProxy mOutputMediaItemListProxy; + + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return FlagsParameterization.allCombinationsOf( + Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION, + Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING); + } + + public OutputMediaItemListProxyTest(FlagsParameterization flags) { + mSetFlagsRule.setFlagsParameterization(flags); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + when(mMediaDevice1.getId()).thenReturn(DEVICE_ID_1); + when(mMediaDevice2.getId()).thenReturn(DEVICE_ID_2); + when(mMediaDevice2.isSuggestedDevice()).thenReturn(true); + when(mMediaDevice3.getId()).thenReturn(DEVICE_ID_3); + when(mMediaDevice4.getId()).thenReturn(DEVICE_ID_4); + mMediaItem1 = MediaItem.createDeviceMediaItem(mMediaDevice1); + mMediaItem2 = MediaItem.createDeviceMediaItem(mMediaDevice2); + mConnectNewDeviceMediaItem = MediaItem.createPairNewDeviceMediaItem(); + + mOutputMediaItemListProxy = new OutputMediaItemListProxy(mContext); + } + + @EnableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION) + @Test + public void updateMediaDevices_shouldUpdateMediaItemList() { + assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue(); + + // Create the initial output media item list with mMediaDevice2 and mMediaDevice3. + mOutputMediaItemListProxy.updateMediaDevices( + /* devices= */ List.of(mMediaDevice2, mMediaDevice3), + /* selectedDevices */ List.of(mMediaDevice3), + /* connectedMediaDevice= */ null, + /* needToHandleMutingExpectedDevice= */ false, + /* connectNewDeviceMediaItem= */ null); + + // Check the output media items to be + // * a media item with the selected mMediaDevice3 + // * a group divider for suggested devices + // * a media item with the mMediaDevice2 + assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList())) + .containsExactly(mMediaDevice3, null, mMediaDevice2); + assertThat(mOutputMediaItemListProxy.getOutputMediaItemList().get(0).isFirstDeviceInGroup()) + .isEqualTo(Flags.enableOutputSwitcherDeviceGrouping()); + + // Update the output media item list with more media devices. + mOutputMediaItemListProxy.updateMediaDevices( + /* devices= */ List.of(mMediaDevice4, mMediaDevice1, mMediaDevice3, mMediaDevice2), + /* selectedDevices */ List.of(mMediaDevice3), + /* connectedMediaDevice= */ null, + /* needToHandleMutingExpectedDevice= */ false, + /* connectNewDeviceMediaItem= */ null); + + // Check the output media items to be + // * a media item with the selected route mMediaDevice3 + // * a group divider for suggested devices + // * a media item with the route mMediaDevice2 + // * a group divider for speakers and displays + // * a media item with the route mMediaDevice4 + // * a media item with the route mMediaDevice1 + assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList())) + .containsExactly( + mMediaDevice3, null, mMediaDevice2, null, mMediaDevice4, mMediaDevice1); + assertThat(mOutputMediaItemListProxy.getOutputMediaItemList().get(0).isFirstDeviceInGroup()) + .isEqualTo(Flags.enableOutputSwitcherDeviceGrouping()); + + // Update the output media item list where mMediaDevice4 is offline and new selected device. + mOutputMediaItemListProxy.updateMediaDevices( + /* devices= */ List.of(mMediaDevice1, mMediaDevice3, mMediaDevice2), + /* selectedDevices */ List.of(mMediaDevice1, mMediaDevice3), + /* connectedMediaDevice= */ null, + /* needToHandleMutingExpectedDevice= */ false, + /* connectNewDeviceMediaItem= */ null); + + // Check the output media items to be + // * a media item with the selected route mMediaDevice3 + // * a group divider for suggested devices + // * a media item with the route mMediaDevice2 + // * a group divider for speakers and displays + // * a media item with the route mMediaDevice1 + assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList())) + .containsExactly(mMediaDevice3, null, mMediaDevice2, null, mMediaDevice1); + assertThat(mOutputMediaItemListProxy.getOutputMediaItemList().get(0).isFirstDeviceInGroup()) + .isEqualTo(Flags.enableOutputSwitcherDeviceGrouping()); + } + + @EnableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION) + @Test + public void updateMediaDevices_multipleSelectedDevices_shouldHaveCorrectDeviceOrdering() { + assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue(); + + // Create the initial output media item list with mMediaDevice2 and mMediaDevice3. + mOutputMediaItemListProxy.updateMediaDevices( + /* devices= */ List.of(mMediaDevice2, mMediaDevice4, mMediaDevice3, mMediaDevice1), + /* selectedDevices */ List.of(mMediaDevice1, mMediaDevice2, mMediaDevice3), + /* connectedMediaDevice= */ null, + /* needToHandleMutingExpectedDevice= */ false, + /* connectNewDeviceMediaItem= */ null); + + if (Flags.enableOutputSwitcherDeviceGrouping()) { + // When the device grouping is enabled, the order of selected devices are preserved: + // * a media item with the selected mMediaDevice2 + // * a media item with the selected mMediaDevice3 + // * a media item with the selected mMediaDevice1 + // * a group divider for speakers and displays + // * a media item with the mMediaDevice4 + assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList())) + .containsExactly( + mMediaDevice2, mMediaDevice3, mMediaDevice1, null, mMediaDevice4); + assertThat( + mOutputMediaItemListProxy + .getOutputMediaItemList() + .get(0) + .isFirstDeviceInGroup()) + .isTrue(); + } else { + // When the device grouping is disabled, the order of selected devices are reverted: + // * a media item with the selected mMediaDevice1 + // * a media item with the selected mMediaDevice3 + // * a media item with the selected mMediaDevice2 + // * a group divider for speakers and displays + // * a media item with the mMediaDevice4 + assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList())) + .containsExactly( + mMediaDevice1, mMediaDevice3, mMediaDevice2, null, mMediaDevice4); + } + + // Update the output media item list with a selected device being deselected. + mOutputMediaItemListProxy.updateMediaDevices( + /* devices= */ List.of(mMediaDevice4, mMediaDevice1, mMediaDevice3, mMediaDevice2), + /* selectedDevices */ List.of(mMediaDevice2, mMediaDevice3), + /* connectedMediaDevice= */ null, + /* needToHandleMutingExpectedDevice= */ false, + /* connectNewDeviceMediaItem= */ null); + + if (Flags.enableOutputSwitcherDeviceGrouping()) { + // When the device grouping is enabled, the order of selected devices are preserved: + // * a media item with the selected mMediaDevice2 + // * a media item with the selected mMediaDevice3 + // * a media item with the selected mMediaDevice1 + // * a group divider for speakers and displays + // * a media item with the mMediaDevice4 + assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList())) + .containsExactly( + mMediaDevice2, mMediaDevice3, mMediaDevice1, null, mMediaDevice4); + assertThat( + mOutputMediaItemListProxy + .getOutputMediaItemList() + .get(0) + .isFirstDeviceInGroup()) + .isTrue(); + } else { + // When the device grouping is disabled, the order of selected devices are reverted: + // * a media item with the selected mMediaDevice1 + // * a media item with the selected mMediaDevice3 + // * a media item with the selected mMediaDevice2 + // * a group divider for speakers and displays + // * a media item with the mMediaDevice4 + assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList())) + .containsExactly( + mMediaDevice1, mMediaDevice3, mMediaDevice2, null, mMediaDevice4); + } + + // Update the output media item list with a selected device is missing. + mOutputMediaItemListProxy.updateMediaDevices( + /* devices= */ List.of(mMediaDevice1, mMediaDevice3, mMediaDevice4), + /* selectedDevices */ List.of(mMediaDevice3), + /* connectedMediaDevice= */ null, + /* needToHandleMutingExpectedDevice= */ false, + /* connectNewDeviceMediaItem= */ null); + + if (Flags.enableOutputSwitcherDeviceGrouping()) { + // When the device grouping is enabled, the order of selected devices are preserved: + // * a media item with the selected mMediaDevice3 + // * a media item with the selected mMediaDevice1 + // * a group divider for speakers and displays + // * a media item with the mMediaDevice4 + assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList())) + .containsExactly(mMediaDevice3, mMediaDevice1, null, mMediaDevice4); + assertThat( + mOutputMediaItemListProxy + .getOutputMediaItemList() + .get(0) + .isFirstDeviceInGroup()) + .isTrue(); + } else { + // When the device grouping is disabled, the order of selected devices are reverted: + // * a media item with the selected mMediaDevice1 + // * a media item with the selected mMediaDevice3 + // * a group divider for speakers and displays + // * a media item with the mMediaDevice4 + assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList())) + .containsExactly(mMediaDevice1, mMediaDevice3, null, mMediaDevice4); + } + } + + @EnableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION) + @Test + public void updateMediaDevices_withConnectNewDeviceMediaItem_shouldUpdateMediaItemList() { + assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue(); + + // Create the initial output media item list with a connect new device media item. + mOutputMediaItemListProxy.updateMediaDevices( + /* devices= */ List.of(mMediaDevice2, mMediaDevice3), + /* selectedDevices */ List.of(mMediaDevice3), + /* connectedMediaDevice= */ null, + /* needToHandleMutingExpectedDevice= */ false, + mConnectNewDeviceMediaItem); + + // Check the output media items to be + // * a media item with the selected mMediaDevice3 + // * a group divider for suggested devices + // * a media item with the mMediaDevice2 + // * a connect new device media item + assertThat(mOutputMediaItemListProxy.getOutputMediaItemList()) + .contains(mConnectNewDeviceMediaItem); + assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList())) + .containsExactly(mMediaDevice3, null, mMediaDevice2, null); + + // Update the output media item list without a connect new device media item. + mOutputMediaItemListProxy.updateMediaDevices( + /* devices= */ List.of(mMediaDevice2, mMediaDevice3), + /* selectedDevices */ List.of(mMediaDevice3), + /* connectedMediaDevice= */ null, + /* needToHandleMutingExpectedDevice= */ false, + /* connectNewDeviceMediaItem= */ null); + + // Check the output media items to be + // * a media item with the selected mMediaDevice3 + // * a group divider for suggested devices + // * a media item with the mMediaDevice2 + assertThat(mOutputMediaItemListProxy.getOutputMediaItemList()) + .doesNotContain(mConnectNewDeviceMediaItem); + assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList())) + .containsExactly(mMediaDevice3, null, mMediaDevice2); + } + + @DisableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION) + @Test + public void clearAndAddAll_shouldUpdateMediaItemList() { + assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue(); + + mOutputMediaItemListProxy.clearAndAddAll(List.of(mMediaItem1)); + assertThat(mOutputMediaItemListProxy.getOutputMediaItemList()).containsExactly(mMediaItem1); + assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse(); + + mOutputMediaItemListProxy.clearAndAddAll(List.of(mMediaItem2)); + assertThat(mOutputMediaItemListProxy.getOutputMediaItemList()).containsExactly(mMediaItem2); + assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse(); + } + + @EnableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION) + @Test + public void clear_flagOn_shouldClearMediaItemList() { + assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue(); + + mOutputMediaItemListProxy.updateMediaDevices( + /* devices= */ List.of(mMediaDevice1), + /* selectedDevices */ List.of(), + /* connectedMediaDevice= */ null, + /* needToHandleMutingExpectedDevice= */ false, + /* connectNewDeviceMediaItem= */ null); + assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse(); + + mOutputMediaItemListProxy.clear(); + assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue(); + } + + @DisableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION) + @Test + public void clear_flagOff_shouldClearMediaItemList() { + assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue(); + + mOutputMediaItemListProxy.clearAndAddAll(List.of(mMediaItem1)); + assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse(); + + mOutputMediaItemListProxy.clear(); + assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue(); + } + + @EnableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION) + @Test + public void removeMutingExpectedDevices_flagOn_shouldClearMediaItemList() { + assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue(); + + mOutputMediaItemListProxy.updateMediaDevices( + /* devices= */ List.of(mMediaDevice1), + /* selectedDevices */ List.of(), + /* connectedMediaDevice= */ null, + /* needToHandleMutingExpectedDevice= */ false, + /* connectNewDeviceMediaItem= */ null); + assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse(); + + mOutputMediaItemListProxy.removeMutingExpectedDevices(); + assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse(); + } + + @DisableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION) + @Test + public void removeMutingExpectedDevices_flagOff_shouldClearMediaItemList() { + assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue(); + + mOutputMediaItemListProxy.clearAndAddAll(List.of(mMediaItem1)); + assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse(); + + mOutputMediaItemListProxy.removeMutingExpectedDevices(); + assertThat(mOutputMediaItemListProxy.getOutputMediaItemList()).containsExactly(mMediaItem1); + assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse(); + } + + private List<MediaDevice> getMediaDevices(List<MediaItem> mediaItems) { + return mediaItems.stream() + .map(item -> item.getMediaDevice().orElse(null)) + .collect(Collectors.toList()); + } +} 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 a71224a68125..23166a800245 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -21,6 +21,7 @@ import static android.view.MotionEvent.BUTTON_SECONDARY; import static android.view.accessibility.AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT; import static android.view.accessibility.AccessibilityManager.AUTOCLICK_DELAY_DEFAULT; import static android.view.accessibility.AccessibilityManager.AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT_DEFAULT; +import static android.view.accessibility.AccessibilityManager.AUTOCLICK_REVERT_TO_LEFT_CLICK_DEFAULT; import static com.android.server.accessibility.autoclick.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK; @@ -159,7 +160,8 @@ public class AutoclickController extends BaseEventStreamTransformation { initiateAutoclickIndicator(handler); } - mClickScheduler = new ClickScheduler(handler, AUTOCLICK_DELAY_DEFAULT); + mClickScheduler = new ClickScheduler( + handler, AUTOCLICK_DELAY_DEFAULT); mAutoclickSettingsObserver = new AutoclickSettingsObserver(mUserId, handler); mAutoclickSettingsObserver.start( mContext.getContentResolver(), @@ -304,6 +306,10 @@ public class AutoclickController extends BaseEventStreamTransformation { Settings.Secure.getUriFor( Settings.Secure.ACCESSIBILITY_AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT); + private final Uri mAutoclickRevertToLeftClickSettingUri = + Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_AUTOCLICK_REVERT_TO_LEFT_CLICK); + private ContentResolver mContentResolver; private ClickScheduler mClickScheduler; private AutoclickIndicatorScheduler mAutoclickIndicatorScheduler; @@ -368,6 +374,13 @@ public class AutoclickController extends BaseEventStreamTransformation { /* observer= */ this, mUserId); onChange(/* selfChange= */ true, mAutoclickIgnoreMinorCursorMovementSettingUri); + + mContentResolver.registerContentObserver( + mAutoclickRevertToLeftClickSettingUri, + /* notifyForDescendants= */ false, + /* observer= */ this, + mUserId); + onChange(/* selfChange= */ true, mAutoclickRevertToLeftClickSettingUri); } } @@ -424,6 +437,20 @@ public class AutoclickController extends BaseEventStreamTransformation { == AccessibilityUtils.State.ON; mClickScheduler.setIgnoreMinorCursorMovement(ignoreMinorCursorMovement); } + + if (mAutoclickRevertToLeftClickSettingUri.equals(uri)) { + boolean revertToLeftClick = + Settings.Secure.getIntForUser( + mContentResolver, + Settings.Secure + .ACCESSIBILITY_AUTOCLICK_REVERT_TO_LEFT_CLICK, + AUTOCLICK_REVERT_TO_LEFT_CLICK_DEFAULT + ? AccessibilityUtils.State.ON + : AccessibilityUtils.State.OFF, + mUserId) + == AccessibilityUtils.State.ON; + mClickScheduler.setRevertToLeftClick(revertToLeftClick); + } } } } @@ -505,6 +532,9 @@ public class AutoclickController extends BaseEventStreamTransformation { /** Whether the minor cursor movement should be ignored. */ private boolean mIgnoreMinorCursorMovement = AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT_DEFAULT; + /** Whether the autoclick type reverts to left click once performing an action. */ + private boolean mRevertToLeftClick = AUTOCLICK_REVERT_TO_LEFT_CLICK_DEFAULT; + /** Whether there is pending click. */ private boolean mActive; /** If active, time at which pending click is scheduled. */ @@ -555,6 +585,7 @@ public class AutoclickController extends BaseEventStreamTransformation { sendClick(); resetInternalState(); + resetSelectedClickTypeIfNecessary(); } /** @@ -633,6 +664,11 @@ public class AutoclickController extends BaseEventStreamTransformation { return mDelay; } + @VisibleForTesting + boolean getRevertToLeftClickForTesting() { + return mRevertToLeftClick; + } + /** * Updates the time at which click sequence should occur. * @@ -692,6 +728,12 @@ public class AutoclickController extends BaseEventStreamTransformation { } } + private void resetSelectedClickTypeIfNecessary() { + if (mRevertToLeftClick && mActiveClickType != AUTOCLICK_TYPE_LEFT_CLICK) { + mAutoclickTypePanel.resetSelectedClickType(); + } + } + /** * @param event Observed motion event. * @return Whether the event coords are far enough from the anchor for the event not to be @@ -716,6 +758,10 @@ public class AutoclickController extends BaseEventStreamTransformation { mIgnoreMinorCursorMovement = ignoreMinorCursorMovement; } + public void setRevertToLeftClick(boolean revertToLeftClick) { + mRevertToLeftClick = revertToLeftClick; + } + private void updateMovementSlop(double slop) { mMovementSlop = slop; } 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 57fa77d73729..5a484d42eb96 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java @@ -283,7 +283,11 @@ public class AutoclickTypePanel { // The pause button calls `togglePause()` directly so it does not need extra logic. mPauseButton.setOnClickListener(v -> togglePause()); - // Initializes panel as collapsed state and only displays the left click button. + resetSelectedClickType(); + } + + /** Reset panel as collapsed state and only displays the left click button. */ + public void resetSelectedClickType() { hideAllClickTypeButtons(); mLeftClickButton.setVisibility(View.VISIBLE); setSelectedClickType(AUTOCLICK_TYPE_LEFT_CLICK); diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java index 81d34f67dee9..fe4fa1068bb3 100644 --- a/services/core/java/com/android/server/am/BroadcastConstants.java +++ b/services/core/java/com/android/server/am/BroadcastConstants.java @@ -41,6 +41,7 @@ import dalvik.annotation.optimization.NeverCompile; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.TimeUnit; /** * Tunable parameters for broadcast dispatch policy @@ -286,6 +287,17 @@ public class BroadcastConstants { "max_frozen_outgoing_broadcasts"; private static final int DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS = 32; + /** + * For {@link BroadcastQueueImpl}: Indicates how long after a process start was initiated, + * it should be considered abandoned and discarded. + */ + public long PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS = + DEFAULT_PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS * Build.HW_TIMEOUT_MULTIPLIER; + private static final String KEY_PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS = + "pending_cold_start_abandon_timeout_millis"; + private static final long DEFAULT_PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS = + TimeUnit.MINUTES.toMillis(5); + // Settings override tracking for this instance private String mSettingsKey; private SettingsObserver mSettingsObserver; @@ -434,6 +446,10 @@ public class BroadcastConstants { MAX_FROZEN_OUTGOING_BROADCASTS = getDeviceConfigInt( KEY_MAX_FROZEN_OUTGOING_BROADCASTS, DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS); + PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS = getDeviceConfigLong( + KEY_PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS, + DEFAULT_PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS) + * Build.HW_TIMEOUT_MULTIPLIER; } // TODO: migrate BroadcastRecord to accept a BroadcastConstants @@ -491,6 +507,8 @@ public class BroadcastConstants { PENDING_COLD_START_CHECK_INTERVAL_MILLIS).println(); pw.print(KEY_MAX_FROZEN_OUTGOING_BROADCASTS, MAX_FROZEN_OUTGOING_BROADCASTS).println(); + pw.print(KEY_PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS, + PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS).println(); pw.decreaseIndent(); pw.println(); } diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index 508c01802156..c0fe73877c01 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -245,6 +245,24 @@ class BroadcastProcessQueue { */ private final ArrayList<BroadcastRecord> mOutgoingBroadcasts = new ArrayList<>(); + /** + * The timestamp, in {@link SystemClock#uptimeMillis()}, at which a cold start was initiated + * for the process associated with this queue. + * + * Note: We could use the already existing {@link ProcessRecord#getStartUptime()} instead + * of this, but the need for this timestamp is to identify an issue (b/393898613) where the + * suspicion is that process is not attached or getting changed. So, we don't want to rely on + * ProcessRecord directly for this purpose. + */ + private long mProcessStartInitiatedTimestampMillis; + + /** + * Indicates whether the number of current receivers has been incremented using + * {@link ProcessReceiverRecord#incrementCurReceivers()}. This allows to skip decrementing + * the receivers when it is not required. + */ + private boolean mCurReceiversIncremented; + public BroadcastProcessQueue(@NonNull BroadcastConstants constants, @NonNull String processName, int uid) { this.constants = Objects.requireNonNull(constants); @@ -652,6 +670,52 @@ class BroadcastProcessQueue { return mActiveFirstLaunch; } + public void incrementCurAppReceivers() { + app.mReceivers.incrementCurReceivers(); + mCurReceiversIncremented = true; + } + + public void decrementCurAppReceivers() { + if (mCurReceiversIncremented) { + app.mReceivers.decrementCurReceivers(); + mCurReceiversIncremented = false; + } + } + + public void setProcessStartInitiatedTimestampMillis(@UptimeMillisLong long timestampMillis) { + mProcessStartInitiatedTimestampMillis = timestampMillis; + } + + @UptimeMillisLong + public long getProcessStartInitiatedTimestampMillis() { + return mProcessStartInitiatedTimestampMillis; + } + + public boolean hasProcessStartInitiationTimedout() { + if (mProcessStartInitiatedTimestampMillis <= 0) { + return false; + } + return (SystemClock.uptimeMillis() - mProcessStartInitiatedTimestampMillis) + > constants.PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS; + } + + /** + * Returns if the process start initiation is expected to be timed out at this point. This + * allows us to dump necessary state for debugging before the process start is timed out + * and discarded. + */ + public boolean isProcessStartInitiationTimeoutExpected() { + if (mProcessStartInitiatedTimestampMillis <= 0) { + return false; + } + return (SystemClock.uptimeMillis() - mProcessStartInitiatedTimestampMillis) + > constants.PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS / 2; + } + + public void clearProcessStartInitiatedTimestampMillis() { + mProcessStartInitiatedTimestampMillis = 0; + } + /** * Get package name of the first application loaded into this process. */ @@ -1558,6 +1622,10 @@ class BroadcastProcessQueue { if (mActiveReEnqueued) { pw.print("activeReEnqueued:"); pw.println(mActiveReEnqueued); } + if (mProcessStartInitiatedTimestampMillis > 0) { + pw.print("processStartInitiatedTimestamp:"); pw.println( + TimeUtils.formatUptime(mProcessStartInitiatedTimestampMillis)); + } } @NeverCompile diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java index d276b9a94791..6e893ad0a425 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java @@ -534,6 +534,7 @@ class BroadcastQueueImpl extends BroadcastQueue { // skip to look for another warm process if (mRunningColdStart == null) { mRunningColdStart = queue; + mRunningColdStart.clearProcessStartInitiatedTimestampMillis(); } else if (isPendingColdStartValid()) { // Move to considering next runnable queue queue = nextQueue; @@ -542,6 +543,7 @@ class BroadcastQueueImpl extends BroadcastQueue { // Pending cold start is not valid, so clear it and move on. clearInvalidPendingColdStart(); mRunningColdStart = queue; + mRunningColdStart.clearProcessStartInitiatedTimestampMillis(); } } @@ -588,7 +590,9 @@ class BroadcastQueueImpl extends BroadcastQueue { @GuardedBy("mService") private boolean isPendingColdStartValid() { - if (mRunningColdStart.app.getPid() > 0) { + if (mRunningColdStart.hasProcessStartInitiationTimedout()) { + return false; + } else if (mRunningColdStart.app.getPid() > 0) { // If the process has already started, check if it wasn't killed. return !mRunningColdStart.app.isKilled(); } else { @@ -673,6 +677,7 @@ class BroadcastQueueImpl extends BroadcastQueue { if ((mRunningColdStart != null) && (mRunningColdStart == queue)) { // We've been waiting for this app to cold start, and it's ready // now; dispatch its next broadcast and clear the slot + mRunningColdStart.clearProcessStartInitiatedTimestampMillis(); mRunningColdStart = null; // Now that we're running warm, we can finally request that OOM @@ -756,6 +761,7 @@ class BroadcastQueueImpl extends BroadcastQueue { // We've been waiting for this app to cold start, and it had // trouble; clear the slot and fail delivery below + mRunningColdStart.clearProcessStartInitiatedTimestampMillis(); mRunningColdStart = null; // We might be willing to kick off another cold start @@ -1036,6 +1042,7 @@ class BroadcastQueueImpl extends BroadcastQueue { "startProcessLocked failed"); return true; } + queue.setProcessStartInitiatedTimestampMillis(SystemClock.uptimeMillis()); // TODO: b/335420031 - cache receiver intent to avoid multiple calls to getReceiverIntent. mService.mProcessList.getAppStartInfoTracker().handleProcessBroadcastStart( startTimeNs, queue.app, r.getReceiverIntent(receiver), r.alarm /* isAlarm */); @@ -1991,6 +1998,32 @@ class BroadcastQueueImpl extends BroadcastQueue { if (mRunningColdStart != null) { checkState(getRunningIndexOf(mRunningColdStart) >= 0, "isOrphaned " + mRunningColdStart); + + final BroadcastProcessQueue queue = getProcessQueue(mRunningColdStart.processName, + mRunningColdStart.uid); + checkState(queue == mRunningColdStart, "Conflicting " + mRunningColdStart + + " with queue " + queue + + ";\n mRunningColdStart.app: " + mRunningColdStart.app.toDetailedString() + + ";\n queue.app: " + queue.app.toDetailedString()); + + checkState(mRunningColdStart.app != null, "Empty cold start queue " + + mRunningColdStart); + + if (mRunningColdStart.isProcessStartInitiationTimeoutExpected()) { + final StringBuilder sb = new StringBuilder(); + sb.append("Process start timeout expected for app "); + sb.append(mRunningColdStart.app); + sb.append(" in queue "); + sb.append(mRunningColdStart); + sb.append("; startUpTime: "); + final long startupTimeMs = + mRunningColdStart.getProcessStartInitiatedTimestampMillis(); + sb.append(startupTimeMs == 0 ? "<none>" + : TimeUtils.formatDuration(startupTimeMs - SystemClock.uptimeMillis())); + sb.append(";\n app: "); + sb.append(mRunningColdStart.app.toDetailedString()); + checkState(false, sb.toString()); + } } // Verify health of all known process queues @@ -2090,7 +2123,7 @@ class BroadcastQueueImpl extends BroadcastQueue { @GuardedBy("mService") private void notifyStartedRunning(@NonNull BroadcastProcessQueue queue) { if (queue.app != null) { - queue.app.mReceivers.incrementCurReceivers(); + queue.incrementCurAppReceivers(); // Don't bump its LRU position if it's in the background restricted. if (mService.mInternal.getRestrictionLevel( @@ -2115,7 +2148,7 @@ class BroadcastQueueImpl extends BroadcastQueue { @GuardedBy("mService") private void notifyStoppedRunning(@NonNull BroadcastProcessQueue queue) { if (queue.app != null) { - queue.app.mReceivers.decrementCurReceivers(); + queue.decrementCurAppReceivers(); if (queue.runningOomAdjusted) { mService.enqueueOomAdjTargetLocked(queue.app); diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index eea667ef2f39..400c699bf93f 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -70,6 +70,7 @@ import com.android.server.wm.WindowProcessController; import com.android.server.wm.WindowProcessListener; import java.io.PrintWriter; +import java.io.StringWriter; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; @@ -1414,6 +1415,16 @@ class ProcessRecord implements WindowProcessListener { return mStringName = sb.toString(); } + String toDetailedString() { + final StringBuilder sb = new StringBuilder(); + sb.append(this); + final StringWriter sw = new StringWriter(); + final PrintWriter pw = new PrintWriter(sw); + dump(pw, " "); + sb.append(sw); + return sb.toString(); + } + /* * Return true if package has been added false if not */ diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index cfd22fbdeece..cb4342f27bc8 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -253,11 +253,12 @@ public class SettingsToPropertiesMapper { "pixel_state_server", "pixel_system_sw_video", "pixel_video_sw", + "pixel_vpn", "pixel_watch", + "pixel_watch_debug_trace", "pixel_wifi", "platform_compat", "platform_security", - "pixel_watch_debug_trace", "pmw", "power", "preload_safety", diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java index 0e1fbf3a6d1a..f50f45a182d0 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java @@ -118,7 +118,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { @Override void shutdown() { mSqliteWriteHandler.removeAllPendingMessages(); - mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear()); + mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.evictAllAppOpEvents()); } @Override @@ -172,10 +172,14 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { @Nullable String[] opNamesFilter, @Nullable String attributionTagFilter, int opFlagsFilter, Set<String> attributionExemptPkgs) { + IntArray opCodes = getAppOpCodes(filter, opNamesFilter); // flush the cache into database before read. - mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear()); + if (opCodes != null) { + mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.evictAppOpEvents(opCodes)); + } else { + mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.evictAllAppOpEvents()); + } boolean assembleChains = attributionExemptPkgs != null; - IntArray opCodes = getAppOpCodes(filter, opNamesFilter); beginTimeMillis = Math.max(beginTimeMillis, Instant.now().minus(sDiscreteHistoryCutoff, ChronoUnit.MILLIS).toEpochMilli()); List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter, @@ -214,7 +218,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix, int nDiscreteOps) { // flush the cache into database before dump. - mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear()); + mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.evictAllAppOpEvents()); IntArray opCodes = new IntArray(); if (dumpOp != AppOpsManager.OP_NONE) { opCodes.add(dumpOp); @@ -366,7 +370,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { try { List<DiscreteOp> evictedEvents; synchronized (mDiscreteOpCache) { - evictedEvents = mDiscreteOpCache.evict(); + evictedEvents = mDiscreteOpCache.evictOldAppOpEvents(); } mDiscreteOpsDbHelper.insertDiscreteOps(evictedEvents); } finally { @@ -389,7 +393,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { try { List<DiscreteOp> evictedEvents; synchronized (mDiscreteOpCache) { - evictedEvents = mDiscreteOpCache.evict(); + evictedEvents = mDiscreteOpCache.evictOldAppOpEvents(); // if nothing to evict, just write the whole cache to database. if (evictedEvents.isEmpty() && mDiscreteOpCache.size() >= mDiscreteOpCache.capacity()) { @@ -451,9 +455,10 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { } /** - * Evict entries older than {@link DiscreteOpsRegistry#sDiscreteHistoryQuantization}. + * Evict entries older than {@link DiscreteOpsRegistry#sDiscreteHistoryQuantization} i.e. + * app op events older than one minute (default quantization) will be evicted. */ - private List<DiscreteOp> evict() { + private List<DiscreteOp> evictOldAppOpEvents() { synchronized (this) { List<DiscreteOp> evictedEvents = new ArrayList<>(); Set<DiscreteOp> snapshot = new ArraySet<>(mCache); @@ -470,11 +475,9 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { } /** - * Remove all the entries from cache. - * - * @return return all removed entries. + * Evict all app op entries from cache, and return the list of removed ops. */ - public List<DiscreteOp> getAllEventsAndClear() { + public List<DiscreteOp> evictAllAppOpEvents() { synchronized (this) { List<DiscreteOp> cachedOps = new ArrayList<>(mCache.size()); if (mCache.isEmpty()) { @@ -486,6 +489,25 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { } } + /** + * Evict specified app ops from cache, and return the list of evicted ops. + */ + public List<DiscreteOp> evictAppOpEvents(IntArray ops) { + synchronized (this) { + List<DiscreteOp> evictedOps = new ArrayList<>(); + if (mCache.isEmpty()) { + return evictedOps; + } + for (DiscreteOp discreteOp: mCache) { + if (ops.contains(discreteOp.getOpCode())) { + evictedOps.add(discreteOp); + } + } + evictedOps.forEach(mCache::remove); + return evictedOps; + } + } + int size() { return mCache.size(); } @@ -646,7 +668,10 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { + ", uidState=" + getUidStateName(mUidState) + ", chainId=" + mChainId + ", accessTime=" + mAccessTime - + ", duration=" + mDuration + '}'; + + ", mDiscretizedAccessTime=" + mDiscretizedAccessTime + + ", duration=" + mDuration + + ", mDiscretizedDuration=" + mDiscretizedDuration + + '}'; } public int getUid() { diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index cf598e89c988..62264dd73795 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -96,6 +96,7 @@ import android.os.ServiceManager; import android.os.ServiceSpecificException; import android.os.ShellCommand; import android.os.SystemClock; +import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; @@ -1564,6 +1565,12 @@ class PackageManagerShellCommand extends ShellCommand { private int doRunInstall(final InstallParams params) throws RemoteException { final PrintWriter pw = getOutPrintWriter(); + // Do not allow app installation if boot has not completed already + if (!SystemProperties.getBoolean("sys.boot_completed", false)) { + pw.println("Error: device is still booting."); + return 1; + } + int requestUserId = params.userId; if (requestUserId != UserHandle.USER_ALL && requestUserId != UserHandle.USER_CURRENT) { UserManagerInternal umi = @@ -2174,6 +2181,13 @@ class PackageManagerShellCommand extends ShellCommand { private int runUninstall() throws RemoteException { final PrintWriter pw = getOutPrintWriter(); + + // Do not allow app uninstallation if boot has not completed already + if (!SystemProperties.getBoolean("sys.boot_completed", false)) { + pw.println("Error: device is still booting."); + return 1; + } + int flags = 0; int userId = UserHandle.USER_ALL; long versionCode = PackageManager.VERSION_CODE_HIGHEST; diff --git a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java index 799157520ca5..800fc7c25de5 100644 --- a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java +++ b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java @@ -23,6 +23,7 @@ import android.content.ComponentName; import android.content.Context; import android.net.NetworkCapabilities; import android.net.NetworkRequest; +import android.os.Binder; import android.os.Environment; import android.util.AtomicFile; import android.util.Slog; @@ -119,7 +120,7 @@ class CertificateRevocationStatusManager { } catch (IOException | JSONException ex) { Slog.d(TAG, "Fallback to check stored revocation status", ex); if (ex instanceof IOException && mShouldScheduleJob) { - scheduleJobToFetchRemoteRevocationJob(); + Binder.withCleanCallingIdentity(this::scheduleJobToFetchRemoteRevocationJob); } try { revocationList = getStoredRevocationList(); @@ -210,7 +211,7 @@ class CertificateRevocationStatusManager { return; } Slog.d(TAG, "Scheduling job to fetch remote CRL."); - jobScheduler.schedule( + jobScheduler.forNamespace(TAG).schedule( new JobInfo.Builder( JOB_ID, new ComponentName( diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index e91d88901751..919b14b6db62 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -625,7 +625,7 @@ final class ActivityRecord extends WindowToken { @VisibleForTesting final TaskFragment.ConfigOverrideHint mResolveConfigHint; - private final boolean mOptOutEdgeToEdge; + final boolean mOptOutEdgeToEdge; private static ConstrainDisplayApisConfig sConstrainDisplayApisConfig; diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java index b91a12598e01..80df081a6271 100644 --- a/services/core/java/com/android/server/wm/AppCompatUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatUtils.java @@ -216,6 +216,7 @@ final class AppCompatUtils { AppCompatCameraPolicy.getCameraCompatFreeformMode(top); appCompatTaskInfo.setHasMinAspectRatioOverride(top.mAppCompatController .getDesktopAspectRatioPolicy().hasMinAspectRatioOverride(task)); + appCompatTaskInfo.setOptOutEdgeToEdge(top.mOptOutEdgeToEdge); } /** diff --git a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java index 7a959c14fbd2..ce3ad889a308 100644 --- a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java +++ b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java @@ -24,6 +24,8 @@ import static android.content.pm.ActivityInfo.isFixedOrientationPortrait; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static com.android.internal.policy.SystemBarUtils.getDesktopViewAppHeaderHeightPx; +import static com.android.internal.policy.DesktopModeCompatUtils.shouldExcludeCaptionFromAppBounds; import static com.android.server.wm.LaunchParamsUtil.applyLayoutGravity; import static com.android.server.wm.LaunchParamsUtil.calculateLayoutBounds; @@ -64,7 +66,8 @@ public final class DesktopModeBoundsCalculator { */ static void updateInitialBounds(@NonNull Task task, @Nullable WindowLayout layout, @Nullable ActivityRecord activity, @Nullable ActivityOptions options, - @NonNull Rect outBounds, @NonNull Consumer<String> logger) { + @NonNull LaunchParamsController.LaunchParams outParams, + @NonNull Consumer<String> logger) { // Use stable frame instead of raw frame to avoid launching freeform windows on top of // stable insets, which usually are system widgets such as sysbar & navbar. final Rect stableBounds = new Rect(); @@ -77,36 +80,44 @@ public final class DesktopModeBoundsCalculator { // during the size update. final boolean shouldRespectOptionPosition = updateOptionBoundsSize && DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue(); + final int captionHeight = activity != null && shouldExcludeCaptionFromAppBounds( + activity.info, task.isResizeable(), activity.mOptOutEdgeToEdge) + ? getDesktopViewAppHeaderHeightPx(activity.mWmService.mContext) : 0; if (options != null && options.getLaunchBounds() != null && !updateOptionBoundsSize) { - outBounds.set(options.getLaunchBounds()); - logger.accept("inherit-from-options=" + outBounds); + outParams.mBounds.set(options.getLaunchBounds()); + logger.accept("inherit-from-options=" + outParams.mBounds); } else if (layout != null) { final int verticalGravity = layout.gravity & Gravity.VERTICAL_GRAVITY_MASK; final int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; if (layout.hasSpecifiedSize()) { - calculateLayoutBounds(stableBounds, layout, outBounds, + calculateLayoutBounds(stableBounds, layout, outParams.mBounds, calculateIdealSize(stableBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE)); - applyLayoutGravity(verticalGravity, horizontalGravity, outBounds, + applyLayoutGravity(verticalGravity, horizontalGravity, outParams.mBounds, stableBounds); logger.accept("layout specifies sizes, inheriting size and applying gravity"); } else if (verticalGravity > 0 || horizontalGravity > 0) { - outBounds.set(calculateInitialBounds(task, activity, stableBounds, options, - shouldRespectOptionPosition)); - applyLayoutGravity(verticalGravity, horizontalGravity, outBounds, + outParams.mBounds.set(calculateInitialBounds(task, activity, stableBounds, options, + shouldRespectOptionPosition, captionHeight)); + applyLayoutGravity(verticalGravity, horizontalGravity, outParams.mBounds, stableBounds); logger.accept("layout specifies gravity, applying desired bounds and gravity"); logger.accept("respecting option bounds cascaded position=" + shouldRespectOptionPosition); } } else { - outBounds.set(calculateInitialBounds(task, activity, stableBounds, options, - shouldRespectOptionPosition)); + outParams.mBounds.set(calculateInitialBounds(task, activity, stableBounds, options, + shouldRespectOptionPosition, captionHeight)); logger.accept("layout not specified, applying desired bounds"); logger.accept("respecting option bounds cascaded position=" + shouldRespectOptionPosition); } + if (updateOptionBoundsSize && captionHeight != 0) { + outParams.mAppBounds.set(outParams.mBounds); + outParams.mAppBounds.top += captionHeight; + logger.accept("excluding caption height from app bounds"); + } } /** @@ -119,7 +130,8 @@ public final class DesktopModeBoundsCalculator { @NonNull private static Rect calculateInitialBounds(@NonNull Task task, @NonNull ActivityRecord activity, @NonNull Rect stableBounds, - @Nullable ActivityOptions options, boolean shouldRespectOptionPosition + @Nullable ActivityOptions options, boolean shouldRespectOptionPosition, + int captionHeight ) { // Display bounds not taking into account insets. final TaskDisplayArea displayArea = task.getDisplayArea(); @@ -160,7 +172,8 @@ public final class DesktopModeBoundsCalculator { } // If activity is unresizeable, regardless of orientation, calculate maximum size // (within the ideal size) maintaining original aspect ratio. - yield maximizeSizeGivenAspectRatio(activityOrientation, idealSize, appAspectRatio); + yield maximizeSizeGivenAspectRatio(activityOrientation, idealSize, appAspectRatio, + captionHeight); } case ORIENTATION_PORTRAIT -> { // Device in portrait orientation. @@ -188,11 +201,12 @@ public final class DesktopModeBoundsCalculator { // ratio. yield maximizeSizeGivenAspectRatio(activityOrientation, new Size(customPortraitWidthForLandscapeApp, idealSize.getHeight()), - appAspectRatio); + appAspectRatio, captionHeight); } // For portrait unresizeable activities, calculate maximum size (within the ideal // size) maintaining original aspect ratio. - yield maximizeSizeGivenAspectRatio(activityOrientation, idealSize, appAspectRatio); + yield maximizeSizeGivenAspectRatio(activityOrientation, idealSize, appAspectRatio, + captionHeight); } default -> idealSize; }; @@ -232,13 +246,15 @@ public final class DesktopModeBoundsCalculator { * Calculates the largest size that can fit in a given area while maintaining a specific aspect * ratio. */ + // TODO(b/400617906): Merge duplicate initial bounds calculations to shared class. @NonNull private static Size maximizeSizeGivenAspectRatio( @ScreenOrientation int orientation, @NonNull Size targetArea, - float aspectRatio + float aspectRatio, + int captionHeight ) { - final int targetHeight = targetArea.getHeight(); + final int targetHeight = targetArea.getHeight() - captionHeight; final int targetWidth = targetArea.getWidth(); final int finalHeight; final int finalWidth; @@ -275,7 +291,7 @@ public final class DesktopModeBoundsCalculator { finalHeight = (int) (finalWidth / aspectRatio); } } - return new Size(finalWidth, finalHeight); + return new Size(finalWidth, finalHeight + captionHeight); } /** diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java index ddcb5eccb1d8..6698d2ec7cc4 100644 --- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java @@ -150,7 +150,7 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { } DesktopModeBoundsCalculator.updateInitialBounds(task, layout, activity, options, - outParams.mBounds, this::appendLog); + outParams, this::appendLog); appendLog("final desktop mode task bounds set to %s", outParams.mBounds); if (options != null && options.getFlexibleLaunchSize()) { // Return result done to prevent other modifiers from respecting option bounds and diff --git a/services/core/java/com/android/server/wm/LaunchParamsController.java b/services/core/java/com/android/server/wm/LaunchParamsController.java index fa65bda7104d..8eec98cb5fcc 100644 --- a/services/core/java/com/android/server/wm/LaunchParamsController.java +++ b/services/core/java/com/android/server/wm/LaunchParamsController.java @@ -26,6 +26,7 @@ import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier. import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP; import android.annotation.IntDef; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityOptions; import android.app.WindowConfiguration.WindowingMode; @@ -138,6 +139,10 @@ class LaunchParamsController { mService.deferWindowLayout(); try { if (task.getRootTask().inMultiWindowMode()) { + if (!mTmpParams.mAppBounds.isEmpty()) { + task.getRequestedOverrideConfiguration().windowConfiguration.setAppBounds( + mTmpParams.mAppBounds); + } task.setBounds(mTmpParams.mBounds); return true; } @@ -169,6 +174,9 @@ class LaunchParamsController { static class LaunchParams { /** The bounds within the parent container. */ final Rect mBounds = new Rect(); + /** The bounds within the parent container respecting insets. Usually empty. */ + @NonNull + final Rect mAppBounds = new Rect(); /** The display area the {@link Task} would prefer to be on. */ @Nullable @@ -181,6 +189,7 @@ class LaunchParamsController { /** Sets values back to default. {@link #isEmpty} will return {@code true} once called. */ void reset() { mBounds.setEmpty(); + mAppBounds.setEmpty(); mPreferredTaskDisplayArea = null; mWindowingMode = WINDOWING_MODE_UNDEFINED; } @@ -188,13 +197,14 @@ class LaunchParamsController { /** Copies the values set on the passed in {@link LaunchParams}. */ void set(LaunchParams params) { mBounds.set(params.mBounds); + mAppBounds.set(params.mAppBounds); mPreferredTaskDisplayArea = params.mPreferredTaskDisplayArea; mWindowingMode = params.mWindowingMode; } /** Returns {@code true} if no values have been explicitly set. */ boolean isEmpty() { - return mBounds.isEmpty() && mPreferredTaskDisplayArea == null + return mBounds.isEmpty() && mAppBounds.isEmpty() && mPreferredTaskDisplayArea == null && mWindowingMode == WINDOWING_MODE_UNDEFINED; } @@ -215,12 +225,14 @@ class LaunchParamsController { if (mPreferredTaskDisplayArea != that.mPreferredTaskDisplayArea) return false; if (mWindowingMode != that.mWindowingMode) return false; + if (!mAppBounds.equals(that.mAppBounds)) return false; return mBounds != null ? mBounds.equals(that.mBounds) : that.mBounds == null; } @Override public int hashCode() { int result = mBounds != null ? mBounds.hashCode() : 0; + result = 31 * result + mAppBounds.hashCode(); result = 31 * result + (mPreferredTaskDisplayArea != null ? mPreferredTaskDisplayArea.hashCode() : 0); result = 31 * result + mWindowingMode; diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index ce91fc5baba1..d528776a2c25 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -100,7 +100,6 @@ 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; -import static com.android.input.flags.Flags.removeInputChannelFromWindowstate; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ADD_REMOVE; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS; @@ -613,10 +612,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // Input channel and input window handle used by the input dispatcher. final InputWindowHandleWrapper mInputWindowHandle; - /** - * Only populated if flag REMOVE_INPUT_CHANNEL_FROM_WINDOWSTATE is disabled. - */ - private InputChannel mInputChannel; /** * The token will be assigned to {@link InputWindowHandle#token} if this window can receive @@ -1830,12 +1825,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP * Input Manager uses when discarding windows from input consideration. */ boolean isPotentialDragTarget(boolean targetInterceptsGlobalDrag) { - if (removeInputChannelFromWindowstate()) { - return (targetInterceptsGlobalDrag || isVisibleNow()) && !mRemoved - && mInputChannelToken != null && mInputWindowHandle != null; - } return (targetInterceptsGlobalDrag || isVisibleNow()) && !mRemoved - && mInputChannel != null && mInputWindowHandle != null; + && mInputChannelToken != null && mInputWindowHandle != null; } /** @@ -2583,25 +2574,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (mInputChannelToken != null) { throw new IllegalStateException("Window already has an input channel token."); } - if (removeInputChannelFromWindowstate()) { - String name = getName(); - InputChannel channel = mWmService.mInputManager.createInputChannel(name); - mInputChannelToken = channel.getToken(); - mInputWindowHandle.setToken(mInputChannelToken); - mWmService.mInputToWindowMap.put(mInputChannelToken, this); - channel.copyTo(outInputChannel); - channel.dispose(); - return; - } - if (mInputChannel != null) { - throw new IllegalStateException("Window already has an input channel."); - } String name = getName(); - mInputChannel = mWmService.mInputManager.createInputChannel(name); - mInputChannelToken = mInputChannel.getToken(); + InputChannel channel = mWmService.mInputManager.createInputChannel(name); + mInputChannelToken = channel.getToken(); mInputWindowHandle.setToken(mInputChannelToken); mWmService.mInputToWindowMap.put(mInputChannelToken, this); - mInputChannel.copyTo(outInputChannel); + channel.copyTo(outInputChannel); + channel.dispose(); } /** @@ -2624,12 +2603,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mInputChannelToken = null; } - if (!removeInputChannelFromWindowstate()) { - if (mInputChannel != null) { - mInputChannel.dispose(); - mInputChannel = null; - } - } mInputWindowHandle.setToken(null); } diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index a8c49e11e4e9..e32ce525cb40 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -2309,13 +2309,6 @@ static jint nativeGetKeyCodeForKeyLocation(JNIEnv* env, jobject nativeImplObj, j locationKeyCode); } -static void handleInputChannelDisposed(JNIEnv* env, jobject /* inputChannelObj */, - const std::shared_ptr<InputChannel>& inputChannel, - void* data) { - NativeInputManager* im = static_cast<NativeInputManager*>(data); - im->removeInputChannel(inputChannel->getConnectionToken()); -} - static jobject nativeCreateInputChannel(JNIEnv* env, jobject nativeImplObj, jstring nameObj) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); @@ -2337,8 +2330,6 @@ static jobject nativeCreateInputChannel(JNIEnv* env, jobject nativeImplObj, jstr return nullptr; } - android_view_InputChannel_setDisposeCallback(env, inputChannelObj, - handleInputChannelDisposed, im); return inputChannelObj; } diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java b/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java index 7c239ef02e58..586ff52aa78c 100644 --- a/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java +++ b/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java @@ -328,6 +328,7 @@ public class TestDreamEnvironment { case DREAM_STATE_STARTED -> startDream(); case DREAM_STATE_WOKEN -> wakeDream(); } + mTestableLooper.processAllMessages(); } while (mCurrentDreamState < state); return true; diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java index 2a513ae3a8e8..d79d88400cf9 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java @@ -953,11 +953,13 @@ public final class AlarmManagerServiceTest { @Test @EnableFlags(Flags.FLAG_ACQUIRE_WAKELOCK_BEFORE_SEND) - public void testWakelockOrdering() throws Exception { + public void testWakelockOrderingFirstAlarm() throws Exception { final long triggerTime = mNowElapsedTest + 5000; final PendingIntent alarmPi = getNewMockPendingIntent(); setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, alarmPi); + // Pretend that it is the first alarm in this batch, or no other alarms are still processing + mService.mBroadcastRefCount = 0; mNowElapsedTest = mTestTimer.getElapsed(); mTestTimer.expire(); @@ -975,20 +977,51 @@ public final class AlarmManagerServiceTest { @Test @EnableFlags(Flags.FLAG_ACQUIRE_WAKELOCK_BEFORE_SEND) - public void testWakelockReleasedWhenSendFails() throws Exception { + public void testWakelockOrderingNonFirst() throws Exception { final long triggerTime = mNowElapsedTest + 5000; final PendingIntent alarmPi = getNewMockPendingIntent(); setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, alarmPi); + // Pretend that some previous alarms are still processing. + mService.mBroadcastRefCount = 3; + mNowElapsedTest = mTestTimer.getElapsed(); + mTestTimer.expire(); + + final ArgumentCaptor<PendingIntent.OnFinished> onFinishedCaptor = + ArgumentCaptor.forClass(PendingIntent.OnFinished.class); + verify(alarmPi).send(eq(mMockContext), eq(0), any(Intent.class), onFinishedCaptor.capture(), + any(Handler.class), isNull(), any()); + onFinishedCaptor.getValue().onSendFinished(alarmPi, null, 0, null, null); + + verify(mWakeLock, never()).acquire(); + verify(mWakeLock, never()).release(); + } + + @Test + @EnableFlags(Flags.FLAG_ACQUIRE_WAKELOCK_BEFORE_SEND) + public void testWakelockReleasedWhenSendFails() throws Exception { + final PendingIntent alarmPi = getNewMockPendingIntent(); doThrow(new PendingIntent.CanceledException("test")).when(alarmPi).send(eq(mMockContext), eq(0), any(Intent.class), any(), any(Handler.class), isNull(), any()); + setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 5000, alarmPi); + + // Pretend that it is the first alarm in this batch, or no other alarms are still processing + mService.mBroadcastRefCount = 0; mNowElapsedTest = mTestTimer.getElapsed(); mTestTimer.expire(); final InOrder inOrder = Mockito.inOrder(mWakeLock); inOrder.verify(mWakeLock).acquire(); inOrder.verify(mWakeLock).release(); + + setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 5000, alarmPi); + + // Pretend that some previous alarms are still processing. + mService.mBroadcastRefCount = 4; + mNowElapsedTest = mTestTimer.getElapsed(); + mTestTimer.expire(); + inOrder.verifyNoMoreInteractions(); } @Test diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index 3a9c99d57d71..d540b2ec13eb 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -155,7 +155,6 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { doAnswer((invocation) -> { Log.v(TAG, "Intercepting startProcessLocked() for " + Arrays.toString(invocation.getArguments())); - assertHealth(); final String processName = invocation.getArgument(0); final ProcessStartBehavior behavior = mNewProcessStartBehaviors.getOrDefault( processName, mNextProcessStartBehavior.getAndSet(ProcessStartBehavior.SUCCESS)); @@ -206,6 +205,9 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { mActiveProcesses.remove(res); res.setKilled(true); break; + case MISSING_RESPONSE: + res.setPendingStart(true); + break; default: throw new UnsupportedOperationException(); } @@ -244,6 +246,7 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0; mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500; mConstants.MAX_FROZEN_OUTGOING_BROADCASTS = 10; + mConstants.PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS = 2000; } @After @@ -279,6 +282,8 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { FAIL_NULL, /** Process is killed without reporting to BroadcastQueue */ KILLED_WITHOUT_NOTIFY, + /** Process start fails without no response */ + MISSING_RESPONSE, } private enum ProcessBehavior { @@ -1173,6 +1178,37 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { verifyScheduleReceiver(times(1), receiverOrangeApp, timezone); } + @Test + public void testProcessStartWithMissingResponse() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); + + mNewProcessStartBehaviors.put(PACKAGE_GREEN, ProcessStartBehavior.MISSING_RESPONSE); + + final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, List.of( + withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10), + withPriority(makeRegisteredReceiver(receiverBlueApp), 5), + withPriority(makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW), 0)))); + + final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED); + enqueueBroadcast(makeBroadcastRecord(timezone, callerApp, + List.of(makeManifestReceiver(PACKAGE_ORANGE, CLASS_ORANGE)))); + + waitForIdle(); + final ProcessRecord receiverGreenApp = mAms.getProcessRecordLocked(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + final ProcessRecord receiverYellowApp = mAms.getProcessRecordLocked(PACKAGE_YELLOW, + getUidForPackage(PACKAGE_YELLOW)); + final ProcessRecord receiverOrangeApp = mAms.getProcessRecordLocked(PACKAGE_ORANGE, + getUidForPackage(PACKAGE_ORANGE)); + + verifyScheduleReceiver(times(1), receiverGreenApp, airplane); + verifyScheduleRegisteredReceiver(times(1), receiverBlueApp, airplane); + verifyScheduleReceiver(times(1), receiverYellowApp, airplane); + verifyScheduleReceiver(times(1), receiverOrangeApp, timezone); + } + /** * Verify that a broadcast sent to a frozen app, which gets killed as part of unfreezing * process due to pending sync binder transactions, is delivered as expected. diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 9cfa51a85988..8253595a50d1 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -2374,14 +2374,6 @@ public class AccessibilityManagerServiceTest { return lockState; } - private void assertStartActivityWithExpectedComponentName(Context mockContext, - String componentName) { - verify(mockContext).startActivityAsUser(mIntentArgumentCaptor.capture(), - any(Bundle.class), any(UserHandle.class)); - assertThat(mIntentArgumentCaptor.getValue().getStringExtra( - Intent.EXTRA_COMPONENT_NAME)).isEqualTo(componentName); - } - private void assertStartActivityWithExpectedShortcutType(Context mockContext, @UserShortcutType int shortcutType) { verify(mockContext).startActivityAsUser(mIntentArgumentCaptor.capture(), @@ -2484,10 +2476,6 @@ public class AccessibilityManagerServiceTest { return mMockContext; } - public void addMockUserContext(int userId, Context context) { - mMockUserContexts.put(userId, context); - } - @Override @NonNull public Context createContextAsUser(UserHandle user, int flags) { 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 ea25e7992dd9..2be43c6f21a5 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 @@ -18,6 +18,7 @@ package com.android.server.accessibility.autoclick; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK; import static com.android.server.testutils.MockitoUtilsKt.eq; import static com.google.common.truth.Truth.assertThat; @@ -547,6 +548,29 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void triggerRightClickWithRevertToLeftClickEnabled_resetClickType() { + // Move mouse to initialize autoclick panel. + injectFakeMouseActionHoverMoveEvent(); + + AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class); + mController.mAutoclickTypePanel = mockAutoclickTypePanel; + mController.clickPanelController.handleAutoclickTypeChange(AUTOCLICK_TYPE_RIGHT_CLICK); + + // Set ACCESSIBILITY_AUTOCLICK_REVERT_TO_LEFT_CLICK to true. + Settings.Secure.putIntForUser(mTestableContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_AUTOCLICK_REVERT_TO_LEFT_CLICK, + AccessibilityUtils.State.ON, + mTestableContext.getUserId()); + mController.onChangeForTesting(/* selfChange= */ true, + Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_AUTOCLICK_REVERT_TO_LEFT_CLICK)); + when(mockAutoclickTypePanel.isPaused()).thenReturn(false); + mController.mClickScheduler.run(); + assertThat(mController.mClickScheduler.getRevertToLeftClickForTesting()).isTrue(); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void pauseButton_flagOn_clickNotTriggeredWhenPaused() { injectFakeMouseActionHoverMoveEvent(); @@ -766,6 +790,8 @@ public class AutoclickControllerTest { // Set click type to right click. mController.clickPanelController.handleAutoclickTypeChange( AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK); + AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class); + mController.mAutoclickTypePanel = mockAutoclickTypePanel; // Send hover move event. MotionEvent hoverMove = MotionEvent.obtain( diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpSqlPersistenceTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpSqlPersistenceTest.java index 01fee7f66497..918159f9262b 100644 --- a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpSqlPersistenceTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpSqlPersistenceTest.java @@ -97,7 +97,8 @@ public class DiscreteAppOpSqlPersistenceTest { mDiscreteRegistry.recordDiscreteAccess(opEvent2); List<DiscreteOp> discreteOps = mDiscreteRegistry.getAllDiscreteOps(); - assertThat(discreteOps.size()).isEqualTo(1); + assertWithMessage("Expected list size is 1, but the list is: " + discreteOps) + .that(discreteOps.size()).isEqualTo(1); assertThat(discreteOps).contains(opEvent); } diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java index e87e107cd793..3e86f7b57294 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java @@ -41,6 +41,7 @@ import static android.util.DisplayMetrics.DENSITY_DEFAULT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.internal.policy.SystemBarUtils.getDesktopViewAppHeaderHeightPx; import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_INITIAL_BOUNDS_SCALE; import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_LANDSCAPE_APP_PADDING; import static com.android.server.wm.DesktopModeBoundsCalculator.centerInScreen; @@ -893,9 +894,11 @@ public class DesktopModeLaunchParamsModifierTests extends @Test @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, - Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS}) + Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS, + Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS}) public void testDefaultLandscapeBounds_landscapeDevice_unResizable_landscapeOrientation() { setupDesktopModeLaunchParamsModifier(); + final int captionHeight = getDesktopViewAppHeaderHeightPx(mContext); final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE, LANDSCAPE_DISPLAY_BOUNDS); @@ -903,11 +906,11 @@ public class DesktopModeLaunchParamsModifierTests extends final ActivityRecord activity = createActivity(display, SCREEN_ORIENTATION_LANDSCAPE, task, /* ignoreOrientationRequest */ true); - - final int desiredWidth = - (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final float displayAspectRatio = (float) LANDSCAPE_DISPLAY_BOUNDS.width() + / LANDSCAPE_DISPLAY_BOUNDS.height(); final int desiredHeight = (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredWidth = (int) ((desiredHeight - captionHeight) * displayAspectRatio); assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task) .setActivity(activity).calculate()); @@ -916,7 +919,8 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + @EnableFlags({Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS, + Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS}) public void testUnResizablePortraitBounds_landscapeDevice_unResizable_portraitOrientation() { setupDesktopModeLaunchParamsModifier(); @@ -925,6 +929,7 @@ public class DesktopModeLaunchParamsModifierTests extends final Task task = createTask(display, /* isResizeable */ false); final ActivityRecord activity = createActivity(display, SCREEN_ORIENTATION_PORTRAIT, task, /* ignoreOrientationRequest */ true); + final int captionHeight = getDesktopViewAppHeaderHeightPx(mContext); spyOn(activity.mAppCompatController.getDesktopAspectRatioPolicy()); doReturn(LETTERBOX_ASPECT_RATIO).when(activity.mAppCompatController @@ -932,7 +937,7 @@ public class DesktopModeLaunchParamsModifierTests extends final int desiredHeight = (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); - final int desiredWidth = (int) (desiredHeight / LETTERBOX_ASPECT_RATIO); + final int desiredWidth = (int) ((desiredHeight - captionHeight) / LETTERBOX_ASPECT_RATIO); assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task) .setActivity(activity).calculate()); @@ -1070,7 +1075,8 @@ public class DesktopModeLaunchParamsModifierTests extends @Test @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, - Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS}) + Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS, + Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS}) public void testDefaultPortraitBounds_portraitDevice_unResizable_portraitOrientation() { setupDesktopModeLaunchParamsModifier(); @@ -1079,12 +1085,14 @@ public class DesktopModeLaunchParamsModifierTests extends final Task task = createTask(display, /* isResizeable */ false); final ActivityRecord activity = createActivity(display, SCREEN_ORIENTATION_PORTRAIT, task, /* ignoreOrientationRequest */ true); + final int captionHeight = getDesktopViewAppHeaderHeightPx(mContext); - - final int desiredWidth = - (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final float displayAspectRatio = (float) PORTRAIT_DISPLAY_BOUNDS.height() + / PORTRAIT_DISPLAY_BOUNDS.width(); final int desiredHeight = - (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredWidth = + (int) ((desiredHeight - captionHeight) / displayAspectRatio); assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task) .setActivity(activity).calculate()); @@ -1093,7 +1101,8 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + @EnableFlags({Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS, + Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS}) public void testUnResizableLandscapeBounds_portraitDevice_unResizable_landscapeOrientation() { setupDesktopModeLaunchParamsModifier(); @@ -1102,6 +1111,7 @@ public class DesktopModeLaunchParamsModifierTests extends final Task task = createTask(display, /* isResizeable */ false); final ActivityRecord activity = createActivity(display, SCREEN_ORIENTATION_LANDSCAPE, task, /* ignoreOrientationRequest */ true); + final int captionHeight = getDesktopViewAppHeaderHeightPx(mContext); spyOn(activity.mAppCompatController.getDesktopAspectRatioPolicy()); doReturn(LETTERBOX_ASPECT_RATIO).when(activity.mAppCompatController @@ -1109,7 +1119,7 @@ public class DesktopModeLaunchParamsModifierTests extends final int desiredWidth = PORTRAIT_DISPLAY_BOUNDS.width() - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2); - final int desiredHeight = (int) (desiredWidth / LETTERBOX_ASPECT_RATIO); + final int desiredHeight = (int) (desiredWidth / LETTERBOX_ASPECT_RATIO) + captionHeight; assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task) .setActivity(activity).calculate()); diff --git a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java index 67a95de8a5c1..0d5828a3c4e1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java @@ -44,6 +44,7 @@ import android.app.ActivityOptions; import android.content.ComponentName; import android.content.pm.ActivityInfo.WindowLayout; import android.graphics.Rect; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.util.ArrayMap; import android.util.SparseArray; @@ -52,6 +53,7 @@ import androidx.test.filters.MediumTest; import com.android.server.wm.LaunchParamsController.LaunchParams; import com.android.server.wm.LaunchParamsController.LaunchParamsModifier; +import com.android.window.flags.Flags; import org.junit.Before; import org.junit.Test; @@ -372,6 +374,34 @@ public class LaunchParamsControllerTests extends WindowTestsBase { assertEquals(expected, task.mLastNonFullscreenBounds); } + /** + * Ensures that app bounds are set to exclude freeform caption if window is in freeform. + */ + @Test + @EnableFlags(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS) + public void testLayoutTaskBoundsFreeformAppBounds() { + final Rect expected = new Rect(10, 20, 30, 40); + + final LaunchParams params = new LaunchParams(); + params.mBounds.set(expected); + params.mAppBounds.set(expected); + final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params); + final Task task = new TaskBuilder(mAtm.mTaskSupervisor) + .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + final ActivityOptions options = ActivityOptions.makeBasic().setFlexibleLaunchSize(true); + + mController.registerModifier(positioner); + + assertNotEquals(expected, task.getBounds()); + + layoutTask(task, options); + + // Task will make adjustments to requested bounds. We only need to guarantee that the + // requested bounds are expected. + assertEquals(expected, + task.getRequestedOverrideConfiguration().windowConfiguration.getAppBounds()); + } + public static class InstrumentedPositioner implements LaunchParamsModifier { private final int mReturnVal; @@ -473,4 +503,9 @@ public class LaunchParamsControllerTests extends WindowTestsBase { mController.layoutTask(task, null /* layout */, null /* activity */, null /* source */, null /* options */); } + + private void layoutTask(@NonNull Task task, ActivityOptions options) { + mController.layoutTask(task, null /* layout */, null /* activity */, null /* source */, + options /* options */); + } } diff --git a/tests/Input/assets/testPointerScale.png b/tests/Input/assets/testPointerScale.png Binary files differindex 54d37c24afc6..781df47a5e24 100644 --- a/tests/Input/assets/testPointerScale.png +++ b/tests/Input/assets/testPointerScale.png diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index 649241aaaa8c..6b099450064f 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -238,6 +238,36 @@ public class TestableLooper { while (processQueuedMessages() != 0) ; } + public long peekWhen() { + if (isAtLeastBaklava()) { + return peekWhenBaklava(); + } else { + return peekWhenLegacy(); + } + } + + private long peekWhenBaklava() { + Long when = mQueueWrapper.peekWhen(); + if (when != null) { + return when; + } else { + return 0; + } + } + + private long peekWhenLegacy() { + try { + Message msg = (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(mLooper.getQueue()); + if (msg != null) { + return msg.getWhen(); + } else { + return 0; + } + } catch (IllegalAccessException e) { + throw new RuntimeException("Access failed in TestableLooper: set - Message.when", e); + } + } + public void moveTimeForward(long milliSeconds) { if (isAtLeastBaklava()) { moveTimeForwardBaklava(milliSeconds); |