Merge "[flexiglass] Add SceneTransitionLayout (STL) overlays to the framework." into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 3c5686b..edb119e 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -100,6 +100,7 @@
"framework-jobscheduler-job.flags-aconfig-java",
"framework_graphics_flags_java_lib",
"hwui_flags_java_lib",
+ "interaction_jank_monitor_flags_lib",
"libcore_exported_aconfig_flags_lib",
"libgui_flags_java_lib",
"power_flags_lib",
@@ -1565,3 +1566,17 @@
aconfig_declarations: "dropbox_flags",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+
+// Zero Jank
+aconfig_declarations {
+ name: "interaction_jank_monitor_flags",
+ package: "com.android.internal.jank",
+ container: "system",
+ srcs: ["core/java/com/android/internal/jank/flags.aconfig"],
+}
+
+java_aconfig_library {
+ name: "interaction_jank_monitor_flags_lib",
+ aconfig_declarations: "interaction_jank_monitor_flags",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/apct-tests/perftests/protolog/src/com/android/internal/protolog/ProtologPerfTest.java b/apct-tests/perftests/protolog/src/com/android/internal/protolog/ProtoLogPerfTest.java
similarity index 98%
rename from apct-tests/perftests/protolog/src/com/android/internal/protolog/ProtologPerfTest.java
rename to apct-tests/perftests/protolog/src/com/android/internal/protolog/ProtoLogPerfTest.java
index e1edb37..92dd9be 100644
--- a/apct-tests/perftests/protolog/src/com/android/internal/protolog/ProtologPerfTest.java
+++ b/apct-tests/perftests/protolog/src/com/android/internal/protolog/ProtoLogPerfTest.java
@@ -33,7 +33,7 @@
import java.util.Collection;
@RunWith(Parameterized.class)
-public class ProtologPerfTest {
+public class ProtoLogPerfTest {
@Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
@Parameters(name="logToProto_{0}_logToLogcat_{1}")
@@ -49,7 +49,7 @@
private final boolean mLogToProto;
private final boolean mLogToLogcat;
- public ProtologPerfTest(boolean logToProto, boolean logToLogcat) {
+ public ProtoLogPerfTest(boolean logToProto, boolean logToLogcat) {
mLogToProto = logToProto;
mLogToLogcat = logToLogcat;
}
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index ec23cfe..0a35c5a 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1542,6 +1542,10 @@
method @Deprecated public final void setPreviewSurface(android.view.Surface) throws java.io.IOException;
}
+ public final class Sensor {
+ method public int getHandle();
+ }
+
public final class SensorPrivacyManager {
method @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setCameraPrivacyAllowlist(@NonNull java.util.List<java.lang.String>);
method @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setSensorPrivacy(int, int, boolean);
@@ -3666,7 +3670,11 @@
method @Nullable public android.view.Display.Mode getUserPreferredDisplayMode();
method public boolean hasAccess(int);
method @RequiresPermission(android.Manifest.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE) public void setUserPreferredDisplayMode(@NonNull android.view.Display.Mode);
+ field public static final int FLAG_ALWAYS_UNLOCKED = 512; // 0x200
+ field public static final int FLAG_OWN_FOCUS = 2048; // 0x800
+ field public static final int FLAG_TOUCH_FEEDBACK_DISABLED = 1024; // 0x400
field public static final int FLAG_TRUSTED = 128; // 0x80
+ field public static final int REMOVE_MODE_DESTROY_CONTENT = 1; // 0x1
field public static final int TYPE_EXTERNAL = 2; // 0x2
field public static final int TYPE_INTERNAL = 1; // 0x1
field public static final int TYPE_OVERLAY = 4; // 0x4
diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
index 82f183f..68bc9bc 100644
--- a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
+++ b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
@@ -17,7 +17,13 @@
package android.companion.virtual.sensor;
+import static android.hardware.Sensor.REPORTING_MODE_CONTINUOUS;
+import static android.hardware.Sensor.REPORTING_MODE_ONE_SHOT;
+import static android.hardware.Sensor.REPORTING_MODE_ON_CHANGE;
+import static android.hardware.Sensor.REPORTING_MODE_SPECIAL_TRIGGER;
+
import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -30,6 +36,8 @@
import android.os.Parcel;
import android.os.Parcelable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
@@ -71,6 +79,17 @@
private final int mFlags;
+ /** @hide */
+ @IntDef(prefix = "REPORTING_MODE_", value = {
+ REPORTING_MODE_CONTINUOUS,
+ REPORTING_MODE_ON_CHANGE,
+ REPORTING_MODE_ONE_SHOT,
+ REPORTING_MODE_SPECIAL_TRIGGER
+ })
+
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ReportingMode {}
+
private VirtualSensorConfig(int type, @NonNull String name, @Nullable String vendor,
float maximumRange, float resolution, float power, int minDelay, int maxDelay,
int flags) {
@@ -240,7 +259,7 @@
* @see Sensor#getReportingMode()
*/
@FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
- public int getReportingMode() {
+ public @ReportingMode int getReportingMode() {
return ((mFlags & REPORTING_MODE_MASK) >> REPORTING_MODE_SHIFT);
}
@@ -442,11 +461,11 @@
*/
@FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
@NonNull
- public VirtualSensorConfig.Builder setReportingMode(int reportingMode) {
- if (reportingMode != Sensor.REPORTING_MODE_CONTINUOUS
- && reportingMode != Sensor.REPORTING_MODE_ON_CHANGE
- && reportingMode != Sensor.REPORTING_MODE_ONE_SHOT
- && reportingMode != Sensor.REPORTING_MODE_SPECIAL_TRIGGER) {
+ public VirtualSensorConfig.Builder setReportingMode(@ReportingMode int reportingMode) {
+ if (reportingMode != REPORTING_MODE_CONTINUOUS
+ && reportingMode != REPORTING_MODE_ON_CHANGE
+ && reportingMode != REPORTING_MODE_ONE_SHOT
+ && reportingMode != REPORTING_MODE_SPECIAL_TRIGGER) {
throw new IllegalArgumentException("Invalid reporting mode: " + reportingMode);
}
mFlags |= reportingMode << REPORTING_MODE_SHIFT;
diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java
index 10c3730..e0b9f60 100644
--- a/core/java/android/hardware/Sensor.java
+++ b/core/java/android/hardware/Sensor.java
@@ -17,7 +17,9 @@
package android.hardware;
+import android.annotation.SuppressLint;
import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.hardware.input.InputSensorInfo;
import android.os.Build;
@@ -1182,6 +1184,8 @@
/** @hide */
@UnsupportedAppUsage
+ @SuppressLint("UnflaggedApi") // Promotion to TestApi
+ @TestApi
public int getHandle() {
return mHandle;
}
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 1f7ed8b..82c52a6 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -314,6 +314,8 @@
* @hide
* @see #getFlags()
*/
+ @SuppressLint("UnflaggedApi") // Promotion to TestApi
+ @TestApi
public static final int FLAG_ALWAYS_UNLOCKED = 1 << 9;
/**
@@ -323,6 +325,8 @@
* @hide
* @see #getFlags()
*/
+ @SuppressLint("UnflaggedApi") // Promotion to TestApi
+ @TestApi
public static final int FLAG_TOUCH_FEEDBACK_DISABLED = 1 << 10;
/**
@@ -336,6 +340,8 @@
* @see #FLAG_TRUSTED
* @hide
*/
+ @SuppressLint("UnflaggedApi") // Promotion to TestApi
+ @TestApi
public static final int FLAG_OWN_FOCUS = 1 << 11;
/**
@@ -642,6 +648,8 @@
* @hide
*/
// TODO (b/114338689): Remove the flag and use WindowManager#REMOVE_CONTENT_MODE_DESTROY
+ @SuppressLint("UnflaggedApi") // Promotion to TestApi
+ @TestApi
public static final int REMOVE_MODE_DESTROY_CONTENT = 1;
/** @hide */
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index 477e35b..391d757 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -310,21 +310,22 @@
}
final boolean requestedVisible = (mController.getRequestedVisibleTypes() & mType) != 0;
+ // If we don't have control or the leash (in case of the IME), we enforce the
+ // visibility to be hidden, as otherwise we would let the app know too early.
+ if (mSourceControl == null) {
+ if (DEBUG) {
+ Log.d(TAG, TextUtils.formatSimple(
+ "applyLocalVisibilityOverride: No control in %s for type %s, "
+ + "requestedVisible=%s",
+ mController.getHost().getRootViewTitle(),
+ WindowInsets.Type.toString(mType), requestedVisible));
+ }
+ return false;
+ }
if (Flags.refactorInsetsController()) {
- // If we don't have control or the leash (in case of the IME), we enforce the
- // visibility to be hidden, as otherwise we would let the app know too early.
- if (mSourceControl == null) {
- if (DEBUG) {
- Log.d(TAG, TextUtils.formatSimple(
- "applyLocalVisibilityOverride: No control in %s for type %s, "
- + "requestedVisible=%s",
- mController.getHost().getRootViewTitle(),
- WindowInsets.Type.toString(mType), requestedVisible));
- }
- return false;
- // TODO(b/323136120) add a flag to the control, to define whether a leash is needed
- } else if (mId != InsetsSource.ID_IME_CAPTION_BAR
- && mSourceControl.getLeash() == null) {
+ // TODO(b/323136120) add a flag to the control, to define whether a leash is
+ // needed and make it generic for all types
+ if (mId == InsetsSource.ID_IME && mSourceControl.getLeash() == null) {
if (DEBUG) {
Log.d(TAG, TextUtils.formatSimple(
"applyLocalVisibilityOverride: Set the source visibility to false, as"
@@ -338,16 +339,6 @@
// changed state
return wasVisible;
}
- } else {
- // If we don't have control, we are not able to change the visibility.
- if (mSourceControl == null) {
- if (DEBUG) {
- Log.d(TAG, "applyLocalVisibilityOverride: No control in "
- + mController.getHost().getRootViewTitle()
- + " requestedVisible=" + requestedVisible);
- }
- return false;
- }
}
if (source.isVisible() == requestedVisible) {
return false;
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 9e4b27d..2dda835 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -445,16 +445,20 @@
// Jank due to unknown reasons.
public static final int UNKNOWN = 0x80;
- public JankData(long frameVsyncId, @JankType int jankType, long frameIntervalNs) {
+ public JankData(long frameVsyncId, @JankType int jankType, long frameIntervalNs,
+ long scheduledAppFrameTimeNs, long actualAppFrameTimeNs) {
this.frameVsyncId = frameVsyncId;
this.jankType = jankType;
this.frameIntervalNs = frameIntervalNs;
-
+ this.scheduledAppFrameTimeNs = scheduledAppFrameTimeNs;
+ this.actualAppFrameTimeNs = actualAppFrameTimeNs;
}
public final long frameVsyncId;
public final @JankType int jankType;
public final long frameIntervalNs;
+ public final long scheduledAppFrameTimeNs;
+ public final long actualAppFrameTimeNs;
}
/**
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 217bca7..ebf87f1 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -115,6 +115,16 @@
}
flag {
+ name: "respect_orientation_change_for_unresizeable"
+ namespace: "lse_desktop_experience"
+ description: "Whether to resize task to respect requested orientation change of unresizeable activity"
+ bug: "353338503"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "enable_camera_compat_for_desktop_windowing"
namespace: "lse_desktop_experience"
description: "Whether to apply Camera Compat treatment to fixed-orientation apps in desktop windowing mode"
diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java
index 53ef49b..d474c6d 100644
--- a/core/java/com/android/internal/jank/FrameTracker.java
+++ b/core/java/com/android/internal/jank/FrameTracker.java
@@ -127,7 +127,7 @@
private Runnable mWaitForFinishTimedOut;
private static class JankInfo {
- long frameVsyncId;
+ final long frameVsyncId;
long totalDurationNanos;
boolean isFirstFrame;
boolean hwuiCallbackFired;
@@ -135,29 +135,42 @@
@JankType int jankType;
@RefreshRate int refreshRate;
- static JankInfo createFromHwuiCallback(long frameVsyncId, long totalDurationNanos,
- boolean isFirstFrame) {
- return new JankInfo(frameVsyncId, true, false, JANK_NONE, UNKNOWN_REFRESH_RATE,
- totalDurationNanos, isFirstFrame);
+ static JankInfo createFromHwuiCallback(
+ long frameVsyncId, long totalDurationNanos, boolean isFirstFrame) {
+ return new JankInfo(frameVsyncId).update(totalDurationNanos, isFirstFrame);
}
- static JankInfo createFromSurfaceControlCallback(long frameVsyncId,
- @JankType int jankType, @RefreshRate int refreshRate) {
- return new JankInfo(
- frameVsyncId, false, true, jankType, refreshRate, 0, false /* isFirstFrame */);
+ static JankInfo createFromSurfaceControlCallback(SurfaceControl.JankData jankStat) {
+ return new JankInfo(jankStat.frameVsyncId).update(jankStat);
}
- private JankInfo(long frameVsyncId, boolean hwuiCallbackFired,
- boolean surfaceControlCallbackFired, @JankType int jankType,
- @RefreshRate int refreshRate,
- long totalDurationNanos, boolean isFirstFrame) {
+ private JankInfo(long frameVsyncId) {
this.frameVsyncId = frameVsyncId;
- this.hwuiCallbackFired = hwuiCallbackFired;
- this.surfaceControlCallbackFired = surfaceControlCallbackFired;
- this.jankType = jankType;
- this.refreshRate = refreshRate;
- this.totalDurationNanos = totalDurationNanos;
+ this.hwuiCallbackFired = false;
+ this.surfaceControlCallbackFired = false;
+ this.jankType = JANK_NONE;
+ this.refreshRate = UNKNOWN_REFRESH_RATE;
+ this.totalDurationNanos = 0;
+ this.isFirstFrame = false;
+ }
+
+ private JankInfo update(SurfaceControl.JankData jankStat) {
+ this.surfaceControlCallbackFired = true;
+ this.jankType = jankStat.jankType;
+ this.refreshRate = DisplayRefreshRate.getRefreshRate(jankStat.frameIntervalNs);
+ if (Flags.useSfFrameDuration()) {
+ this.totalDurationNanos = jankStat.actualAppFrameTimeNs;
+ }
+ return this;
+ }
+
+ private JankInfo update(long totalDurationNanos, boolean isFirstFrame) {
+ this.hwuiCallbackFired = true;
+ if (!Flags.useSfFrameDuration()) {
+ this.totalDurationNanos = totalDurationNanos;
+ }
this.isFirstFrame = isFirstFrame;
+ return this;
}
@Override
@@ -457,16 +470,12 @@
if (!isInRange(jankStat.frameVsyncId)) {
continue;
}
- int refreshRate = DisplayRefreshRate.getRefreshRate(jankStat.frameIntervalNs);
JankInfo info = findJankInfo(jankStat.frameVsyncId);
if (info != null) {
- info.surfaceControlCallbackFired = true;
- info.jankType = jankStat.jankType;
- info.refreshRate = refreshRate;
+ info.update(jankStat);
} else {
mJankInfos.put((int) jankStat.frameVsyncId,
- JankInfo.createFromSurfaceControlCallback(
- jankStat.frameVsyncId, jankStat.jankType, refreshRate));
+ JankInfo.createFromSurfaceControlCallback(jankStat));
}
}
processJankInfos();
@@ -513,9 +522,7 @@
}
JankInfo info = findJankInfo(frameVsyncId);
if (info != null) {
- info.hwuiCallbackFired = true;
- info.totalDurationNanos = totalDurationNanos;
- info.isFirstFrame = isFirstFrame;
+ info.update(totalDurationNanos, isFirstFrame);
} else {
mJankInfos.put((int) frameVsyncId, JankInfo.createFromHwuiCallback(
frameVsyncId, totalDurationNanos, isFirstFrame));
diff --git a/core/java/com/android/internal/jank/flags.aconfig b/core/java/com/android/internal/jank/flags.aconfig
new file mode 100644
index 0000000..676bb70
--- /dev/null
+++ b/core/java/com/android/internal/jank/flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.internal.jank"
+container: "system"
+
+flag {
+ name: "use_sf_frame_duration"
+ namespace: "android_platform_window_surfaces"
+ description: "Whether to get the frame duration from SurfaceFlinger, or HWUI"
+ bug: "354763298"
+}
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 0f53164..17c89f8 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -2089,9 +2089,11 @@
jobjectArray jJankDataArray = env->NewObjectArray(jankData.size(),
gJankDataClassInfo.clazz, nullptr);
for (size_t i = 0; i < jankData.size(); i++) {
- jobject jJankData = env->NewObject(gJankDataClassInfo.clazz, gJankDataClassInfo.ctor,
- jankData[i].frameVsyncId, jankData[i].jankType,
- jankData[i].frameIntervalNs);
+ jobject jJankData =
+ env->NewObject(gJankDataClassInfo.clazz, gJankDataClassInfo.ctor,
+ jankData[i].frameVsyncId, jankData[i].jankType,
+ jankData[i].frameIntervalNs, jankData[i].scheduledAppFrameTimeNs,
+ jankData[i].actualAppFrameTimeNs);
env->SetObjectArrayElement(jJankDataArray, i, jJankData);
env->DeleteLocalRef(jJankData);
}
@@ -2727,7 +2729,7 @@
jclass jankDataClazz =
FindClassOrDie(env, "android/view/SurfaceControl$JankData");
gJankDataClassInfo.clazz = MakeGlobalRefOrDie(env, jankDataClazz);
- gJankDataClassInfo.ctor = GetMethodIDOrDie(env, gJankDataClassInfo.clazz, "<init>", "(JIJ)V");
+ gJankDataClassInfo.ctor = GetMethodIDOrDie(env, gJankDataClassInfo.clazz, "<init>", "(JIJJJ)V");
jclass onJankDataListenerClazz =
FindClassOrDie(env, "android/view/SurfaceControl$OnJankDataListener");
gJankDataListenerClassInfo.clazz = MakeGlobalRefOrDie(env, onJankDataListenerClazz);
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index c084b4c..e1394bc 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -1535,7 +1535,7 @@
<!-- @hide -->
<style name="PointerIconVectorStyleStrokeBlack">
<item name="pointerIconVectorStroke">@color/black</item>
- <item name="pointerIconVectorStrokeInverse">@color/white</item>
+ <item name="pointerIconVectorStrokeInverse">@color/black</item>
</style>
<!-- @hide -->
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index edf461a..b0e48f1 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -803,3 +803,11 @@
include_annotations: ["android.platform.test.annotations.PlatinumTest"],
exclude_annotations: FLAKY_OR_IGNORED,
}
+
+test_module_config {
+ name: "FrameworksCoreTests_android_tracing",
+ base: "FrameworksCoreTests",
+ team: "trendy_team_windowing_tools",
+ test_suites: ["device-tests"],
+ include_filters: ["android.tracing"],
+}
diff --git a/core/tests/coretests/src/android/tracing/TEST_MAPPING b/core/tests/coretests/src/android/tracing/TEST_MAPPING
new file mode 100644
index 0000000..4b7adf9
--- /dev/null
+++ b/core/tests/coretests/src/android/tracing/TEST_MAPPING
@@ -0,0 +1,8 @@
+{
+ "postsubmit": [
+ {
+ "name": "FrameworksCoreTests_android_tracing",
+ "file_patterns": [".*\\.java"]
+ }
+ ]
+}
diff --git a/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java b/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
index 499caf5..c3a5b19c94 100644
--- a/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
+++ b/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
@@ -359,7 +359,7 @@
tracker.end(FrameTracker.REASON_END_NORMAL);
// Send incomplete callback for 102L
- sendSfFrame(tracker, 102L, JANK_NONE);
+ sendSfFrame(tracker, 4, 102L, JANK_NONE);
// Send janky but complete callbck fo 103L
sendFrame(tracker, 50, JANK_APP_DEADLINE_MISSED, 103L);
@@ -629,7 +629,7 @@
if (!tracker.mSurfaceOnly) {
sendHwuiFrame(tracker, durationMillis, vsyncId, firstWindowFrame);
}
- sendSfFrame(tracker, vsyncId, jankType);
+ sendSfFrame(tracker, durationMillis, vsyncId, jankType);
}
private void sendHwuiFrame(FrameTracker tracker, long durationMillis, long vsyncId,
@@ -645,11 +645,13 @@
captor.getValue().run();
}
- private void sendSfFrame(FrameTracker tracker, long vsyncId, @JankType int jankType) {
+ private void sendSfFrame(
+ FrameTracker tracker, long durationMillis, long vsyncId, @JankType int jankType) {
final ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
doNothing().when(tracker).postCallback(captor.capture());
mListenerCapture.getValue().onJankDataAvailable(new JankData[] {
- new JankData(vsyncId, jankType, FRAME_TIME_60Hz)
+ new JankData(vsyncId, jankType, FRAME_TIME_60Hz, FRAME_TIME_60Hz,
+ TimeUnit.MILLISECONDS.toNanos(durationMillis))
});
captor.getValue().run();
}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
index 47d5274..424d4bf 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
@@ -48,7 +48,7 @@
TASK_STACK_OBSERVER_IN_SHELL(Flags::enableTaskStackObserverInShell, true),
SIZE_CONSTRAINTS(Flags::enableDesktopWindowingSizeConstraints, true),
DISABLE_SNAP_RESIZE(Flags::disableNonResizableAppSnapResizing, true),
- DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, true),
+ DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, false),
ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true),
BACK_NAVIGATION(Flags::enableDesktopWindowingBackNavigation, true),
EDGE_DRAG_RESIZE(Flags::enableWindowingEdgeDragResize, true),
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 4db8a82..46cb6ec 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
@@ -59,6 +59,7 @@
import com.android.wm.shell.dagger.back.ShellBackAnimationModule;
import com.android.wm.shell.dagger.pip.PipModule;
import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler;
+import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver;
@@ -237,7 +238,8 @@
InteractionJankMonitor interactionJankMonitor,
AppToWebGenericLinksParser genericLinksParser,
MultiInstanceHelper multiInstanceHelper,
- Optional<DesktopTasksLimiter> desktopTasksLimiter) {
+ Optional<DesktopTasksLimiter> desktopTasksLimiter,
+ Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler) {
if (DesktopModeStatus.canEnterDesktopMode(context)) {
return new DesktopModeWindowDecorViewModel(
context,
@@ -259,7 +261,8 @@
interactionJankMonitor,
genericLinksParser,
multiInstanceHelper,
- desktopTasksLimiter);
+ desktopTasksLimiter,
+ desktopActivityOrientationHandler);
}
return new CaptionWindowDecorViewModel(
context,
@@ -677,6 +680,24 @@
@WMSingleton
@Provides
+ static Optional<DesktopActivityOrientationChangeHandler> provideActivityOrientationHandler(
+ Context context,
+ ShellInit shellInit,
+ ShellTaskOrganizer shellTaskOrganizer,
+ TaskStackListenerImpl taskStackListener,
+ ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
+ @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository
+ ) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
+ return Optional.of(new DesktopActivityOrientationChangeHandler(
+ context, shellInit, shellTaskOrganizer, taskStackListener,
+ toggleResizeDesktopTaskTransitionHandler, desktopModeTaskRepository));
+ }
+ return Optional.empty();
+ }
+
+ @WMSingleton
+ @Provides
static Optional<DesktopTasksTransitionObserver> provideDesktopTasksTransitionObserver(
Context context,
Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt
new file mode 100644
index 0000000..59e0068
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.content.pm.ActivityInfo.ScreenOrientation
+import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import android.content.res.Configuration.ORIENTATION_PORTRAIT
+import android.graphics.Rect
+import android.util.Size
+import android.window.WindowContainerTransaction
+import com.android.window.flags.Flags
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.TaskStackListenerCallback
+import com.android.wm.shell.common.TaskStackListenerImpl
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.sysui.ShellInit
+
+/** Handles task resizing to respect orientation change of non-resizeable activities in desktop. */
+class DesktopActivityOrientationChangeHandler(
+ context: Context,
+ shellInit: ShellInit,
+ private val shellTaskOrganizer: ShellTaskOrganizer,
+ private val taskStackListener: TaskStackListenerImpl,
+ private val resizeHandler: ToggleResizeDesktopTaskTransitionHandler,
+ private val taskRepository: DesktopModeTaskRepository,
+) {
+
+ init {
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
+ shellInit.addInitCallback({ onInit() }, this)
+ }
+ }
+
+ private fun onInit() {
+ taskStackListener.addListener(object : TaskStackListenerCallback {
+ override fun onActivityRequestedOrientationChanged(
+ taskId: Int,
+ @ScreenOrientation requestedOrientation: Int
+ ) {
+ // Handle requested screen orientation changes at runtime.
+ handleActivityOrientationChange(taskId, requestedOrientation)
+ }
+ })
+ }
+
+ /**
+ * Triggered with onTaskInfoChanged to handle:
+ * * New activity launching from same task with different orientation
+ * * Top activity closing in same task with different orientation to previous activity
+ */
+ fun handleActivityOrientationChange(oldTask: RunningTaskInfo, newTask: RunningTaskInfo) {
+ val newTopActivityInfo = newTask.topActivityInfo ?: return
+ val oldTopActivityInfo = oldTask.topActivityInfo ?: return
+ // Check if screen orientation is different from old task info so there is no duplicated
+ // calls to handle runtime requested orientation changes.
+ if (oldTopActivityInfo.screenOrientation != newTopActivityInfo.screenOrientation) {
+ handleActivityOrientationChange(newTask.taskId, newTopActivityInfo.screenOrientation)
+ }
+ }
+
+ private fun handleActivityOrientationChange(
+ taskId: Int,
+ @ScreenOrientation requestedOrientation: Int
+ ) {
+ if (!Flags.respectOrientationChangeForUnresizeable()) return
+ val task = shellTaskOrganizer.getRunningTaskInfo(taskId) ?: return
+ if (!isDesktopModeShowing(task.displayId) || !task.isFreeform || task.isResizeable) return
+
+ val taskBounds = task.configuration.windowConfiguration.bounds
+ val taskHeight = taskBounds.height()
+ val taskWidth = taskBounds.width()
+ if (taskWidth == taskHeight) return
+ val orientation =
+ if (taskWidth > taskHeight) ORIENTATION_LANDSCAPE else ORIENTATION_PORTRAIT
+
+ // Non-resizeable activity requested opposite orientation.
+ if (orientation == ORIENTATION_PORTRAIT
+ && ActivityInfo.isFixedOrientationLandscape(requestedOrientation)
+ || orientation == ORIENTATION_LANDSCAPE
+ && ActivityInfo.isFixedOrientationPortrait(requestedOrientation)) {
+
+ val finalSize = Size(taskHeight, taskWidth)
+ // Use the center x as the resizing anchor point.
+ val left = taskBounds.centerX() - finalSize.width / 2
+ val right = left + finalSize.width
+ val finalBounds = Rect(left, taskBounds.top, right, taskBounds.top + finalSize.height)
+
+ val wct = WindowContainerTransaction().setBounds(task.token, finalBounds)
+ resizeHandler.startTransition(wct)
+ }
+ }
+
+ private fun isDesktopModeShowing(displayId: Int): Boolean =
+ taskRepository.getVisibleTaskCount(displayId) > 0
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
index 336e5e3..0637474 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -22,6 +22,7 @@
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.content.Context
import android.os.IBinder
+import android.os.SystemProperties
import android.os.Trace
import android.util.SparseArray
import android.view.SurfaceControl
@@ -52,8 +53,6 @@
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
-const val VISIBLE_TASKS_COUNTER_NAME = "DESKTOP_MODE_VISIBLE_TASKS"
-
/**
* A [Transitions.TransitionObserver] that observes transitions and the proposed changes to log
* appropriate desktop mode session log events. This observes transitions related to desktop mode
@@ -307,6 +306,8 @@
VISIBLE_TASKS_COUNTER_NAME,
postTransitionVisibleFreeformTasks.size().toLong()
)
+ SystemProperties.set(VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY,
+ postTransitionVisibleFreeformTasks.size().toString())
}
// old tasks that were resized or repositioned
// TODO(b/347935387): Log changes only once they are stable.
@@ -326,6 +327,8 @@
VISIBLE_TASKS_COUNTER_NAME,
postTransitionVisibleFreeformTasks.size().toLong()
)
+ SystemProperties.set(VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY,
+ postTransitionVisibleFreeformTasks.size().toString())
}
}
}
@@ -431,4 +434,12 @@
return this.type == WindowManager.TRANSIT_TO_FRONT &&
this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS
}
+
+ companion object {
+ @VisibleForTesting
+ const val VISIBLE_TASKS_COUNTER_NAME = "desktop_mode_visible_tasks"
+ @VisibleForTesting
+ const val VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY =
+ "debug.tracing." + VISIBLE_TASKS_COUNTER_NAME
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 1f95667..61fc09b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -97,6 +97,7 @@
import com.android.wm.shell.common.MultiInstanceHelper;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
@@ -166,6 +167,8 @@
private TaskOperations mTaskOperations;
private final Supplier<SurfaceControl.Transaction> mTransactionFactory;
private final Transitions mTransitions;
+ private final Optional<DesktopActivityOrientationChangeHandler>
+ mActivityOrientationChangeHandler;
private SplitScreenController mSplitScreenController;
@@ -215,7 +218,8 @@
InteractionJankMonitor interactionJankMonitor,
AppToWebGenericLinksParser genericLinksParser,
MultiInstanceHelper multiInstanceHelper,
- Optional<DesktopTasksLimiter> desktopTasksLimiter
+ Optional<DesktopTasksLimiter> desktopTasksLimiter,
+ Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler
) {
this(
context,
@@ -241,7 +245,8 @@
rootTaskDisplayAreaOrganizer,
new SparseArray<>(),
interactionJankMonitor,
- desktopTasksLimiter);
+ desktopTasksLimiter,
+ activityOrientationChangeHandler);
}
@VisibleForTesting
@@ -269,7 +274,8 @@
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId,
InteractionJankMonitor interactionJankMonitor,
- Optional<DesktopTasksLimiter> desktopTasksLimiter) {
+ Optional<DesktopTasksLimiter> desktopTasksLimiter,
+ Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler) {
mContext = context;
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
@@ -297,6 +303,7 @@
com.android.internal.R.string.config_systemUi);
mInteractionJankMonitor = interactionJankMonitor;
mDesktopTasksLimiter = desktopTasksLimiter;
+ mActivityOrientationChangeHandler = activityOrientationChangeHandler;
mOnDisplayChangingListener = (displayId, fromRotation, toRotation, displayAreaInfo, t) -> {
DesktopModeWindowDecoration decoration;
RunningTaskInfo taskInfo;
@@ -388,6 +395,8 @@
incrementEventReceiverTasks(taskInfo.displayId);
}
decoration.relayout(taskInfo);
+ mActivityOrientationChangeHandler.ifPresent(handler ->
+ handler.handleActivityOrientationChange(oldTaskInfo, taskInfo));
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index deef378..9c73e4a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -121,8 +121,14 @@
/** Closes the maximize window and releases its view. */
fun close() {
- maximizeMenuView?.animateCloseMenu {
- maximizeMenu?.releaseView()
+ val view = maximizeMenuView
+ val menu = maximizeMenu
+ if (view == null) {
+ menu?.releaseView()
+ } else {
+ view.animateCloseMenu {
+ menu?.releaseView()
+ }
}
maximizeMenu = null
maximizeMenuView = null
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
index 3fb67cd..1b885aa 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
@@ -21,6 +21,8 @@
import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways
import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart
import android.tools.flicker.assertors.assertions.AppWindowBecomesVisible
+import android.tools.flicker.assertors.assertions.AppWindowCoversLeftHalfScreenAtEnd
+import android.tools.flicker.assertors.assertions.AppWindowCoversRightHalfScreenAtEnd
import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd
import android.tools.flicker.assertors.assertions.AppWindowHasSizeOfAtLeast
import android.tools.flicker.assertors.assertions.AppWindowIsInvisibleAtEnd
@@ -51,21 +53,21 @@
FlickerConfigEntry(
scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"),
extractor =
- ShellTransitionScenarioExtractor(
- transitionMatcher =
- object : ITransitionMatcher {
- override fun findAll(
- transitions: Collection<Transition>
- ): Collection<Transition> {
- return transitions.filter {
- // TODO(351168217) Use jank CUJ to extract a longer trace
- it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP
- }
- }
+ ShellTransitionScenarioExtractor(
+ transitionMatcher =
+ object : ITransitionMatcher {
+ override fun findAll(
+ transitions: Collection<Transition>
+ ): Collection<Transition> {
+ return transitions.filter {
+ // TODO(351168217) Use jank CUJ to extract a longer trace
+ it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP
}
- ),
+ }
+ }
+ ),
assertions =
- AssertionTemplates.COMMON_ASSERTIONS +
+ AssertionTemplates.COMMON_ASSERTIONS +
listOf(
AppLayerIsVisibleAlways(DESKTOP_MODE_APP),
AppWindowOnTopAtEnd(DESKTOP_MODE_APP),
@@ -81,24 +83,24 @@
FlickerConfigEntry(
scenarioId = ScenarioId("CLOSE_APP"),
extractor =
- ShellTransitionScenarioExtractor(
- transitionMatcher =
- object : ITransitionMatcher {
- override fun findAll(
- transitions: Collection<Transition>
- ): Collection<Transition> {
- // In case there are multiple windows closing, filter out the
- // last window closing. It should use the CLOSE_LAST_APP
- // scenario below.
- return transitions
- .filter { it.type == TransitionType.CLOSE }
- .sortedByDescending { it.id }
- .drop(1)
- }
- }
- ),
+ ShellTransitionScenarioExtractor(
+ transitionMatcher =
+ object : ITransitionMatcher {
+ override fun findAll(
+ transitions: Collection<Transition>
+ ): Collection<Transition> {
+ // In case there are multiple windows closing, filter out the
+ // last window closing. It should use the CLOSE_LAST_APP
+ // scenario below.
+ return transitions
+ .filter { it.type == TransitionType.CLOSE }
+ .sortedByDescending { it.id }
+ .drop(1)
+ }
+ }
+ ),
assertions =
- AssertionTemplates.COMMON_ASSERTIONS +
+ AssertionTemplates.COMMON_ASSERTIONS +
listOf(
AppWindowOnTopAtStart(DESKTOP_MODE_APP),
AppLayerIsVisibleAtStart(DESKTOP_MODE_APP),
@@ -110,22 +112,22 @@
FlickerConfigEntry(
scenarioId = ScenarioId("CLOSE_LAST_APP"),
extractor =
- ShellTransitionScenarioExtractor(
- transitionMatcher =
- object : ITransitionMatcher {
- override fun findAll(
- transitions: Collection<Transition>
- ): Collection<Transition> {
- val lastTransition =
- transitions
- .filter { it.type == TransitionType.CLOSE }
- .maxByOrNull { it.id }!!
- return listOf(lastTransition)
- }
- }
- ),
+ ShellTransitionScenarioExtractor(
+ transitionMatcher =
+ object : ITransitionMatcher {
+ override fun findAll(
+ transitions: Collection<Transition>
+ ): Collection<Transition> {
+ val lastTransition =
+ transitions
+ .filter { it.type == TransitionType.CLOSE }
+ .maxByOrNull { it.id }!!
+ return listOf(lastTransition)
+ }
+ }
+ ),
assertions =
- AssertionTemplates.COMMON_ASSERTIONS +
+ AssertionTemplates.COMMON_ASSERTIONS +
listOf(
AppWindowIsInvisibleAtEnd(DESKTOP_MODE_APP),
LauncherWindowReplacesAppAsTopWindow(DESKTOP_MODE_APP),
@@ -138,12 +140,12 @@
FlickerConfigEntry(
scenarioId = ScenarioId("CORNER_RESIZE"),
extractor =
- TaggedScenarioExtractorBuilder()
- .setTargetTag(CujType.CUJ_DESKTOP_MODE_RESIZE_WINDOW)
- .setTransitionMatcher(
- TaggedCujTransitionMatcher(associatedTransitionRequired = false)
- )
- .build(),
+ TaggedScenarioExtractorBuilder()
+ .setTargetTag(CujType.CUJ_DESKTOP_MODE_RESIZE_WINDOW)
+ .setTransitionMatcher(
+ TaggedCujTransitionMatcher(associatedTransitionRequired = false)
+ )
+ .build(),
assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS
)
@@ -151,18 +153,78 @@
FlickerConfigEntry(
scenarioId = ScenarioId("CORNER_RESIZE_TO_MINIMUM_SIZE"),
extractor =
- TaggedScenarioExtractorBuilder()
- .setTargetTag(CujType.CUJ_DESKTOP_MODE_RESIZE_WINDOW)
- .setTransitionMatcher(
- TaggedCujTransitionMatcher(associatedTransitionRequired = false)
- )
- .build(),
+ TaggedScenarioExtractorBuilder()
+ .setTargetTag(CujType.CUJ_DESKTOP_MODE_RESIZE_WINDOW)
+ .setTransitionMatcher(
+ TaggedCujTransitionMatcher(associatedTransitionRequired = false)
+ )
+ .build(),
assertions =
AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
listOf(AppWindowHasSizeOfAtLeast(DESKTOP_MODE_APP, 770, 700))
.associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
)
+ val SNAP_RESIZE_LEFT_WITH_BUTTON =
+ FlickerConfigEntry(
+ scenarioId = ScenarioId("SNAP_RESIZE_LEFT_WITH_BUTTON"),
+ extractor =
+ TaggedScenarioExtractorBuilder()
+ .setTargetTag(CujType.CUJ_DESKTOP_MODE_SNAP_RESIZE)
+ .setTransitionMatcher(
+ TaggedCujTransitionMatcher(associatedTransitionRequired = false)
+ )
+ .build(),
+ assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
+ listOf(AppWindowCoversLeftHalfScreenAtEnd(DESKTOP_MODE_APP))
+ .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+ )
+
+ val SNAP_RESIZE_RIGHT_WITH_BUTTON =
+ FlickerConfigEntry(
+ scenarioId = ScenarioId("SNAP_RESIZE_RIGHT_WITH_BUTTON"),
+ extractor =
+ TaggedScenarioExtractorBuilder()
+ .setTargetTag(CujType.CUJ_DESKTOP_MODE_SNAP_RESIZE)
+ .setTransitionMatcher(
+ TaggedCujTransitionMatcher(associatedTransitionRequired = false)
+ )
+ .build(),
+ assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
+ listOf(AppWindowCoversRightHalfScreenAtEnd(DESKTOP_MODE_APP))
+ .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+ )
+
+ val SNAP_RESIZE_LEFT_WITH_DRAG =
+ FlickerConfigEntry(
+ scenarioId = ScenarioId("SNAP_RESIZE_LEFT_WITH_DRAG"),
+ extractor =
+ TaggedScenarioExtractorBuilder()
+ .setTargetTag(CujType.CUJ_DESKTOP_MODE_SNAP_RESIZE)
+ .setTransitionMatcher(
+ TaggedCujTransitionMatcher(associatedTransitionRequired = false)
+ )
+ .build(),
+ assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
+ listOf(AppWindowCoversLeftHalfScreenAtEnd(DESKTOP_MODE_APP))
+ .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+ )
+
+ val SNAP_RESIZE_RIGHT_WITH_DRAG =
+ FlickerConfigEntry(
+ scenarioId = ScenarioId("SNAP_RESIZE_RIGHT_WITH_DRAG"),
+ extractor =
+ TaggedScenarioExtractorBuilder()
+ .setTargetTag(CujType.CUJ_DESKTOP_MODE_SNAP_RESIZE)
+ .setTransitionMatcher(
+ TaggedCujTransitionMatcher(associatedTransitionRequired = false)
+ )
+ .build(),
+ assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
+ listOf(AppWindowCoversRightHalfScreenAtEnd(DESKTOP_MODE_APP))
+ .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+ )
+
val SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE =
FlickerConfigEntry(
scenarioId = ScenarioId("SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE"),
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowLeftWithButton.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowLeftWithButton.kt
new file mode 100644
index 0000000..b5090086
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowLeftWithButton.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.SNAP_RESIZE_LEFT_WITH_BUTTON
+import com.android.wm.shell.scenarios.SnapResizeAppWindowWithButton
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Snap resize app window using the Snap Left button from the maximize menu.
+ *
+ * Assert that the app window fills the left half the display after being snap resized.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SnapResizeAppWindowLeftWithButton : SnapResizeAppWindowWithButton(toLeft = true) {
+ @ExpectedScenarios(["SNAP_RESIZE_LEFT_WITH_BUTTON"])
+ @Test
+ override fun snapResizeAppWindowWithButton() = super.snapResizeAppWindowWithButton()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(SNAP_RESIZE_LEFT_WITH_BUTTON)
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowLeftWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowLeftWithDrag.kt
new file mode 100644
index 0000000..a22e760
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowLeftWithDrag.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.SNAP_RESIZE_LEFT_WITH_DRAG
+import com.android.wm.shell.scenarios.SnapResizeAppWindowWithDrag
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Snap resize app window by dragging it to the left edge of the screen.
+ *
+ * Assert that the app window fills the left half the display after being snap resized.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SnapResizeAppWindowLeftWithDrag : SnapResizeAppWindowWithDrag(toLeft = true) {
+ @ExpectedScenarios(["SNAP_RESIZE_LEFT_WITH_DRAG"])
+ @Test
+ override fun snapResizeAppWindowWithDrag() = super.snapResizeAppWindowWithDrag()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(SNAP_RESIZE_LEFT_WITH_DRAG)
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowRightWithButton.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowRightWithButton.kt
new file mode 100644
index 0000000..375a2b8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowRightWithButton.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.SNAP_RESIZE_RIGHT_WITH_BUTTON
+import com.android.wm.shell.scenarios.SnapResizeAppWindowWithButton
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Snap resize app window using the Snap Right button from the maximize menu.
+ *
+ * Assert that the app window fills the right half the display after being snap resized.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SnapResizeAppWindowRightWithButton : SnapResizeAppWindowWithButton(toLeft = false) {
+ @ExpectedScenarios(["SNAP_RESIZE_RIGHT_WITH_BUTTON"])
+ @Test
+ override fun snapResizeAppWindowWithButton() = super.snapResizeAppWindowWithButton()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(SNAP_RESIZE_RIGHT_WITH_BUTTON)
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowRightWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowRightWithDrag.kt
new file mode 100644
index 0000000..4a9daf7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowRightWithDrag.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.SNAP_RESIZE_RIGHT_WITH_DRAG
+import com.android.wm.shell.scenarios.SnapResizeAppWindowWithDrag
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Snap resize app window by dragging it to the right edge of the screen.
+ *
+ * Assert that the app window fills the right half the display after being snap resized.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SnapResizeAppWindowRightWithDrag : SnapResizeAppWindowWithDrag(toLeft = false) {
+ @ExpectedScenarios(["SNAP_RESIZE_RIGHT_WITH_DRAG"])
+ @Test
+ override fun snapResizeAppWindowWithDrag() = super.snapResizeAppWindowWithDrag()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(SNAP_RESIZE_RIGHT_WITH_DRAG)
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
new file mode 100644
index 0000000..b14f163
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.pm.ActivityInfo
+import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
+import android.graphics.Rect
+import android.os.Binder
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.AndroidTestingRunner
+import android.view.Display.DEFAULT_DISPLAY
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.ExtendedMockito.never
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
+import com.android.window.flags.Flags.FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.common.TaskStackListenerImpl
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertTrue
+import kotlin.test.assertNotNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.isNull
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.capture
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
+
+/**
+ * Test class for {@link DesktopActivityOrientationChangeHandler}
+ *
+ * Usage: atest WMShellUnitTests:DesktopActivityOrientationChangeHandlerTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE, FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE)
+class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() {
+ @JvmField @Rule val setFlagsRule = SetFlagsRule()
+
+ @Mock lateinit var testExecutor: ShellExecutor
+ @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer
+ @Mock lateinit var transitions: Transitions
+ @Mock lateinit var resizeTransitionHandler: ToggleResizeDesktopTaskTransitionHandler
+ @Mock lateinit var taskStackListener: TaskStackListenerImpl
+
+ private lateinit var mockitoSession: StaticMockitoSession
+ private lateinit var handler: DesktopActivityOrientationChangeHandler
+ private lateinit var shellInit: ShellInit
+ private lateinit var taskRepository: DesktopModeTaskRepository
+ // Mock running tasks are registered here so we can get the list from mock shell task organizer.
+ private val runningTasks = mutableListOf<RunningTaskInfo>()
+
+ @Before
+ fun setUp() {
+ mockitoSession =
+ mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .spyStatic(DesktopModeStatus::class.java)
+ .startMocking()
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+
+ shellInit = spy(ShellInit(testExecutor))
+ taskRepository = DesktopModeTaskRepository()
+ whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
+ whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
+
+ handler = DesktopActivityOrientationChangeHandler(context, shellInit, shellTaskOrganizer,
+ taskStackListener, resizeTransitionHandler, taskRepository)
+
+ shellInit.init()
+ }
+
+ @After
+ fun tearDown() {
+ mockitoSession.finishMocking()
+
+ runningTasks.clear()
+ }
+
+ @Test
+ fun instantiate_addInitCallback() {
+ verify(shellInit).addInitCallback(any(), any<DesktopActivityOrientationChangeHandler>())
+ }
+
+ @Test
+ fun instantiate_cannotEnterDesktopMode_doNotAddInitCallback() {
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false)
+ clearInvocations(shellInit)
+
+ handler = DesktopActivityOrientationChangeHandler(context, shellInit, shellTaskOrganizer,
+ taskStackListener, resizeTransitionHandler, taskRepository)
+
+ verify(shellInit, never()).addInitCallback(any(),
+ any<DesktopActivityOrientationChangeHandler>())
+ }
+
+ @Test
+ fun handleActivityOrientationChange_resizeable_doNothing() {
+ val task = setUpFreeformTask()
+
+ taskStackListener.onActivityRequestedOrientationChanged(task.taskId,
+ SCREEN_ORIENTATION_LANDSCAPE)
+
+ verify(resizeTransitionHandler, never()).startTransition(any(), any())
+ }
+
+ @Test
+ fun handleActivityOrientationChange_nonResizeableFullscreen_doNothing() {
+ val task = createFullscreenTask()
+ task.isResizeable = false
+ val activityInfo = ActivityInfo()
+ activityInfo.screenOrientation = SCREEN_ORIENTATION_PORTRAIT
+ task.topActivityInfo = activityInfo
+ whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ taskRepository.addActiveTask(DEFAULT_DISPLAY, task.taskId)
+ taskRepository.updateTaskVisibility(DEFAULT_DISPLAY, task.taskId, visible = true)
+ runningTasks.add(task)
+
+ taskStackListener.onActivityRequestedOrientationChanged(task.taskId,
+ SCREEN_ORIENTATION_LANDSCAPE)
+
+ verify(resizeTransitionHandler, never()).startTransition(any(), any())
+ }
+
+ @Test
+ fun handleActivityOrientationChange_nonResizeablePortrait_requestSameOrientation_doNothing() {
+ val task = setUpFreeformTask(isResizeable = false)
+ val newTask = setUpFreeformTask(isResizeable = false,
+ orientation = SCREEN_ORIENTATION_SENSOR_PORTRAIT)
+
+ handler.handleActivityOrientationChange(task, newTask)
+
+ verify(resizeTransitionHandler, never()).startTransition(any(), any())
+ }
+
+ @Test
+ fun handleActivityOrientationChange_notInDesktopMode_doNothing() {
+ val task = setUpFreeformTask(isResizeable = false)
+ taskRepository.updateTaskVisibility(task.displayId, task.taskId, visible = false)
+
+ taskStackListener.onActivityRequestedOrientationChanged(task.taskId,
+ SCREEN_ORIENTATION_LANDSCAPE)
+
+ verify(resizeTransitionHandler, never()).startTransition(any(), any())
+ }
+
+ @Test
+ fun handleActivityOrientationChange_nonResizeablePortrait_respectLandscapeRequest() {
+ val task = setUpFreeformTask(isResizeable = false)
+ val oldBounds = task.configuration.windowConfiguration.bounds
+ val newTask = setUpFreeformTask(isResizeable = false,
+ orientation = SCREEN_ORIENTATION_LANDSCAPE)
+
+ handler.handleActivityOrientationChange(task, newTask)
+
+ val wct = getLatestResizeDesktopTaskWct()
+ val finalBounds = findBoundsChange(wct, newTask)
+ assertNotNull(finalBounds)
+ val finalWidth = finalBounds.width()
+ val finalHeight = finalBounds.height()
+ // Bounds is landscape.
+ assertTrue(finalWidth > finalHeight)
+ // Aspect ratio remains the same.
+ assertEquals(oldBounds.height() / oldBounds.width(), finalWidth / finalHeight)
+ // Anchor point for resizing is at the center.
+ assertEquals(oldBounds.centerX(), finalBounds.centerX())
+ }
+
+ @Test
+ fun handleActivityOrientationChange_nonResizeableLandscape_respectPortraitRequest() {
+ val oldBounds = Rect(0, 0, 500, 200)
+ val task = setUpFreeformTask(
+ isResizeable = false, orientation = SCREEN_ORIENTATION_LANDSCAPE, bounds = oldBounds
+ )
+ val newTask = setUpFreeformTask(isResizeable = false, bounds = oldBounds)
+
+ handler.handleActivityOrientationChange(task, newTask)
+
+ val wct = getLatestResizeDesktopTaskWct()
+ val finalBounds = findBoundsChange(wct, newTask)
+ assertNotNull(finalBounds)
+ val finalWidth = finalBounds.width()
+ val finalHeight = finalBounds.height()
+ // Bounds is portrait.
+ assertTrue(finalHeight > finalWidth)
+ // Aspect ratio remains the same.
+ assertEquals(oldBounds.width() / oldBounds.height(), finalHeight / finalWidth)
+ // Anchor point for resizing is at the center.
+ assertEquals(oldBounds.centerX(), finalBounds.centerX())
+ }
+
+ private fun setUpFreeformTask(
+ displayId: Int = DEFAULT_DISPLAY,
+ isResizeable: Boolean = true,
+ orientation: Int = SCREEN_ORIENTATION_PORTRAIT,
+ bounds: Rect? = Rect(0, 0, 200, 500)
+ ): RunningTaskInfo {
+ val task = createFreeformTask(displayId, bounds)
+ val activityInfo = ActivityInfo()
+ activityInfo.screenOrientation = orientation
+ task.topActivityInfo = activityInfo
+ task.isResizeable = isResizeable
+ whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ taskRepository.addActiveTask(displayId, task.taskId)
+ taskRepository.updateTaskVisibility(displayId, task.taskId, visible = true)
+ taskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId)
+ runningTasks.add(task)
+ return task
+ }
+
+ private fun getLatestResizeDesktopTaskWct(
+ currentBounds: Rect? = null
+ ): WindowContainerTransaction {
+ val arg: ArgumentCaptor<WindowContainerTransaction> =
+ ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(resizeTransitionHandler, atLeastOnce())
+ .startTransition(capture(arg), eq(currentBounds))
+ return arg.value
+ }
+
+ private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? =
+ wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
index e49eb36..d399b20 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
@@ -22,6 +22,8 @@
import android.graphics.Point
import android.graphics.Rect
import android.os.IBinder
+import android.os.SystemProperties
+import android.os.Trace
import android.testing.AndroidTestingRunner
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CHANGE
@@ -38,6 +40,7 @@
import android.window.TransitionInfo.Change
import android.window.WindowContainerToken
import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.modules.utils.testing.ExtendedMockitoRule
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.ShellExecutor
@@ -86,7 +89,11 @@
@JvmField
@Rule
val extendedMockitoRule =
- ExtendedMockitoRule.Builder(this).mockStatic(DesktopModeStatus::class.java).build()!!
+ ExtendedMockitoRule.Builder(this)
+ .mockStatic(DesktopModeStatus::class.java)
+ .mockStatic(SystemProperties::class.java)
+ .mockStatic(Trace::class.java)
+ .build()!!
private val testExecutor = mock<ShellExecutor>()
private val mockShellInit = mock<ShellInit>()
@@ -695,6 +702,17 @@
assertNotNull(sessionId)
verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), eq(enterReason))
verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), eq(taskUpdate))
+ ExtendedMockito.verify {
+ Trace.setCounter(
+ eq(Trace.TRACE_TAG_WINDOW_MANAGER),
+ eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_NAME),
+ eq(taskUpdate.visibleTaskCount.toLong()))
+ }
+ ExtendedMockito.verify {
+ SystemProperties.set(
+ eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY),
+ eq(taskUpdate.visibleTaskCount.toString()))
+ }
verifyZeroInteractions(desktopModeEventLogger)
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index da0aca7b..1ea8e8a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -79,6 +79,7 @@
import com.android.wm.shell.common.MultiInstanceHelper
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler
import com.android.wm.shell.desktopmode.DesktopTasksController
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
import com.android.wm.shell.desktopmode.DesktopTasksLimiter
@@ -167,6 +168,8 @@
@Mock private lateinit var mockMultiInstanceHelper: MultiInstanceHelper
@Mock private lateinit var mockTasksLimiter: DesktopTasksLimiter
@Mock private lateinit var mockFreeformTaskTransitionStarter: FreeformTaskTransitionStarter
+ @Mock private lateinit var mockActivityOrientationChangeHandler:
+ DesktopActivityOrientationChangeHandler
private lateinit var spyContext: TestableContext
private val transactionFactory = Supplier<SurfaceControl.Transaction> {
@@ -220,7 +223,8 @@
mockRootTaskDisplayAreaOrganizer,
windowDecorByTaskIdSpy,
mockInteractionJankMonitor,
- Optional.of(mockTasksLimiter)
+ Optional.of(mockTasksLimiter),
+ Optional.of(mockActivityOrientationChangeHandler)
)
desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExt.kt b/packages/SettingsLib/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExt.kt
deleted file mode 100644
index 02d684d..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExt.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.media.data.repository
-
-import android.media.AudioManager
-import android.media.IVolumeController
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.buffer
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.launch
-
-/** Returns [AudioManager.setVolumeController] events as a [Flow] */
-fun AudioManager.volumeControllerEvents(): Flow<VolumeControllerEvent> =
- callbackFlow {
- volumeController =
- object : IVolumeController.Stub() {
- override fun displaySafeVolumeWarning(flags: Int) {
- launch { send(VolumeControllerEvent.DisplaySafeVolumeWarning(flags)) }
- }
-
- override fun volumeChanged(streamType: Int, flags: Int) {
- launch { send(VolumeControllerEvent.VolumeChanged(streamType, flags)) }
- }
-
- override fun masterMuteChanged(flags: Int) {
- launch { send(VolumeControllerEvent.MasterMuteChanged(flags)) }
- }
-
- override fun setLayoutDirection(layoutDirection: Int) {
- launch { send(VolumeControllerEvent.SetLayoutDirection(layoutDirection)) }
- }
-
- override fun dismiss() {
- launch { send(VolumeControllerEvent.Dismiss) }
- }
-
- override fun setA11yMode(mode: Int) {
- launch { send(VolumeControllerEvent.SetA11yMode(mode)) }
- }
-
- override fun displayCsdWarning(
- csdWarning: Int,
- displayDurationMs: Int,
- ) {
- launch {
- send(
- VolumeControllerEvent.DisplayCsdWarning(
- csdWarning,
- displayDurationMs,
- )
- )
- }
- }
- }
- awaitClose { volumeController = null }
- }
- .buffer()
-
-/** Models events received via [IVolumeController] */
-sealed interface VolumeControllerEvent {
-
- /** @see [IVolumeController.displaySafeVolumeWarning] */
- data class DisplaySafeVolumeWarning(val flags: Int) : VolumeControllerEvent
-
- /** @see [IVolumeController.volumeChanged] */
- data class VolumeChanged(val streamType: Int, val flags: Int) : VolumeControllerEvent
-
- /** @see [IVolumeController.masterMuteChanged] */
- data class MasterMuteChanged(val flags: Int) : VolumeControllerEvent
-
- /** @see [IVolumeController.setLayoutDirection] */
- data class SetLayoutDirection(val layoutDirection: Int) : VolumeControllerEvent
-
- /** @see [IVolumeController.setA11yMode] */
- data class SetA11yMode(val mode: Int) : VolumeControllerEvent
-
- /** @see [IVolumeController.displayCsdWarning] */
- data class DisplayCsdWarning(
- val csdWarning: Int,
- val displayDurationMs: Int,
- ) : VolumeControllerEvent
-
- /** @see [IVolumeController.dismiss] */
- data object Dismiss : VolumeControllerEvent
-}
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/model/VolumeControllerEvent.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/model/VolumeControllerEvent.kt
new file mode 100644
index 0000000..0fe385b
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/model/VolumeControllerEvent.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.volume.data.model
+
+import android.media.IVolumeController
+
+/** Models events received via [IVolumeController] */
+sealed interface VolumeControllerEvent {
+
+ /** @see [IVolumeController.displaySafeVolumeWarning] */
+ data class DisplaySafeVolumeWarning(val flags: Int) : VolumeControllerEvent
+
+ /** @see [IVolumeController.volumeChanged] */
+ data class VolumeChanged(val streamType: Int, val flags: Int) : VolumeControllerEvent
+
+ /** @see [IVolumeController.masterMuteChanged] */
+ data class MasterMuteChanged(val flags: Int) : VolumeControllerEvent
+
+ /** @see [IVolumeController.setLayoutDirection] */
+ data class SetLayoutDirection(val layoutDirection: Int) : VolumeControllerEvent
+
+ /** @see [IVolumeController.setA11yMode] */
+ data class SetA11yMode(val mode: Int) : VolumeControllerEvent
+
+ /** @see [IVolumeController.displayCsdWarning] */
+ data class DisplayCsdWarning(
+ val csdWarning: Int,
+ val displayDurationMs: Int,
+ ) : VolumeControllerEvent
+
+ /** @see [IVolumeController.dismiss] */
+ data object Dismiss : VolumeControllerEvent
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
index 0e71116..3e2d832 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
@@ -22,9 +22,12 @@
import android.media.AudioManager
import android.media.AudioManager.AudioDeviceCategory
import android.media.AudioManager.OnCommunicationDeviceChangedListener
+import android.media.IVolumeController
import android.provider.Settings
+import android.util.Log
import androidx.concurrent.futures.DirectExecutor
import com.android.internal.util.ConcurrentUtils
+import com.android.settingslib.volume.data.model.VolumeControllerEvent
import com.android.settingslib.volume.shared.AudioLogger
import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
import com.android.settingslib.volume.shared.model.AudioManagerEvent
@@ -36,10 +39,13 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
@@ -73,6 +79,11 @@
*/
val communicationDevice: StateFlow<AudioDeviceInfo?>
+ /** Events from [AudioManager.setVolumeController] */
+ val volumeControllerEvents: Flow<VolumeControllerEvent>
+
+ fun init()
+
/** State of the [AudioStream]. */
fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel>
@@ -90,8 +101,9 @@
suspend fun setRingerMode(audioStream: AudioStream, mode: RingerMode)
/** Gets audio device category. */
- @AudioDeviceCategory
- suspend fun getBluetoothAudioDeviceCategory(bluetoothAddress: String): Int
+ @AudioDeviceCategory suspend fun getBluetoothAudioDeviceCategory(bluetoothAddress: String): Int
+
+ suspend fun notifyVolumeControllerVisible(isVisible: Boolean)
}
class AudioRepositoryImpl(
@@ -101,8 +113,10 @@
private val backgroundCoroutineContext: CoroutineContext,
private val coroutineScope: CoroutineScope,
private val logger: AudioLogger,
+ shouldUseVolumeController: Boolean,
) : AudioRepository {
+ private val volumeController = ProducingVolumeController()
private val streamSettingNames: Map<AudioStream, String> =
mapOf(
AudioStream(AudioManager.STREAM_VOICE_CALL) to Settings.System.VOLUME_VOICE,
@@ -116,12 +130,19 @@
AudioStream(AudioManager.STREAM_ASSISTANT) to Settings.System.VOLUME_ASSISTANT,
)
+ override val volumeControllerEvents: Flow<VolumeControllerEvent> =
+ if (shouldUseVolumeController) {
+ volumeController.events
+ } else {
+ emptyFlow()
+ }
+
override val mode: StateFlow<Int> =
callbackFlow {
- val listener = AudioManager.OnModeChangedListener { newMode -> trySend(newMode) }
- audioManager.addOnModeChangedListener(ConcurrentUtils.DIRECT_EXECUTOR, listener)
- awaitClose { audioManager.removeOnModeChangedListener(listener) }
- }
+ val listener = AudioManager.OnModeChangedListener { newMode -> trySend(newMode) }
+ audioManager.addOnModeChangedListener(ConcurrentUtils.DIRECT_EXECUTOR, listener)
+ awaitClose { audioManager.removeOnModeChangedListener(listener) }
+ }
.onStart { emit(audioManager.mode) }
.flowOn(backgroundCoroutineContext)
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), audioManager.mode)
@@ -141,14 +162,14 @@
override val communicationDevice: StateFlow<AudioDeviceInfo?>
get() =
callbackFlow {
- val listener = OnCommunicationDeviceChangedListener { trySend(Unit) }
- audioManager.addOnCommunicationDeviceChangedListener(
- ConcurrentUtils.DIRECT_EXECUTOR,
- listener,
- )
+ val listener = OnCommunicationDeviceChangedListener { trySend(Unit) }
+ audioManager.addOnCommunicationDeviceChangedListener(
+ ConcurrentUtils.DIRECT_EXECUTOR,
+ listener,
+ )
- awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) }
- }
+ awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) }
+ }
.filterNotNull()
.map { audioManager.communicationDevice }
.onStart { emit(audioManager.communicationDevice) }
@@ -159,20 +180,30 @@
audioManager.communicationDevice,
)
+ override fun init() {
+ try {
+ audioManager.volumeController = volumeController
+ } catch (error: SecurityException) {
+ Log.wtf("AudioManager", "Unable to set the volume controller", error)
+ }
+ }
+
override fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> {
return merge(
- audioManagerEventsReceiver.events.filter {
- if (it is StreamAudioManagerEvent) {
- it.audioStream == audioStream
- } else {
- true
- }
- },
- volumeSettingChanges(audioStream),
- )
+ audioManagerEventsReceiver.events.filter {
+ if (it is StreamAudioManagerEvent) {
+ it.audioStream == audioStream
+ } else {
+ true
+ }
+ },
+ volumeSettingChanges(audioStream),
+ volumeControllerEvents.filter { it is VolumeControllerEvent.VolumeChanged },
+ )
.conflate()
.map { getCurrentAudioStream(audioStream) }
.onStart { emit(getCurrentAudioStream(audioStream)) }
+ .distinctUntilChanged()
.onEach { logger.onVolumeUpdateReceived(audioStream, it) }
.flowOn(backgroundCoroutineContext)
}
@@ -228,6 +259,12 @@
}
}
+ override suspend fun notifyVolumeControllerVisible(isVisible: Boolean) {
+ withContext(backgroundCoroutineContext) {
+ audioManager.notifyVolumeControllerVisible(volumeController, isVisible)
+ }
+ }
+
private fun getMinVolume(stream: AudioStream): Int =
try {
audioManager.getStreamMinVolume(stream.value)
@@ -253,3 +290,45 @@
}
}
}
+
+private class ProducingVolumeController : IVolumeController.Stub() {
+
+ private val mutableEvents = MutableSharedFlow<VolumeControllerEvent>(extraBufferCapacity = 32)
+ val events = mutableEvents.asSharedFlow()
+
+ override fun displaySafeVolumeWarning(flags: Int) {
+ mutableEvents.tryEmit(VolumeControllerEvent.DisplaySafeVolumeWarning(flags))
+ }
+
+ override fun volumeChanged(streamType: Int, flags: Int) {
+ mutableEvents.tryEmit(VolumeControllerEvent.VolumeChanged(streamType, flags))
+ }
+
+ override fun masterMuteChanged(flags: Int) {
+ mutableEvents.tryEmit(VolumeControllerEvent.MasterMuteChanged(flags))
+ }
+
+ override fun setLayoutDirection(layoutDirection: Int) {
+ mutableEvents.tryEmit(VolumeControllerEvent.SetLayoutDirection(layoutDirection))
+ }
+
+ override fun dismiss() {
+ mutableEvents.tryEmit(VolumeControllerEvent.Dismiss)
+ }
+
+ override fun setA11yMode(mode: Int) {
+ mutableEvents.tryEmit(VolumeControllerEvent.SetA11yMode(mode))
+ }
+
+ override fun displayCsdWarning(
+ csdWarning: Int,
+ displayDurationMs: Int,
+ ) {
+ mutableEvents.tryEmit(
+ VolumeControllerEvent.DisplayCsdWarning(
+ csdWarning,
+ displayDurationMs,
+ )
+ )
+ }
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
index 0e43acb..52e6391 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
@@ -44,6 +44,7 @@
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@@ -111,6 +112,7 @@
testScope.testScheduler,
testScope.backgroundScope,
logger,
+ true,
)
}
@@ -261,8 +263,8 @@
@Test
fun getBluetoothAudioDeviceCategory() {
testScope.runTest {
- `when`(audioManager.getBluetoothAudioDeviceCategory("12:34:56:78")).thenReturn(
- AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES)
+ `when`(audioManager.getBluetoothAudioDeviceCategory("12:34:56:78"))
+ .thenReturn(AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES)
val category = underTest.getBluetoothAudioDeviceCategory("12:34:56:78")
runCurrent()
@@ -271,6 +273,27 @@
}
}
+ @Test
+ fun useVolumeControllerDisabled_setVolumeController_notCalled() {
+ testScope.runTest {
+ underTest =
+ AudioRepositoryImpl(
+ eventsReceiver,
+ audioManager,
+ contentResolver,
+ testScope.testScheduler,
+ testScope.backgroundScope,
+ logger,
+ false,
+ )
+
+ underTest.volumeControllerEvents.launchIn(backgroundScope)
+ runCurrent()
+
+ verify(audioManager, never()).volumeController = any()
+ }
+ }
+
private fun triggerConnectedDeviceChange(communicationDevice: AudioDeviceInfo?) {
verify(audioManager)
.addOnCommunicationDeviceChangedListener(
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExtTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryVolumeControllerEventsTest.kt
similarity index 76%
rename from packages/SettingsLib/tests/integ/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExtTest.kt
rename to packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryVolumeControllerEventsTest.kt
index 83b612d..f5c2f01 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExtTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryVolumeControllerEventsTest.kt
@@ -14,12 +14,15 @@
* limitations under the License.
*/
-package com.android.settingslib.media.data.repository
+package com.android.settingslib.volume.data.repository
+import android.content.ContentResolver
import android.media.AudioManager
import android.media.IVolumeController
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.settingslib.volume.data.model.VolumeControllerEvent
+import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
@@ -39,16 +42,32 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
-class AudioManagerVolumeControllerExtTest {
+class AudioRepositoryVolumeControllerEventsTest {
private val testScope = TestScope()
@Captor private lateinit var volumeControllerCaptor: ArgumentCaptor<IVolumeController>
@Mock private lateinit var audioManager: AudioManager
+ @Mock private lateinit var contentResolver: ContentResolver
+
+ private val logger = FakeAudioRepositoryLogger()
+ private val eventsReceiver = FakeAudioManagerEventsReceiver()
+
+ private lateinit var underTest: AudioRepository
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
+ underTest =
+ AudioRepositoryImpl(
+ eventsReceiver,
+ audioManager,
+ contentResolver,
+ testScope.testScheduler,
+ testScope.backgroundScope,
+ logger,
+ true,
+ )
}
@Test
@@ -83,7 +102,7 @@
) =
testScope.runTest {
var event: VolumeControllerEvent? = null
- audioManager.volumeControllerEvents().onEach { event = it }.launchIn(backgroundScope)
+ underTest.volumeControllerEvents.onEach { event = it }.launchIn(backgroundScope)
runCurrent()
verify(audioManager).volumeController = volumeControllerCaptor.capture()
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index f98b29a..d394976 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -370,6 +370,9 @@
<uses-permission android:name="android.permission.MONITOR_STICKY_MODIFIER_STATE" />
+ <!-- Listen to keyboard shortcut events from input manager -->
+ <uses-permission android:name="android.permission.MANAGE_KEY_GESTURES" />
+
<!-- To follow the grammatical gender preference -->
<uses-permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" />
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
index 808e666..5f7b1ad 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
@@ -35,6 +35,7 @@
import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.res.R
import com.android.systemui.util.animation.MeasurementInput
+import kotlinx.coroutines.ExperimentalCoroutinesApi
object MediaCarousel {
object Elements {
@@ -46,6 +47,7 @@
}
}
+@ExperimentalCoroutinesApi
@Composable
fun SceneScope.MediaCarousel(
isVisible: Boolean,
@@ -54,7 +56,7 @@
carouselController: MediaCarouselController,
offsetProvider: (() -> IntOffset)? = null,
) {
- if (!isVisible) {
+ if (!isVisible || carouselController.isLockedAndHidden()) {
return
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
index 3aed79f..ca15eff 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
@@ -17,6 +17,9 @@
package com.android.systemui.education.domain.interactor
import android.content.pm.UserInfo
+import android.hardware.input.InputManager
+import android.hardware.input.KeyGestureEvent
+import android.view.KeyEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -41,6 +44,9 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.kotlin.any
+import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -203,6 +209,31 @@
assertThat(model.keyboardFirstConnectionTime).isEqualTo(newUserFirstConnectionTime)
}
+ @Test
+ fun updateShortcutTimeOnKeyboardShortcutTriggered() =
+ testScope.runTest {
+ // runCurrent() to trigger inputManager#registerKeyGestureEventListener in the
+ // interactor
+ runCurrent()
+ val listenerCaptor =
+ ArgumentCaptor.forClass(InputManager.KeyGestureEventListener::class.java)
+ verify(kosmos.mockEduInputManager)
+ .registerKeyGestureEventListener(any(), listenerCaptor.capture())
+
+ val backGestureEvent =
+ KeyGestureEvent(
+ /* deviceId= */ 1,
+ intArrayOf(KeyEvent.KEYCODE_ESCAPE),
+ KeyEvent.META_META_ON,
+ KeyGestureEvent.KEY_GESTURE_TYPE_BACK
+ )
+ listenerCaptor.value.onKeyGestureEvent(backGestureEvent)
+
+ val model by
+ collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK))
+ assertThat(model?.lastShortcutTriggeredTime).isEqualTo(eduClock.instant())
+ }
+
private suspend fun triggerMaxEducationSignals(gestureType: GestureType) {
// Increment max number of signal to try triggering education
for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
index e88349b2..87eeebf 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
@@ -16,8 +16,16 @@
package com.android.systemui.education.domain.interactor
+import android.hardware.input.InputManager
+import android.hardware.input.InputManager.KeyGestureEventListener
+import android.hardware.input.KeyGestureEvent
import com.android.systemui.CoreStartable
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.contextualeducation.GestureType
+import com.android.systemui.contextualeducation.GestureType.ALL_APPS
import com.android.systemui.contextualeducation.GestureType.BACK
+import com.android.systemui.contextualeducation.GestureType.HOME
+import com.android.systemui.contextualeducation.GestureType.OVERVIEW
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.education.dagger.ContextualEducationModule.EduClock
@@ -25,10 +33,14 @@
import com.android.systemui.education.shared.model.EducationInfo
import com.android.systemui.education.shared.model.EducationUiType
import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import java.time.Clock
+import java.util.concurrent.Executor
import javax.inject.Inject
import kotlin.time.Duration.Companion.hours
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@@ -41,10 +53,12 @@
@Background private val backgroundScope: CoroutineScope,
private val contextualEducationInteractor: ContextualEducationInteractor,
private val userInputDeviceRepository: UserInputDeviceRepository,
+ private val inputManager: InputManager,
@EduClock private val clock: Clock,
) : CoreStartable {
companion object {
+ const val TAG = "KeyboardTouchpadEduInteractor"
const val MAX_SIGNAL_COUNT: Int = 2
val usageSessionDuration = 72.hours
}
@@ -52,6 +66,26 @@
private val _educationTriggered = MutableStateFlow<EducationInfo?>(null)
val educationTriggered = _educationTriggered.asStateFlow()
+ private val keyboardShortcutTriggered: Flow<GestureType> = conflatedCallbackFlow {
+ val listener = KeyGestureEventListener { event ->
+ val shortcutType =
+ when (event.keyGestureType) {
+ KeyGestureEvent.KEY_GESTURE_TYPE_BACK -> BACK
+ KeyGestureEvent.KEY_GESTURE_TYPE_HOME -> HOME
+ KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS -> OVERVIEW
+ KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS -> ALL_APPS
+ else -> null
+ }
+
+ if (shortcutType != null) {
+ trySendWithFailureLogging(shortcutType, TAG)
+ }
+ }
+
+ inputManager.registerKeyGestureEventListener(Executor(Runnable::run), listener)
+ awaitClose { inputManager.unregisterKeyGestureEventListener(listener) }
+ }
+
override fun start() {
backgroundScope.launch {
contextualEducationInteractor.backGestureModelFlow.collect {
@@ -89,6 +123,12 @@
}
}
}
+
+ backgroundScope.launch {
+ keyboardShortcutTriggered.collect {
+ contextualEducationInteractor.updateShortcutTriggerTime(it)
+ }
+ }
}
private fun isEducationNeeded(model: GestureEduModel): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
index 7b0b23f..b5d9e2a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
@@ -19,6 +19,7 @@
import android.graphics.Color
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
@@ -41,6 +42,7 @@
keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val dismissCallbackRegistry: DismissCallbackRegistry,
alternateBouncerInteractor: Lazy<AlternateBouncerInteractor>,
+ private val primaryBouncerInteractor: PrimaryBouncerInteractor,
) {
// When we're fully transitioned to the AlternateBouncer, the alpha of the scrim should be:
private val alternateBouncerScrimAlpha = .66f
@@ -73,5 +75,6 @@
fun onBackRequested() {
statusBarKeyguardViewManager.hideAlternateBouncer(false)
dismissCallbackRegistry.notifyDismissCancelled()
+ primaryBouncerInteractor.setDismissAction(null, null)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
index 19cdee7..8fd578e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
@@ -45,6 +45,7 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.Edge
@@ -75,7 +76,6 @@
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.PageIndicator
import com.android.systemui.res.R
-import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shared.system.SysUiStatsLog
@@ -103,6 +103,7 @@
import javax.inject.Provider
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -121,6 +122,7 @@
* Class that is responsible for keeping the view carousel up to date. This also handles changes in
* state and applies them to the media carousel like the expansion.
*/
+@ExperimentalCoroutinesApi
@SysUISingleton
class MediaCarouselController
@Inject
@@ -149,7 +151,7 @@
private val secureSettings: SecureSettings,
private val mediaCarouselViewModel: MediaCarouselViewModel,
private val mediaViewControllerFactory: Provider<MediaViewController>,
- private val sceneInteractor: SceneInteractor,
+ private val deviceEntryInteractor: DeviceEntryInteractor,
) : Dumpable {
/** The current width of the carousel */
var currentCarouselWidth: Int = 0
@@ -904,9 +906,15 @@
/** Return true if the carousel should be hidden because lockscreen is currently visible */
fun isLockedAndHidden(): Boolean {
- val keyguardState = keyguardTransitionInteractor.getFinishedState()
- return !allowMediaPlayerOnLockScreen &&
- KeyguardState.lockscreenVisibleInState(keyguardState)
+ val isOnLockscreen =
+ if (SceneContainerFlag.isEnabled) {
+ !deviceEntryInteractor.isDeviceEntered.value
+ } else {
+ KeyguardState.lockscreenVisibleInState(
+ keyguardTransitionInteractor.getFinishedState()
+ )
+ }
+ return !allowMediaPlayerOnLockScreen && isOnLockscreen
}
private fun reorderAllPlayers(
diff --git a/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java b/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java
index 05f19ef..cc470a6 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java
@@ -67,7 +67,7 @@
@Background CoroutineDispatcher backgroundDispatcher,
@Background Handler handler
) {
- int startingUser = ActivityManager.getCurrentUser();
+ int startingUser = userManager.getBootUser().getIdentifier();
UserTrackerImpl tracker = new UserTrackerImpl(context, featureFlagsProvider, userManager,
iActivityManager, dumpManager, appScope, backgroundDispatcher, handler);
tracker.initialize(startingUser);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index f11fd7b..09b6b68 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -68,6 +68,8 @@
import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor;
import com.android.systemui.dock.DockManager;
import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyguard.DismissCallbackRegistry;
import com.android.systemui.keyguard.KeyguardWmStateRefactor;
import com.android.systemui.keyguard.domain.interactor.KeyguardDismissActionInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardSurfaceBehindInteractor;
@@ -170,6 +172,7 @@
private final Lazy<ShadeController> mShadeController;
private final Lazy<SceneInteractor> mSceneInteractorLazy;
private final Lazy<DeviceEntryInteractor> mDeviceEntryInteractorLazy;
+ private final DismissCallbackRegistry mDismissCallbackRegistry;
private Job mListenForAlternateBouncerTransitionSteps = null;
private Job mListenForKeyguardAuthenticatedBiometricsHandled = null;
@@ -400,7 +403,8 @@
Lazy<SceneInteractor> sceneInteractorLazy,
StatusBarKeyguardViewManagerInteractor statusBarKeyguardViewManagerInteractor,
@Main DelayableExecutor executor,
- Lazy<DeviceEntryInteractor> deviceEntryInteractorLazy
+ Lazy<DeviceEntryInteractor> deviceEntryInteractorLazy,
+ DismissCallbackRegistry dismissCallbackRegistry
) {
mContext = context;
mExecutor = executor;
@@ -437,6 +441,7 @@
mSceneInteractorLazy = sceneInteractorLazy;
mStatusBarKeyguardViewManagerInteractor = statusBarKeyguardViewManagerInteractor;
mDeviceEntryInteractorLazy = deviceEntryInteractorLazy;
+ mDismissCallbackRegistry = dismissCallbackRegistry;
}
KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@@ -994,6 +999,8 @@
}
if (!SceneContainerFlag.isEnabled() && hideBouncerWhenShowing) {
hideAlternateBouncer(true);
+ mDismissCallbackRegistry.notifyDismissCancelled();
+ mPrimaryBouncerInteractor.setDismissAction(null, null);
}
mKeyguardUpdateManager.sendKeyguardReset();
updateStates();
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeControllerCollector.kt b/packages/SystemUI/src/com/android/systemui/volume/VolumeControllerAdapter.kt
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/volume/VolumeControllerCollector.kt
rename to packages/SystemUI/src/com/android/systemui/volume/VolumeControllerAdapter.kt
index 6859191..e836731 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeControllerCollector.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeControllerAdapter.kt
@@ -17,7 +17,8 @@
package com.android.systemui.volume
import android.media.IVolumeController
-import com.android.settingslib.media.data.repository.VolumeControllerEvent
+import com.android.settingslib.volume.data.model.VolumeControllerEvent
+import com.android.settingslib.volume.data.repository.AudioRepository
import com.android.systemui.dagger.qualifiers.Application
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -29,17 +30,17 @@
* [com.android.settingslib.volume.data.repository.AudioRepository.volumeControllerEvents] and the
* old code that uses [IVolumeController] interface directly.
*/
-class VolumeControllerCollector
+class VolumeControllerAdapter
@Inject
-constructor(@Application private val coroutineScope: CoroutineScope) {
+constructor(
+ @Application private val coroutineScope: CoroutineScope,
+ private val audioRepository: AudioRepository,
+) {
/** Collects [Flow] of [VolumeControllerEvent] into [IVolumeController]. */
- fun collectToController(
- eventsFlow: Flow<VolumeControllerEvent>,
- controller: IVolumeController
- ) =
+ fun collectToController(controller: IVolumeController) {
coroutineScope.launch {
- eventsFlow.collect { event ->
+ audioRepository.volumeControllerEvents.collect { event ->
when (event) {
is VolumeControllerEvent.VolumeChanged ->
controller.volumeChanged(event.streamType, event.flags)
@@ -56,4 +57,9 @@
}
}
}
+ }
+
+ fun notifyVolumeControllerVisible(isVisible: Boolean) {
+ coroutineScope.launch { audioRepository.notifyVolumeControllerVisible(isVisible) }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index 1522cc4..d3e8bd3 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -68,6 +68,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.settingslib.volume.MediaSessions;
import com.android.systemui.Dumpable;
+import com.android.systemui.Flags;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
@@ -153,6 +154,7 @@
private final KeyguardManager mKeyguardManager;
private final ActivityManager mActivityManager;
private final UserTracker mUserTracker;
+ private final VolumeControllerAdapter mVolumeControllerAdapter;
protected C mCallbacks = new C();
private final State mState = new State();
protected final MediaSessionsCallbacks mMediaSessionsCallbacksW;
@@ -197,6 +199,7 @@
NotificationManager notificationManager,
VibratorHelper vibrator,
IAudioService iAudioService,
+ VolumeControllerAdapter volumeControllerAdapter,
AccessibilityManager accessibilityManager,
PackageManager packageManager,
WakefulnessLifecycle wakefulnessLifecycle,
@@ -233,6 +236,7 @@
mVibrator = vibrator;
mHasVibrator = mVibrator.hasVibrator();
mAudioService = iAudioService;
+ mVolumeControllerAdapter = volumeControllerAdapter;
mKeyguardManager = keyguardManager;
mActivityManager = activityManager;
mUserTracker = userTracker;
@@ -259,10 +263,14 @@
}
protected void setVolumeController() {
- try {
- mAudio.setVolumeController(mVolumeController);
- } catch (SecurityException e) {
- Log.w(TAG, "Unable to set the volume controller", e);
+ if (Flags.useVolumeController()) {
+ mVolumeControllerAdapter.collectToController(mVolumeController);
+ } else {
+ try {
+ mAudio.setVolumeController(mVolumeController);
+ } catch (SecurityException e) {
+ Log.w(TAG, "Unable to set the volume controller", e);
+ }
}
}
@@ -384,7 +392,11 @@
}
public void notifyVisible(boolean visible) {
- mWorker.obtainMessage(W.NOTIFY_VISIBLE, visible ? 1 : 0, 0).sendToTarget();
+ if (Flags.useVolumeController()) {
+ mVolumeControllerAdapter.notifyVolumeControllerVisible(visible);
+ } else {
+ mWorker.obtainMessage(W.NOTIFY_VISIBLE, visible ? 1 : 0, 0).sendToTarget();
+ }
}
public void userActivity() {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
index 68d12f6..536403c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
@@ -20,9 +20,9 @@
import android.content.Context;
import android.content.res.Configuration;
-import android.os.Handler;
import android.util.Log;
+import com.android.settingslib.volume.data.repository.AudioRepository;
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.qs.tiles.DndTile;
@@ -39,23 +39,26 @@
private static final String TAG = "VolumeUI";
private static boolean LOGD = Log.isLoggable(TAG, Log.DEBUG);
- private final Handler mHandler = new Handler();
-
private boolean mEnabled;
private final Context mContext;
private VolumeDialogComponent mVolumeComponent;
private AudioSharingInteractor mAudioSharingInteractor;
+ private AudioRepository mAudioRepository;
@Inject
- public VolumeUI(Context context, VolumeDialogComponent volumeDialogComponent,
+ public VolumeUI(Context context,
+ VolumeDialogComponent volumeDialogComponent,
+ AudioRepository audioRepository,
AudioSharingInteractor audioSharingInteractor) {
mContext = context;
mVolumeComponent = volumeDialogComponent;
+ mAudioRepository = audioRepository;
mAudioSharingInteractor = audioSharingInteractor;
}
@Override
public void start() {
+ mAudioRepository.init();
boolean enableVolumeUi = mContext.getResources().getBoolean(R.bool.enable_volume_ui);
boolean enableSafetyWarning =
mContext.getResources().getBoolean(R.bool.enable_safety_warning);
@@ -77,7 +80,8 @@
@Override
public void dump(PrintWriter pw, String[] args) {
- pw.print("mEnabled="); pw.println(mEnabled);
+ pw.print("mEnabled=");
+ pw.println(mEnabled);
if (!mEnabled) return;
mVolumeComponent.dump(pw, args);
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
index d39daaf..20d598a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
@@ -70,6 +70,7 @@
coroutineContext,
coroutineScope,
volumeLogger,
+ com.android.systemui.Flags.useVolumeController(),
)
@Provides
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
index 664a0bd..844a166 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
@@ -21,6 +21,7 @@
import androidx.test.filters.SmallTest
import com.android.internal.policy.IKeyguardDismissCallback
import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
@@ -30,6 +31,7 @@
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
@@ -69,6 +71,12 @@
@Test
fun onBackRequested() =
testScope.runTest {
+ kosmos.primaryBouncerInteractor.setDismissAction(
+ mock(ActivityStarter.OnDismissAction::class.java),
+ {},
+ )
+ assertThat(kosmos.primaryBouncerInteractor.bouncerDismissAction).isNotNull()
+
val dismissCallback = mock(IKeyguardDismissCallback::class.java)
kosmos.dismissCallbackRegistry.addCallback(dismissCallback)
@@ -76,6 +84,7 @@
kosmos.fakeExecutor.runAllReady()
verify(statusBarKeyguardViewManager).hideAlternateBouncer(any())
verify(dismissCallback).onDismissCancelled()
+ assertThat(kosmos.primaryBouncerInteractor.bouncerDismissAction).isNull()
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
index 850916b..46c66e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
@@ -27,19 +27,24 @@
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.SceneKey
import com.android.internal.logging.InstanceId
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.SysuiTestCase
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
@@ -71,14 +76,18 @@
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
import java.util.Locale
import javax.inject.Provider
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
@@ -106,6 +115,7 @@
private const val PAUSED_LOCAL = "paused local"
private const val PLAYING_LOCAL = "playing local"
+@ExperimentalCoroutinesApi
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidJUnit4::class)
@@ -183,7 +193,7 @@
secureSettings = secureSettings,
mediaCarouselViewModel = kosmos.mediaCarouselViewModel,
mediaViewControllerFactory = mediaViewControllerFactory,
- sceneInteractor = kosmos.sceneInteractor,
+ deviceEntryInteractor = kosmos.deviceEntryInteractor,
)
verify(configurationController).addCallback(capture(configListener))
verify(visualStabilityProvider)
@@ -868,7 +878,6 @@
}
@DisableSceneContainer
- @ExperimentalCoroutinesApi
@Test
fun testKeyguardGone_showMediaCarousel() =
kosmos.testScope.runTest {
@@ -892,7 +901,6 @@
}
@EnableSceneContainer
- @ExperimentalCoroutinesApi
@Test
fun testKeyguardGone_showMediaCarousel_scene_container() =
kosmos.testScope.runTest {
@@ -910,7 +918,6 @@
job.cancel()
}
- @ExperimentalCoroutinesApi
@Test
fun keyguardShowing_notAllowedOnLockscreen_updateVisibility() {
kosmos.testScope.runTest {
@@ -940,7 +947,6 @@
}
}
- @ExperimentalCoroutinesApi
@Test
fun keyguardShowing_allowedOnLockscreen_updateVisibility() {
kosmos.testScope.runTest {
@@ -970,6 +976,74 @@
}
}
+ @EnableSceneContainer
+ @Test
+ fun deviceEntered_mediaAllowed_notLockedAndHidden() {
+ kosmos.testScope.runTest {
+ val settingsJob =
+ mediaCarouselController.listenForLockscreenSettingChanges(
+ kosmos.applicationCoroutineScope
+ )
+ secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, true)
+ setDeviceEntered(true)
+
+ assertEquals(false, mediaCarouselController.isLockedAndHidden())
+
+ settingsJob.cancel()
+ }
+ }
+
+ @EnableSceneContainer
+ @Test
+ fun deviceEntered_mediaNotAllowed_notLockedAndHidden() {
+ kosmos.testScope.runTest {
+ val settingsJob =
+ mediaCarouselController.listenForLockscreenSettingChanges(
+ kosmos.applicationCoroutineScope
+ )
+ secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, false)
+ setDeviceEntered(true)
+
+ assertEquals(false, mediaCarouselController.isLockedAndHidden())
+
+ settingsJob.cancel()
+ }
+ }
+
+ @EnableSceneContainer
+ @Test
+ fun deviceNotEntered_mediaAllowed_notLockedAndHidden() {
+ kosmos.testScope.runTest {
+ val settingsJob =
+ mediaCarouselController.listenForLockscreenSettingChanges(
+ kosmos.applicationCoroutineScope
+ )
+ secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, true)
+ setDeviceEntered(false)
+
+ assertEquals(false, mediaCarouselController.isLockedAndHidden())
+
+ settingsJob.cancel()
+ }
+ }
+
+ @EnableSceneContainer
+ @Test
+ fun deviceNotEntered_mediaNotAllowed_lockedAndHidden() {
+ kosmos.testScope.runTest {
+ val settingsJob =
+ mediaCarouselController.listenForLockscreenSettingChanges(
+ kosmos.applicationCoroutineScope
+ )
+ secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, false)
+ setDeviceEntered(false)
+
+ assertEquals(true, mediaCarouselController.isLockedAndHidden())
+
+ settingsJob.cancel()
+ }
+ }
+
@Test
fun testInvisibleToUserAndExpanded_playersNotListening() {
// Add players to carousel.
@@ -1129,4 +1203,30 @@
mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
)
}
+
+ private fun TestScope.setDeviceEntered(isEntered: Boolean) {
+ if (isEntered) {
+ // Unlock the device, marking the device as entered
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+ }
+ setScene(
+ if (isEntered) {
+ Scenes.Gone
+ } else {
+ Scenes.Lockscreen
+ }
+ )
+ assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isEqualTo(isEntered)
+ }
+
+ private fun TestScope.setScene(key: SceneKey) {
+ kosmos.sceneInteractor.changeScene(key, "test")
+ kosmos.sceneInteractor.setTransitionState(
+ MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
+ )
+ runCurrent()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt
index 263b001..a82e5c4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt
@@ -14,6 +14,7 @@
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -29,7 +30,6 @@
import org.mockito.Mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@SmallTest
@@ -73,8 +73,8 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
- `when`(context.user).thenReturn(UserHandle.SYSTEM)
- `when`(context.createContextAsUser(ArgumentMatchers.any(), anyInt())).thenReturn(context)
+ whenever(context.user).thenReturn(UserHandle.SYSTEM)
+ whenever(context.createContextAsUser(ArgumentMatchers.any(), anyInt())).thenReturn(context)
}
@Test
@@ -94,7 +94,7 @@
tracker.addCallback(callback, executor)
val profileID = tracker.userId + 10
- `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
+ whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
val id = invocation.getArgument<Int>(0)
val info = UserInfo(id, "", UserInfo.FLAG_FULL)
val infoProfile =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
index 774aa51..2e2ac3e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
@@ -33,9 +33,11 @@
import com.android.systemui.flags.Flags
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.TruthJUnit.assume
+import java.util.concurrent.Executor
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@@ -54,10 +56,7 @@
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
-import java.util.concurrent.Executor
-
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -71,27 +70,19 @@
fun isBackgroundUserTrackerEnabled(): Iterable<Boolean> = listOf(true, false)
}
- @Mock
- private lateinit var context: Context
+ @Mock private lateinit var context: Context
- @Mock
- private lateinit var userManager: UserManager
+ @Mock private lateinit var userManager: UserManager
- @Mock
- private lateinit var iActivityManager: IActivityManager
+ @Mock private lateinit var iActivityManager: IActivityManager
- @Mock
- private lateinit var userSwitchingReply: IRemoteCallback
+ @Mock private lateinit var userSwitchingReply: IRemoteCallback
- @Mock(stubOnly = true)
- private lateinit var dumpManager: DumpManager
+ @Mock(stubOnly = true) private lateinit var dumpManager: DumpManager
- @Mock(stubOnly = true)
- private lateinit var handler: Handler
+ @Mock(stubOnly = true) private lateinit var handler: Handler
- @Parameterized.Parameter
- @JvmField
- var isBackgroundUserTrackerEnabled: Boolean = false
+ @Parameterized.Parameter @JvmField var isBackgroundUserTrackerEnabled: Boolean = false
private val testScope = TestScope()
private val testDispatcher = StandardTestDispatcher(testScope.testScheduler)
@@ -104,365 +95,379 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
- `when`(context.userId).thenReturn(UserHandle.USER_SYSTEM)
- `when`(context.user).thenReturn(UserHandle.SYSTEM)
- `when`(context.createContextAsUser(any(), anyInt())).thenAnswer { invocation ->
+ whenever(context.userId).thenReturn(UserHandle.USER_SYSTEM)
+ whenever(context.user).thenReturn(UserHandle.SYSTEM)
+ whenever(context.createContextAsUser(any(), anyInt())).thenAnswer { invocation ->
val user = invocation.getArgument<UserHandle>(0)
- `when`(context.user).thenReturn(user)
- `when`(context.userId).thenReturn(user.identifier)
+ whenever(context.user).thenReturn(user)
+ whenever(context.userId).thenReturn(user.identifier)
context
}
- `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
+ whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
val info = UserInfo(invocation.getArgument<Int>(0), "", UserInfo.FLAG_FULL)
listOf(info)
}
featureFlags.set(Flags.USER_TRACKER_BACKGROUND_CALLBACKS, isBackgroundUserTrackerEnabled)
tracker =
- UserTrackerImpl(
- context,
- { featureFlags },
- userManager,
- iActivityManager,
- dumpManager,
- testScope.backgroundScope,
- testDispatcher,
- handler,
- )
+ UserTrackerImpl(
+ context,
+ { featureFlags },
+ userManager,
+ iActivityManager,
+ dumpManager,
+ testScope.backgroundScope,
+ testDispatcher,
+ handler,
+ )
}
+ @Test fun testNotInitialized() = testScope.runTest { assertThat(tracker.initialized).isFalse() }
+
+ @Test(expected = IllegalStateException::class)
+ fun testGetUserIdBeforeInitThrowsException() = testScope.runTest { tracker.userId }
+
+ @Test(expected = IllegalStateException::class)
+ fun testGetUserHandleBeforeInitThrowsException() = testScope.runTest { tracker.userHandle }
+
+ @Test(expected = IllegalStateException::class)
+ fun testGetUserContextBeforeInitThrowsException() = testScope.runTest { tracker.userContext }
+
+ @Test(expected = IllegalStateException::class)
+ fun testGetUserContentResolverBeforeInitThrowsException() =
+ testScope.runTest { tracker.userContentResolver }
+
+ @Test(expected = IllegalStateException::class)
+ fun testGetUserProfilesBeforeInitThrowsException() = testScope.runTest { tracker.userProfiles }
+
@Test
- fun testNotInitialized() = testScope.runTest {
- assertThat(tracker.initialized).isFalse()
- }
+ fun testInitialize() =
+ testScope.runTest {
+ tracker.initialize(0)
- @Test(expected = IllegalStateException::class)
- fun testGetUserIdBeforeInitThrowsException() = testScope.runTest {
- tracker.userId
- }
-
- @Test(expected = IllegalStateException::class)
- fun testGetUserHandleBeforeInitThrowsException() = testScope.runTest {
- tracker.userHandle
- }
-
- @Test(expected = IllegalStateException::class)
- fun testGetUserContextBeforeInitThrowsException() = testScope.runTest {
- tracker.userContext
- }
-
- @Test(expected = IllegalStateException::class)
- fun testGetUserContentResolverBeforeInitThrowsException() = testScope.runTest {
- tracker.userContentResolver
- }
-
- @Test(expected = IllegalStateException::class)
- fun testGetUserProfilesBeforeInitThrowsException() = testScope.runTest {
- tracker.userProfiles
- }
+ assertThat(tracker.initialized).isTrue()
+ }
@Test
- fun testInitialize() = testScope.runTest {
- tracker.initialize(0)
+ fun testReceiverRegisteredOnInitialize() =
+ testScope.runTest {
+ tracker.initialize(0)
- assertThat(tracker.initialized).isTrue()
- }
+ val captor = ArgumentCaptor.forClass(IntentFilter::class.java)
- @Test
- fun testReceiverRegisteredOnInitialize() = testScope.runTest {
- tracker.initialize(0)
-
- val captor = ArgumentCaptor.forClass(IntentFilter::class.java)
-
- verify(context)
+ verify(context)
.registerReceiverForAllUsers(eq(tracker), capture(captor), isNull(), eq(handler))
- with(captor.value) {
- assertThat(countActions()).isEqualTo(11)
- assertThat(hasAction(Intent.ACTION_LOCALE_CHANGED)).isTrue()
- assertThat(hasAction(Intent.ACTION_USER_INFO_CHANGED)).isTrue()
- assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)).isTrue()
- assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)).isTrue()
- assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_ADDED)).isTrue()
- assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_REMOVED)).isTrue()
- assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)).isTrue()
- assertThat(hasAction(Intent.ACTION_PROFILE_ADDED)).isTrue()
- assertThat(hasAction(Intent.ACTION_PROFILE_REMOVED)).isTrue()
- assertThat(hasAction(Intent.ACTION_PROFILE_AVAILABLE)).isTrue()
- assertThat(hasAction(Intent.ACTION_PROFILE_UNAVAILABLE)).isTrue()
- }
- }
-
- @Test
- fun testInitialValuesSet() = testScope.runTest {
- val testID = 4
- tracker.initialize(testID)
-
- verify(userManager).getProfiles(testID)
-
- assertThat(tracker.userId).isEqualTo(testID)
- assertThat(tracker.userHandle).isEqualTo(UserHandle.of(testID))
- assertThat(tracker.userContext.userId).isEqualTo(testID)
- assertThat(tracker.userContext.user).isEqualTo(UserHandle.of(testID))
- assertThat(tracker.userProfiles).hasSize(1)
-
- val info = tracker.userProfiles[0]
- assertThat(info.id).isEqualTo(testID)
- }
-
- @Test
- fun testUserSwitch() = testScope.runTest {
- tracker.initialize(0)
- val newID = 5
-
- val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
- verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
- captor.value.onBeforeUserSwitching(newID)
- captor.value.onUserSwitching(newID, userSwitchingReply)
- runCurrent()
- verify(userSwitchingReply).sendResult(any())
-
- verify(userManager).getProfiles(newID)
-
- assertThat(tracker.userId).isEqualTo(newID)
- assertThat(tracker.userHandle).isEqualTo(UserHandle.of(newID))
- assertThat(tracker.userContext.userId).isEqualTo(newID)
- assertThat(tracker.userContext.user).isEqualTo(UserHandle.of(newID))
- assertThat(tracker.userProfiles).hasSize(1)
-
- val info = tracker.userProfiles[0]
- assertThat(info.id).isEqualTo(newID)
- }
-
- @Test
- fun testManagedProfileAvailable() = testScope.runTest {
- tracker.initialize(0)
- val profileID = tracker.userId + 10
-
- `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
- val id = invocation.getArgument<Int>(0)
- val info = UserInfo(id, "", UserInfo.FLAG_FULL)
- val infoProfile = UserInfo(
- id + 10,
- "",
- "",
- UserInfo.FLAG_MANAGED_PROFILE,
- UserManager.USER_TYPE_PROFILE_MANAGED
- )
- infoProfile.profileGroupId = id
- listOf(info, infoProfile)
+ with(captor.value) {
+ assertThat(countActions()).isEqualTo(11)
+ assertThat(hasAction(Intent.ACTION_LOCALE_CHANGED)).isTrue()
+ assertThat(hasAction(Intent.ACTION_USER_INFO_CHANGED)).isTrue()
+ assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)).isTrue()
+ assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)).isTrue()
+ assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_ADDED)).isTrue()
+ assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_REMOVED)).isTrue()
+ assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)).isTrue()
+ assertThat(hasAction(Intent.ACTION_PROFILE_ADDED)).isTrue()
+ assertThat(hasAction(Intent.ACTION_PROFILE_REMOVED)).isTrue()
+ assertThat(hasAction(Intent.ACTION_PROFILE_AVAILABLE)).isTrue()
+ assertThat(hasAction(Intent.ACTION_PROFILE_UNAVAILABLE)).isTrue()
+ }
}
- val intent = Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
- .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
- tracker.onReceive(context, intent)
-
- assertThat(tracker.userProfiles.map { it.id }).containsExactly(tracker.userId, profileID)
- }
-
@Test
- fun testManagedProfileUnavailable() = testScope.runTest {
- tracker.initialize(0)
- val profileID = tracker.userId + 10
+ fun testInitialValuesSet() =
+ testScope.runTest {
+ val testID = 4
+ tracker.initialize(testID)
- `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
- val id = invocation.getArgument<Int>(0)
- val info = UserInfo(id, "", UserInfo.FLAG_FULL)
- val infoProfile = UserInfo(
- id + 10,
- "",
- "",
- UserInfo.FLAG_MANAGED_PROFILE or UserInfo.FLAG_QUIET_MODE,
- UserManager.USER_TYPE_PROFILE_MANAGED
- )
- infoProfile.profileGroupId = id
- listOf(info, infoProfile)
+ verify(userManager).getProfiles(testID)
+
+ assertThat(tracker.userId).isEqualTo(testID)
+ assertThat(tracker.userHandle).isEqualTo(UserHandle.of(testID))
+ assertThat(tracker.userContext.userId).isEqualTo(testID)
+ assertThat(tracker.userContext.user).isEqualTo(UserHandle.of(testID))
+ assertThat(tracker.userProfiles).hasSize(1)
+
+ val info = tracker.userProfiles[0]
+ assertThat(info.id).isEqualTo(testID)
}
- val intent = Intent(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
- .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
- tracker.onReceive(context, intent)
-
- assertThat(tracker.userProfiles.map { it.id }).containsExactly(tracker.userId, profileID)
- }
-
@Test
- fun testManagedProfileStartedAndRemoved() = testScope.runTest {
- tracker.initialize(0)
- val profileID = tracker.userId + 10
+ fun testUserSwitch() =
+ testScope.runTest {
+ tracker.initialize(0)
+ val newID = 5
- `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
- val id = invocation.getArgument<Int>(0)
- val info = UserInfo(id, "", UserInfo.FLAG_FULL)
- val infoProfile = UserInfo(
- id + 10,
- "",
- "",
- UserInfo.FLAG_MANAGED_PROFILE,
- UserManager.USER_TYPE_PROFILE_MANAGED
- )
- infoProfile.profileGroupId = id
- listOf(info, infoProfile)
+ val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
+ verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+ captor.value.onBeforeUserSwitching(newID)
+ captor.value.onUserSwitching(newID, userSwitchingReply)
+ runCurrent()
+ verify(userSwitchingReply).sendResult(any())
+
+ verify(userManager).getProfiles(newID)
+
+ assertThat(tracker.userId).isEqualTo(newID)
+ assertThat(tracker.userHandle).isEqualTo(UserHandle.of(newID))
+ assertThat(tracker.userContext.userId).isEqualTo(newID)
+ assertThat(tracker.userContext.user).isEqualTo(UserHandle.of(newID))
+ assertThat(tracker.userProfiles).hasSize(1)
+
+ val info = tracker.userProfiles[0]
+ assertThat(info.id).isEqualTo(newID)
}
- // Managed profile started
- val intent = Intent(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)
- .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
- tracker.onReceive(context, intent)
+ @Test
+ fun testManagedProfileAvailable() =
+ testScope.runTest {
+ tracker.initialize(0)
+ val profileID = tracker.userId + 10
- assertThat(tracker.userProfiles.map { it.id }).containsExactly(tracker.userId, profileID)
+ whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
+ val id = invocation.getArgument<Int>(0)
+ val info = UserInfo(id, "", UserInfo.FLAG_FULL)
+ val infoProfile =
+ UserInfo(
+ id + 10,
+ "",
+ "",
+ UserInfo.FLAG_MANAGED_PROFILE,
+ UserManager.USER_TYPE_PROFILE_MANAGED
+ )
+ infoProfile.profileGroupId = id
+ listOf(info, infoProfile)
+ }
- `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
- listOf(UserInfo(invocation.getArgument(0), "", UserInfo.FLAG_FULL))
+ val intent =
+ Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
+ .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
+ tracker.onReceive(context, intent)
+
+ assertThat(tracker.userProfiles.map { it.id })
+ .containsExactly(tracker.userId, profileID)
}
- val intent2 = Intent(Intent.ACTION_MANAGED_PROFILE_REMOVED)
- .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
- tracker.onReceive(context, intent2)
-
- assertThat(tracker.userProfiles.map { it.id }).containsExactly(tracker.userId)
- }
-
@Test
- fun testCallbackNotCalledOnAdd() = testScope.runTest {
- tracker.initialize(0)
- val callback = TestCallback()
+ fun testManagedProfileUnavailable() =
+ testScope.runTest {
+ tracker.initialize(0)
+ val profileID = tracker.userId + 10
- tracker.addCallback(callback, executor)
+ whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
+ val id = invocation.getArgument<Int>(0)
+ val info = UserInfo(id, "", UserInfo.FLAG_FULL)
+ val infoProfile =
+ UserInfo(
+ id + 10,
+ "",
+ "",
+ UserInfo.FLAG_MANAGED_PROFILE or UserInfo.FLAG_QUIET_MODE,
+ UserManager.USER_TYPE_PROFILE_MANAGED
+ )
+ infoProfile.profileGroupId = id
+ listOf(info, infoProfile)
+ }
- assertThat(callback.calledOnProfilesChanged).isEqualTo(0)
- assertThat(callback.calledOnUserChanged).isEqualTo(0)
- }
+ val intent =
+ Intent(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
+ .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
+ tracker.onReceive(context, intent)
- @Test
- fun testCallbackCalledOnUserChanging() = testScope.runTest {
- tracker.initialize(0)
- val callback = TestCallback()
- tracker.addCallback(callback, executor)
-
- val newID = 5
-
- val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
- verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
- captor.value.onBeforeUserSwitching(newID)
- captor.value.onUserSwitching(newID, userSwitchingReply)
- runCurrent()
-
- verify(userSwitchingReply).sendResult(any())
- assertThat(callback.calledOnUserChanging).isEqualTo(1)
- assertThat(callback.lastUser).isEqualTo(newID)
- assertThat(callback.lastUserContext?.userId).isEqualTo(newID)
- }
-
- @Test
- fun testAsyncCallbackWaitsUserToChange() = testScope.runTest {
- // Skip this test for CountDownLatch variation. The problem is that there would be a
- // deadlock if the callbacks processing runs on the same thread as the callback (which
- // is blocked by the latch). Before the change it works because the callbacks are
- // processed on a binder thread which is always distinct.
- // This is the issue that this feature addresses.
- assume().that(isBackgroundUserTrackerEnabled).isTrue()
-
- tracker.initialize(0)
- val callback = TestCallback()
- val callbackExecutor = FakeExecutor(FakeSystemClock())
- tracker.addCallback(callback, callbackExecutor)
-
- val newID = 5
-
- val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
- verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
- captor.value.onUserSwitching(newID, userSwitchingReply)
-
- assertThat(callback.calledOnUserChanging).isEqualTo(0)
- verify(userSwitchingReply, never()).sendResult(any())
-
- FakeExecutor.exhaustExecutors(callbackExecutor)
- runCurrent()
- FakeExecutor.exhaustExecutors(callbackExecutor)
- runCurrent()
-
- assertThat(callback.calledOnUserChanging).isEqualTo(1)
- verify(userSwitchingReply).sendResult(any())
- }
-
- @Test
- fun testCallbackCalledOnUserChanged() = testScope.runTest {
- tracker.initialize(0)
- val callback = TestCallback()
- tracker.addCallback(callback, executor)
-
- val newID = 5
-
- val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
- verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
- captor.value.onBeforeUserSwitching(newID)
- captor.value.onUserSwitchComplete(newID)
- runCurrent()
-
- assertThat(callback.calledOnUserChanged).isEqualTo(1)
- assertThat(callback.lastUser).isEqualTo(newID)
- assertThat(callback.lastUserContext?.userId).isEqualTo(newID)
- assertThat(callback.calledOnProfilesChanged).isEqualTo(1)
- assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(newID)
- }
-
- @Test
- fun testCallbackCalledOnUserInfoChanged() = testScope.runTest {
- tracker.initialize(0)
- val callback = TestCallback()
- tracker.addCallback(callback, executor)
- val profileID = tracker.userId + 10
-
- `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
- val id = invocation.getArgument<Int>(0)
- val info = UserInfo(id, "", UserInfo.FLAG_FULL)
- val infoProfile = UserInfo(
- id + 10,
- "",
- "",
- UserInfo.FLAG_MANAGED_PROFILE,
- UserManager.USER_TYPE_PROFILE_MANAGED
- )
- infoProfile.profileGroupId = id
- listOf(info, infoProfile)
+ assertThat(tracker.userProfiles.map { it.id })
+ .containsExactly(tracker.userId, profileID)
}
- val intent = Intent(Intent.ACTION_USER_INFO_CHANGED)
- .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
+ @Test
+ fun testManagedProfileStartedAndRemoved() =
+ testScope.runTest {
+ tracker.initialize(0)
+ val profileID = tracker.userId + 10
- tracker.onReceive(context, intent)
+ whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
+ val id = invocation.getArgument<Int>(0)
+ val info = UserInfo(id, "", UserInfo.FLAG_FULL)
+ val infoProfile =
+ UserInfo(
+ id + 10,
+ "",
+ "",
+ UserInfo.FLAG_MANAGED_PROFILE,
+ UserManager.USER_TYPE_PROFILE_MANAGED
+ )
+ infoProfile.profileGroupId = id
+ listOf(info, infoProfile)
+ }
- assertThat(callback.calledOnUserChanged).isEqualTo(0)
- assertThat(callback.calledOnProfilesChanged).isEqualTo(1)
- assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(0, profileID)
- }
+ // Managed profile started
+ val intent =
+ Intent(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)
+ .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
+ tracker.onReceive(context, intent)
+
+ assertThat(tracker.userProfiles.map { it.id })
+ .containsExactly(tracker.userId, profileID)
+
+ whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
+ listOf(UserInfo(invocation.getArgument(0), "", UserInfo.FLAG_FULL))
+ }
+
+ val intent2 =
+ Intent(Intent.ACTION_MANAGED_PROFILE_REMOVED)
+ .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
+ tracker.onReceive(context, intent2)
+
+ assertThat(tracker.userProfiles.map { it.id }).containsExactly(tracker.userId)
+ }
@Test
- fun testCallbackRemoved() = testScope.runTest {
- tracker.initialize(0)
- val newID = 5
- val profileID = newID + 10
+ fun testCallbackNotCalledOnAdd() =
+ testScope.runTest {
+ tracker.initialize(0)
+ val callback = TestCallback()
- val callback = TestCallback()
- tracker.addCallback(callback, executor)
- tracker.removeCallback(callback)
+ tracker.addCallback(callback, executor)
- val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
- verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
- captor.value.onUserSwitching(newID, userSwitchingReply)
- runCurrent()
- verify(userSwitchingReply).sendResult(any())
- captor.value.onUserSwitchComplete(newID)
+ assertThat(callback.calledOnProfilesChanged).isEqualTo(0)
+ assertThat(callback.calledOnUserChanged).isEqualTo(0)
+ }
- val intentProfiles = Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
- .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
+ @Test
+ fun testCallbackCalledOnUserChanging() =
+ testScope.runTest {
+ tracker.initialize(0)
+ val callback = TestCallback()
+ tracker.addCallback(callback, executor)
- tracker.onReceive(context, intentProfiles)
+ val newID = 5
- assertThat(callback.calledOnUserChanging).isEqualTo(0)
- assertThat(callback.calledOnUserChanged).isEqualTo(0)
- assertThat(callback.calledOnProfilesChanged).isEqualTo(0)
- }
+ val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
+ verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+ captor.value.onBeforeUserSwitching(newID)
+ captor.value.onUserSwitching(newID, userSwitchingReply)
+ runCurrent()
+
+ verify(userSwitchingReply).sendResult(any())
+ assertThat(callback.calledOnUserChanging).isEqualTo(1)
+ assertThat(callback.lastUser).isEqualTo(newID)
+ assertThat(callback.lastUserContext?.userId).isEqualTo(newID)
+ }
+
+ @Test
+ fun testAsyncCallbackWaitsUserToChange() =
+ testScope.runTest {
+ // Skip this test for CountDownLatch variation. The problem is that there would be a
+ // deadlock if the callbacks processing runs on the same thread as the callback (which
+ // is blocked by the latch). Before the change it works because the callbacks are
+ // processed on a binder thread which is always distinct.
+ // This is the issue that this feature addresses.
+ assume().that(isBackgroundUserTrackerEnabled).isTrue()
+
+ tracker.initialize(0)
+ val callback = TestCallback()
+ val callbackExecutor = FakeExecutor(FakeSystemClock())
+ tracker.addCallback(callback, callbackExecutor)
+
+ val newID = 5
+
+ val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
+ verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+ captor.value.onUserSwitching(newID, userSwitchingReply)
+
+ assertThat(callback.calledOnUserChanging).isEqualTo(0)
+ verify(userSwitchingReply, never()).sendResult(any())
+
+ FakeExecutor.exhaustExecutors(callbackExecutor)
+ runCurrent()
+ FakeExecutor.exhaustExecutors(callbackExecutor)
+ runCurrent()
+
+ assertThat(callback.calledOnUserChanging).isEqualTo(1)
+ verify(userSwitchingReply).sendResult(any())
+ }
+
+ @Test
+ fun testCallbackCalledOnUserChanged() =
+ testScope.runTest {
+ tracker.initialize(0)
+ val callback = TestCallback()
+ tracker.addCallback(callback, executor)
+
+ val newID = 5
+
+ val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
+ verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+ captor.value.onBeforeUserSwitching(newID)
+ captor.value.onUserSwitchComplete(newID)
+ runCurrent()
+
+ assertThat(callback.calledOnUserChanged).isEqualTo(1)
+ assertThat(callback.lastUser).isEqualTo(newID)
+ assertThat(callback.lastUserContext?.userId).isEqualTo(newID)
+ assertThat(callback.calledOnProfilesChanged).isEqualTo(1)
+ assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(newID)
+ }
+
+ @Test
+ fun testCallbackCalledOnUserInfoChanged() =
+ testScope.runTest {
+ tracker.initialize(0)
+ val callback = TestCallback()
+ tracker.addCallback(callback, executor)
+ val profileID = tracker.userId + 10
+
+ whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
+ val id = invocation.getArgument<Int>(0)
+ val info = UserInfo(id, "", UserInfo.FLAG_FULL)
+ val infoProfile =
+ UserInfo(
+ id + 10,
+ "",
+ "",
+ UserInfo.FLAG_MANAGED_PROFILE,
+ UserManager.USER_TYPE_PROFILE_MANAGED
+ )
+ infoProfile.profileGroupId = id
+ listOf(info, infoProfile)
+ }
+
+ val intent =
+ Intent(Intent.ACTION_USER_INFO_CHANGED)
+ .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
+
+ tracker.onReceive(context, intent)
+
+ assertThat(callback.calledOnUserChanged).isEqualTo(0)
+ assertThat(callback.calledOnProfilesChanged).isEqualTo(1)
+ assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(0, profileID)
+ }
+
+ @Test
+ fun testCallbackRemoved() =
+ testScope.runTest {
+ tracker.initialize(0)
+ val newID = 5
+ val profileID = newID + 10
+
+ val callback = TestCallback()
+ tracker.addCallback(callback, executor)
+ tracker.removeCallback(callback)
+
+ val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
+ verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+ captor.value.onUserSwitching(newID, userSwitchingReply)
+ runCurrent()
+ verify(userSwitchingReply).sendResult(any())
+ captor.value.onUserSwitchComplete(newID)
+
+ val intentProfiles =
+ Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
+ .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
+
+ tracker.onReceive(context, intentProfiles)
+
+ assertThat(callback.calledOnUserChanging).isEqualTo(0)
+ assertThat(callback.calledOnUserChanged).isEqualTo(0)
+ assertThat(callback.calledOnProfilesChanged).isEqualTo(0)
+ }
private class TestCallback : UserTracker.Callback {
var calledOnUserChanging = 0
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index b75ac2b..54c03e8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -83,6 +83,7 @@
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.flags.DisableSceneContainer;
import com.android.systemui.flags.EnableSceneContainer;
+import com.android.systemui.keyguard.DismissCallbackRegistry;
import com.android.systemui.keyguard.domain.interactor.KeyguardDismissActionInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardSurfaceBehindInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
@@ -171,6 +172,7 @@
@Mock private SelectedUserInteractor mSelectedUserInteractor;
@Mock private DeviceEntryInteractor mDeviceEntryInteractor;
@Mock private SceneInteractor mSceneInteractor;
+ @Mock private DismissCallbackRegistry mDismissCallbackRegistry;
private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback
@@ -242,7 +244,8 @@
() -> mSceneInteractor,
mock(StatusBarKeyguardViewManagerInteractor.class),
mExecutor,
- () -> mDeviceEntryInteractor) {
+ () -> mDeviceEntryInteractor,
+ mDismissCallbackRegistry) {
@Override
public ViewRootImpl getViewRootImpl() {
return mViewRootImpl;
@@ -765,7 +768,8 @@
() -> mSceneInteractor,
mock(StatusBarKeyguardViewManagerInteractor.class),
mExecutor,
- () -> mDeviceEntryInteractor) {
+ () -> mDeviceEntryInteractor,
+ mDismissCallbackRegistry) {
@Override
public ViewRootImpl getViewRootImpl() {
return mViewRootImpl;
@@ -777,7 +781,11 @@
}
@Test
+ @DisableSceneContainer
public void testResetHideBouncerWhenShowing_alternateBouncerHides() {
+ reset(mDismissCallbackRegistry);
+ reset(mPrimaryBouncerInteractor);
+
// GIVEN the keyguard is showing
reset(mAlternateBouncerInteractor);
when(mKeyguardStateController.isShowing()).thenReturn(true);
@@ -785,8 +793,10 @@
// WHEN SBKV is reset with hideBouncerWhenShowing=true
mStatusBarKeyguardViewManager.reset(true);
- // THEN alternate bouncer is hidden
+ // THEN alternate bouncer is hidden and dismiss actions reset
verify(mAlternateBouncerInteractor).hide();
+ verify(mDismissCallbackRegistry).notifyDismissCancelled();
+ verify(mPrimaryBouncerInteractor).setDismissAction(eq(null), eq(null));
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerCollectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerAdapterTest.kt
similarity index 86%
rename from packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerCollectorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerAdapterTest.kt
index dd78e4a..c140364 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerCollectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerAdapterTest.kt
@@ -19,16 +19,17 @@
import android.media.IVolumeController
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.settingslib.media.data.repository.VolumeControllerEvent
+import com.android.settingslib.volume.data.model.VolumeControllerEvent
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
+import com.android.systemui.volume.data.repository.audioRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.filterNotNull
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.eq
@@ -38,14 +39,20 @@
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
-class VolumeControllerCollectorTest : SysuiTestCase() {
+class VolumeControllerAdapterTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val eventsFlow = MutableStateFlow<VolumeControllerEvent?>(null)
- private val underTest = VolumeControllerCollector(kosmos.applicationCoroutineScope)
+ private val underTest =
+ with(kosmos) { VolumeControllerAdapter(applicationCoroutineScope, audioRepository) }
private val volumeController = mock<IVolumeController> {}
+ @Before
+ fun setUp() {
+ kosmos.audioRepository.init()
+ }
+
@Test
fun volumeControllerEvent_volumeChanged_callsMethod() =
testEvent(VolumeControllerEvent.VolumeChanged(3, 0)) {
@@ -90,7 +97,8 @@
private fun testEvent(event: VolumeControllerEvent, verify: () -> Unit) =
kosmos.testScope.runTest {
- underTest.collectToController(eventsFlow.filterNotNull(), volumeController)
+ kosmos.audioRepository.sendVolumeControllerEvent(event)
+ underTest.collectToController(volumeController)
eventsFlow.value = event
runCurrent()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
index 4ea1a0c..f62beeb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
@@ -48,9 +48,11 @@
import com.android.settingslib.flags.Flags;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.SysuiTestCaseExtKt;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.kosmos.Kosmos;
import com.android.systemui.plugins.VolumeDialogController;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.VibratorHelper;
@@ -78,6 +80,8 @@
@TestableLooper.RunWithLooper
public class VolumeDialogControllerImplTest extends SysuiTestCase {
+ private final Kosmos mKosmos = SysuiTestCaseExtKt.testKosmos(this);
+
TestableVolumeDialogControllerImpl mVolumeController;
VolumeDialogControllerImpl.C mCallback;
@Mock
@@ -146,6 +150,7 @@
mNotificationManager,
mVibrator,
mIAudioService,
+ VolumeControllerAdapterKosmosKt.getVolumeControllerAdapter(mKosmos),
mAccessibilityManager,
mPackageManager,
mWakefullnessLifcycle,
@@ -323,6 +328,7 @@
NotificationManager notificationManager,
VibratorHelper optionalVibrator,
IAudioService iAudioService,
+ VolumeControllerAdapter volumeControllerAdapter,
AccessibilityManager accessibilityManager,
PackageManager packageManager,
WakefulnessLifecycle wakefulnessLifecycle,
@@ -342,6 +348,7 @@
notificationManager,
optionalVibrator,
iAudioService,
+ volumeControllerAdapter,
accessibilityManager,
packageManager,
wakefulnessLifecycle,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTestKt.kt b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTestKt.kt
new file mode 100644
index 0000000..98cea9d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTestKt.kt
@@ -0,0 +1,156 @@
+/*
+ * 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.volume
+
+import android.app.activityManager
+import android.app.keyguardManager
+import android.content.applicationContext
+import android.content.packageManager
+import android.media.AudioManager
+import android.media.IVolumeController
+import android.os.Handler
+import android.os.looper
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.TestableLooper
+import android.view.accessibility.accessibilityManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.volume.data.model.VolumeControllerEvent
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.wakefulnessLifecycle
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.VolumeDialogController
+import com.android.systemui.testKosmos
+import com.android.systemui.util.RingerModeLiveData
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.concurrency.FakeThreadFactory
+import com.android.systemui.util.time.fakeSystemClock
+import com.android.systemui.volume.data.repository.audioRepository
+import com.android.systemui.volume.domain.interactor.audioSharingInteractor
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@TestableLooper.RunWithLooper
+class VolumeDialogControllerImplTestKt : SysuiTestCase() {
+
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
+ private val kosmos: Kosmos = testKosmos()
+ private val audioManager: AudioManager = mock {}
+ private val callbacks: VolumeDialogController.Callbacks = mock {}
+
+ private lateinit var threadFactory: FakeThreadFactory
+ private lateinit var underTest: VolumeDialogControllerImpl
+
+ @Before
+ fun setUp() =
+ with(kosmos) {
+ audioRepository.init()
+ threadFactory =
+ FakeThreadFactory(FakeExecutor(fakeSystemClock)).apply { setLooper(looper) }
+ underTest =
+ VolumeDialogControllerImpl(
+ applicationContext,
+ mock {},
+ mock {
+ on { ringerMode }.thenReturn(mock<RingerModeLiveData> {})
+ on { ringerModeInternal }.thenReturn(mock<RingerModeLiveData> {})
+ },
+ threadFactory,
+ audioManager,
+ mock {},
+ mock {},
+ mock {},
+ volumeControllerAdapter,
+ accessibilityManager,
+ packageManager,
+ wakefulnessLifecycle,
+ keyguardManager,
+ activityManager,
+ mock { on { userContext }.thenReturn(applicationContext) },
+ dumpManager,
+ audioSharingInteractor,
+ mock {},
+ )
+ .apply {
+ setEnableDialogs(true, true)
+ addCallback(callbacks, Handler(looper))
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_USE_VOLUME_CONTROLLER)
+ fun useVolumeControllerEnabled_listensToVolumeController() =
+ testVolumeController { stream: Int, flags: Int ->
+ audioRepository.sendVolumeControllerEvent(
+ VolumeControllerEvent.VolumeChanged(streamType = stream, flags = flags)
+ )
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_USE_VOLUME_CONTROLLER)
+ fun useVolumeControllerDisabled_listensToVolumeController() =
+ testVolumeController { stream: Int, flags: Int ->
+ audioManager.emitVolumeChange(stream, flags)
+ }
+
+ private fun testVolumeController(
+ emitVolumeChange: suspend Kosmos.(stream: Int, flags: Int) -> Unit
+ ) =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(wakefulnessLifecycle.wakefulness)
+ .thenReturn(WakefulnessLifecycle.WAKEFULNESS_AWAKE)
+ underTest.setVolumeController()
+ runCurrent()
+
+ emitVolumeChange(AudioManager.STREAM_SYSTEM, AudioManager.FLAG_SHOW_UI)
+ runCurrent()
+ TestableLooper.get(this@VolumeDialogControllerImplTestKt).processAllMessages()
+
+ verify(callbacks) { 1 * { onShowRequested(any(), any(), any()) } }
+ }
+ }
+
+ private companion object {
+
+ private fun AudioManager.emitVolumeChange(stream: Int, flags: Int = 0) {
+ val captor = argumentCaptor<IVolumeController>()
+ verify(this) { 1 * { volumeController = captor.capture() } }
+ captor.firstValue.volumeChanged(stream, flags)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
index 88ab170..811c653 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.education.domain.interactor
+import android.hardware.input.InputManager
import com.android.systemui.education.data.repository.fakeEduClock
import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository
import com.android.systemui.keyboard.data.repository.keyboardRepository
@@ -24,6 +25,7 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.touchpad.data.repository.touchpadRepository
import com.android.systemui.user.data.repository.userRepository
+import org.mockito.kotlin.mock
var Kosmos.keyboardTouchpadEduInteractor by
Kosmos.Fixture {
@@ -37,10 +39,13 @@
touchpadRepository,
userRepository
),
- clock = fakeEduClock
+ clock = fakeEduClock,
+ inputManager = mockEduInputManager
)
}
+var Kosmos.mockEduInputManager by Kosmos.Fixture { mock<InputManager>() }
+
var Kosmos.keyboardTouchpadEduStatsInteractor by
Kosmos.Fixture {
KeyboardTouchpadEduStatsInteractorImpl(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt
index 2958315..f1d87fe 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt
@@ -19,6 +19,7 @@
package com.android.systemui.keyguard.ui.viewmodel
import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
import com.android.systemui.keyguard.dismissCallbackRegistry
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
@@ -32,5 +33,6 @@
keyguardTransitionInteractor = keyguardTransitionInteractor,
dismissCallbackRegistry = dismissCallbackRegistry,
alternateBouncerInteractor = { alternateBouncerInteractor },
+ primaryBouncerInteractor = primaryBouncerInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
index 4dd3ae7..2eb1573 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
@@ -35,7 +35,9 @@
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.media.controls.util.MediaFeatureFlag
import com.android.systemui.media.dialog.MediaOutputDialogManager
+import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.shared.system.ActivityManagerWrapper
import com.android.systemui.shared.system.DevicePolicyManagerWrapper
@@ -46,6 +48,7 @@
import com.android.systemui.statusbar.RankingBuilder
import com.android.systemui.statusbar.SmartReplyController
import com.android.systemui.statusbar.notification.ColorUpdateLogger
+import com.android.systemui.statusbar.notification.ConversationNotificationManager
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
@@ -69,6 +72,7 @@
import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger
import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.SmartActionInflaterImpl
import com.android.systemui.statusbar.policy.SmartReplyConstants
@@ -84,6 +88,7 @@
import com.android.systemui.wmshell.BubblesManager
import java.util.Optional
import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.test.TestScope
import org.junit.Assert.assertTrue
@@ -128,19 +133,19 @@
dependency.injectMockDependency(NotificationShadeWindowController::class.java)
dependency.injectMockDependency(MediaOutputDialogManager::class.java)
- mMockLogger = Mockito.mock(ExpandableNotificationRowLogger::class.java)
- mStatusBarStateController = Mockito.mock(StatusBarStateController::class.java)
- mKeyguardBypassController = Mockito.mock(KeyguardBypassController::class.java)
+ mMockLogger = Mockito.mock(ExpandableNotificationRowLogger::class.java, STUB_ONLY)
+ mStatusBarStateController = Mockito.mock(StatusBarStateController::class.java, STUB_ONLY)
+ mKeyguardBypassController = Mockito.mock(KeyguardBypassController::class.java, STUB_ONLY)
mGroupMembershipManager = GroupMembershipManagerImpl()
- mSmartReplyController = Mockito.mock(SmartReplyController::class.java)
+ mSmartReplyController = Mockito.mock(SmartReplyController::class.java, STUB_ONLY)
val dumpManager = DumpManager()
mGroupExpansionManager = GroupExpansionManagerImpl(dumpManager, mGroupMembershipManager)
- mHeadsUpManager = Mockito.mock(HeadsUpManager::class.java)
+ mHeadsUpManager = Mockito.mock(HeadsUpManager::class.java, STUB_ONLY)
mIconManager =
IconManager(
- Mockito.mock(CommonNotifCollection::class.java),
- Mockito.mock(LauncherApps::class.java),
+ Mockito.mock(CommonNotifCollection::class.java, STUB_ONLY),
+ Mockito.mock(LauncherApps::class.java, STUB_ONLY),
IconBuilder(context),
mTestScope,
mBgCoroutineContext,
@@ -173,7 +178,7 @@
}
)
val remoteViewsFactories = getNotifRemoteViewsFactoryContainer(featureFlags)
- val remoteInputManager = Mockito.mock(NotificationRemoteInputManager::class.java)
+ val remoteInputManager = Mockito.mock(NotificationRemoteInputManager::class.java, STUB_ONLY)
val smartReplyStateInflater =
SmartReplyStateInflaterImpl(
constants = mSmartReplyConstants,
@@ -183,7 +188,8 @@
smartRepliesInflater =
SmartReplyInflaterImpl(
constants = mSmartReplyConstants,
- keyguardDismissUtil = mock(),
+ keyguardDismissUtil =
+ Mockito.mock(KeyguardDismissUtil::class.java, STUB_ONLY),
remoteInputManager = remoteInputManager,
smartReplyController = mSmartReplyController,
context = context
@@ -191,7 +197,7 @@
smartActionsInflater =
SmartActionInflaterImpl(
constants = mSmartReplyConstants,
- activityStarter = mock(),
+ activityStarter = Mockito.mock(ActivityStarter::class.java, STUB_ONLY),
smartReplyController = mSmartReplyController,
headsUpManager = mHeadsUpManager
)
@@ -206,41 +212,42 @@
}
val conversationProcessor =
ConversationNotificationProcessor(
- mock(),
- mock(),
+ Mockito.mock(LauncherApps::class.java, STUB_ONLY),
+ Mockito.mock(ConversationNotificationManager::class.java, STUB_ONLY),
)
+
mContentBinder =
if (NotificationRowContentBinderRefactor.isEnabled)
NotificationRowContentBinderImpl(
- mock(),
+ Mockito.mock(NotifRemoteViewCache::class.java, STUB_ONLY),
remoteInputManager,
conversationProcessor,
- mock(),
- mock(),
- mock(),
+ Mockito.mock(RichOngoingNotificationContentExtractor::class.java, STUB_ONLY),
+ Mockito.mock(RichOngoingNotificationViewInflater::class.java, STUB_ONLY),
+ Mockito.mock(Executor::class.java, STUB_ONLY),
smartReplyStateInflater,
notifLayoutInflaterFactoryProvider,
- mock(),
- mock(),
+ Mockito.mock(HeadsUpStyleProvider::class.java, STUB_ONLY),
+ Mockito.mock(NotificationRowContentBinderLogger::class.java, STUB_ONLY),
)
else
NotificationContentInflater(
- mock(),
+ Mockito.mock(NotifRemoteViewCache::class.java, STUB_ONLY),
remoteInputManager,
conversationProcessor,
- mock(),
- mock(),
+ Mockito.mock(MediaFeatureFlag::class.java, STUB_ONLY),
+ Mockito.mock(Executor::class.java, STUB_ONLY),
smartReplyStateInflater,
notifLayoutInflaterFactoryProvider,
- mock(),
- mock(),
+ Mockito.mock(HeadsUpStyleProvider::class.java, STUB_ONLY),
+ Mockito.mock(NotificationRowContentBinderLogger::class.java, STUB_ONLY),
)
mContentBinder.setInflateSynchronously(true)
mBindStage =
RowContentBindStage(
mContentBinder,
- mock(),
- mock(),
+ Mockito.mock(NotifInflationErrorManager::class.java, STUB_ONLY),
+ Mockito.mock(RowContentBindStageLogger::class.java, STUB_ONLY),
)
val collection = Mockito.mock(CommonNotifCollection::class.java)
@@ -248,7 +255,7 @@
mBindPipeline =
NotifBindPipeline(
collection,
- Mockito.mock(NotifBindPipelineLogger::class.java),
+ Mockito.mock(NotifBindPipelineLogger::class.java, STUB_ONLY),
NotificationEntryProcessorFactoryExecutorImpl(mMainExecutor),
)
mBindPipeline.setStage(mBindStage)
@@ -256,9 +263,11 @@
val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java)
Mockito.verify(collection).addCollectionListener(collectionListenerCaptor.capture())
mBindPipelineEntryListener = collectionListenerCaptor.value
- mPeopleNotificationIdentifier = Mockito.mock(PeopleNotificationIdentifier::class.java)
+ mPeopleNotificationIdentifier =
+ Mockito.mock(PeopleNotificationIdentifier::class.java, STUB_ONLY)
mOnUserInteractionCallback = Mockito.mock(OnUserInteractionCallback::class.java)
- mDismissibilityProvider = Mockito.mock(NotificationDismissibilityProvider::class.java)
+ mDismissibilityProvider =
+ Mockito.mock(NotificationDismissibilityProvider::class.java, STUB_ONLY)
val mFutureDismissalRunnable = Mockito.mock(Runnable::class.java)
whenever(
mOnUserInteractionCallback.registerFutureDismissal(
@@ -320,7 +329,10 @@
// set, but we do not want to override an existing value that is needed by a specific test.
val rowInflaterTask =
- RowInflaterTask(mFakeSystemClock, Mockito.mock(RowInflaterTaskLogger::class.java))
+ RowInflaterTask(
+ mFakeSystemClock,
+ Mockito.mock(RowInflaterTaskLogger::class.java, STUB_ONLY)
+ )
val row = rowInflaterTask.inflateSynchronously(context, null, entry)
entry.row = row
@@ -329,7 +341,7 @@
mBindPipeline.manageRow(entry, row)
row.initialize(
entry,
- Mockito.mock(RemoteInputViewSubcomponent.Factory::class.java),
+ Mockito.mock(RemoteInputViewSubcomponent.Factory::class.java, STUB_ONLY),
APP_NAME,
entry.key,
mMockLogger,
@@ -338,23 +350,23 @@
mGroupExpansionManager,
mHeadsUpManager,
mBindStage,
- Mockito.mock(OnExpandClickListener::class.java),
- Mockito.mock(CoordinateOnClickListener::class.java),
+ Mockito.mock(OnExpandClickListener::class.java, STUB_ONLY),
+ Mockito.mock(CoordinateOnClickListener::class.java, STUB_ONLY),
FalsingManagerFake(),
mStatusBarStateController,
mPeopleNotificationIdentifier,
mOnUserInteractionCallback,
- Optional.of(Mockito.mock(BubblesManager::class.java)),
- Mockito.mock(NotificationGutsManager::class.java),
+ Optional.of(Mockito.mock(BubblesManager::class.java, STUB_ONLY)),
+ Mockito.mock(NotificationGutsManager::class.java, STUB_ONLY),
mDismissibilityProvider,
- Mockito.mock(MetricsLogger::class.java),
- Mockito.mock(NotificationChildrenContainerLogger::class.java),
- Mockito.mock(ColorUpdateLogger::class.java),
+ Mockito.mock(MetricsLogger::class.java, STUB_ONLY),
+ Mockito.mock(NotificationChildrenContainerLogger::class.java, STUB_ONLY),
+ Mockito.mock(ColorUpdateLogger::class.java, STUB_ONLY),
mSmartReplyConstants,
mSmartReplyController,
featureFlags,
- Mockito.mock(IStatusBarService::class.java),
- Mockito.mock(UiEventLogger::class.java)
+ Mockito.mock(IStatusBarService::class.java, STUB_ONLY),
+ Mockito.mock(UiEventLogger::class.java, STUB_ONLY)
)
row.setAboveShelfChangedListener { aboveShelf: Boolean -> }
mBindStage.getStageParams(entry).requireContentViews(extraInflationFlags)
@@ -381,6 +393,8 @@
private val Notification.isConversationStyleNotification
get() = extras.getBoolean(IS_CONVERSATION_FLAG, false)
+ private val STUB_ONLY = Mockito.withSettings().stubOnly()
+
fun markAsConversation(builder: Notification.Builder) {
builder.addExtras(bundleOf(IS_CONVERSATION_FLAG to true))
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerCollectorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerAdapterKosmos.kt
similarity index 79%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerCollectorKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerAdapterKosmos.kt
index d60f14c..4045135b9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerCollectorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerAdapterKosmos.kt
@@ -18,6 +18,7 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.volume.data.repository.audioRepository
-val Kosmos.volumeControllerCollector by
- Kosmos.Fixture { VolumeControllerCollector(applicationCoroutineScope) }
+val Kosmos.volumeControllerAdapter by
+ Kosmos.Fixture { VolumeControllerAdapter(applicationCoroutineScope, audioRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt
index 135cb14..1fa6c3f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt
@@ -18,12 +18,16 @@
import android.media.AudioDeviceInfo
import android.media.AudioManager
+import com.android.settingslib.volume.data.model.VolumeControllerEvent
import com.android.settingslib.volume.data.repository.AudioRepository
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.settingslib.volume.shared.model.AudioStreamModel
import com.android.settingslib.volume.shared.model.RingerMode
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
@@ -39,10 +43,26 @@
override val communicationDevice: StateFlow<AudioDeviceInfo?> =
mutableCommunicationDevice.asStateFlow()
+ private val mutableVolumeControllerEvents = MutableSharedFlow<VolumeControllerEvent>(replay = 1)
+ override val volumeControllerEvents: Flow<VolumeControllerEvent>
+ get() = mutableVolumeControllerEvents.asSharedFlow()
+
private val models: MutableMap<AudioStream, MutableStateFlow<AudioStreamModel>> = mutableMapOf()
private val lastAudibleVolumes: MutableMap<AudioStream, Int> = mutableMapOf()
private val deviceCategories: MutableMap<String, Int> = mutableMapOf()
+ private val mutableIsVolumeControllerVisible = MutableStateFlow(false)
+ val isVolumeControllerVisible: StateFlow<Boolean>
+ get() = mutableIsVolumeControllerVisible.asStateFlow()
+
+ private var mutableIsInitialized: Boolean = false
+ val isInitialized: Boolean
+ get() = mutableIsInitialized
+
+ override fun init() {
+ mutableIsInitialized = true
+ }
+
private fun getAudioStreamModelState(
audioStream: AudioStream
): MutableStateFlow<AudioStreamModel> =
@@ -111,4 +131,16 @@
override suspend fun getBluetoothAudioDeviceCategory(bluetoothAddress: String): Int {
return deviceCategories[bluetoothAddress] ?: AudioManager.AUDIO_DEVICE_CATEGORY_UNKNOWN
}
+
+ suspend fun sendVolumeControllerEvent(event: VolumeControllerEvent) {
+ if (isInitialized) {
+ mutableVolumeControllerEvents.emit(event)
+ }
+ }
+
+ override suspend fun notifyVolumeControllerVisible(isVisible: Boolean) {
+ if (isInitialized) {
+ mutableIsVolumeControllerVisible.value = isVisible
+ }
+ }
}
diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java
index 2de4482..1470e9a 100644
--- a/services/core/java/com/android/server/BatteryService.java
+++ b/services/core/java/com/android/server/BatteryService.java
@@ -23,6 +23,7 @@
import static com.android.server.health.Utils.copyV1Battery;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
@@ -67,6 +68,7 @@
import com.android.internal.app.IBatteryStats;
import com.android.internal.logging.MetricsLogger;
+import com.android.internal.os.SomeArgs;
import com.android.internal.util.DumpUtils;
import com.android.server.am.BatteryStatsService;
import com.android.server.health.HealthServiceWrapper;
@@ -207,18 +209,18 @@
private final CopyOnWriteArraySet<BatteryManagerInternal.ChargingPolicyChangeListener>
mChargingPolicyChangeListeners = new CopyOnWriteArraySet<>();
- private Bundle mBatteryChangedOptions = BroadcastOptions.makeBasic()
+ private static final Bundle BATTERY_CHANGED_OPTIONS = BroadcastOptions.makeBasic()
.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
.setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
.toBundle();
/** Used for both connected/disconnected, so match using key */
- private Bundle mPowerOptions = BroadcastOptions.makeBasic()
+ private static final Bundle POWER_OPTIONS = BroadcastOptions.makeBasic()
.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
.setDeliveryGroupMatchingKey("android", Intent.ACTION_POWER_CONNECTED)
.setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
.toBundle();
/** Used for both low/okay, so match using key */
- private Bundle mBatteryOptions = BroadcastOptions.makeBasic()
+ private static final Bundle BATTERY_OPTIONS = BroadcastOptions.makeBasic()
.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
.setDeliveryGroupMatchingKey("android", Intent.ACTION_BATTERY_OKAY)
.setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
@@ -226,11 +228,60 @@
private MetricsLogger mMetricsLogger;
+ private static final int MSG_BROADCAST_BATTERY_CHANGED = 1;
+ private static final int MSG_BROADCAST_POWER_CONNECTION_CHANGED = 2;
+ private static final int MSG_BROADCAST_BATTERY_LOW_OKAY = 3;
+
+ private final Handler.Callback mLocalCallback = msg -> {
+ switch (msg.what) {
+ case MSG_BROADCAST_BATTERY_CHANGED: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final Context context;
+ final Intent intent;
+ try {
+ context = (Context) args.arg1;
+ intent = (Intent) args.arg2;
+ } finally {
+ args.recycle();
+ }
+ broadcastBatteryChangedIntent(context, intent, BATTERY_CHANGED_OPTIONS);
+ return true;
+ }
+ case MSG_BROADCAST_POWER_CONNECTION_CHANGED: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final Context context;
+ final Intent intent;
+ try {
+ context = (Context) args.arg1;
+ intent = (Intent) args.arg2;
+ } finally {
+ args.recycle();
+ }
+ sendBroadcastToAllUsers(context, intent, POWER_OPTIONS);
+ return true;
+ }
+ case MSG_BROADCAST_BATTERY_LOW_OKAY: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final Context context;
+ final Intent intent;
+ try {
+ context = (Context) args.arg1;
+ intent = (Intent) args.arg2;
+ } finally {
+ args.recycle();
+ }
+ sendBroadcastToAllUsers(context, intent, BATTERY_OPTIONS);
+ return true;
+ }
+ }
+ return false;
+ };
+
public BatteryService(Context context) {
super(context);
mContext = context;
- mHandler = new Handler(true /*async*/);
+ mHandler = new Handler(mLocalCallback, true /*async*/);
mLed = new Led(context, getLocalService(LightsManager.class));
mBatteryStats = BatteryStatsService.getService();
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
@@ -660,25 +711,43 @@
final Intent statusIntent = new Intent(Intent.ACTION_POWER_CONNECTED);
statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
statusIntent.putExtra(BatteryManager.EXTRA_SEQUENCE, mSequence);
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL, null,
- mPowerOptions);
- }
- });
+ if (com.android.server.flags.Flags.consolidateBatteryChangeEvents()) {
+ mHandler.removeMessages(MSG_BROADCAST_POWER_CONNECTION_CHANGED);
+ final SomeArgs args = SomeArgs.obtain();
+ args.arg1 = mContext;
+ args.arg2 = statusIntent;
+ mHandler.obtainMessage(MSG_BROADCAST_POWER_CONNECTION_CHANGED, args)
+ .sendToTarget();
+ } else {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL, null,
+ POWER_OPTIONS);
+ }
+ });
+ }
}
else if (mPlugType == 0 && mLastPlugType != 0) {
final Intent statusIntent = new Intent(Intent.ACTION_POWER_DISCONNECTED);
statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
statusIntent.putExtra(BatteryManager.EXTRA_SEQUENCE, mSequence);
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL, null,
- mPowerOptions);
- }
- });
+ if (com.android.server.flags.Flags.consolidateBatteryChangeEvents()) {
+ mHandler.removeMessages(MSG_BROADCAST_POWER_CONNECTION_CHANGED);
+ final SomeArgs args = SomeArgs.obtain();
+ args.arg1 = mContext;
+ args.arg2 = statusIntent;
+ mHandler.obtainMessage(MSG_BROADCAST_POWER_CONNECTION_CHANGED, args)
+ .sendToTarget();
+ } else {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL, null,
+ POWER_OPTIONS);
+ }
+ });
+ }
}
if (shouldSendBatteryLowLocked()) {
@@ -686,26 +755,44 @@
final Intent statusIntent = new Intent(Intent.ACTION_BATTERY_LOW);
statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
statusIntent.putExtra(BatteryManager.EXTRA_SEQUENCE, mSequence);
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL, null,
- mBatteryOptions);
- }
- });
+ if (com.android.server.flags.Flags.consolidateBatteryChangeEvents()) {
+ mHandler.removeMessages(MSG_BROADCAST_BATTERY_LOW_OKAY);
+ final SomeArgs args = SomeArgs.obtain();
+ args.arg1 = mContext;
+ args.arg2 = statusIntent;
+ mHandler.obtainMessage(MSG_BROADCAST_BATTERY_LOW_OKAY, args)
+ .sendToTarget();
+ } else {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL, null,
+ BATTERY_OPTIONS);
+ }
+ });
+ }
} else if (mSentLowBatteryBroadcast &&
mHealthInfo.batteryLevel >= mLowBatteryCloseWarningLevel) {
mSentLowBatteryBroadcast = false;
final Intent statusIntent = new Intent(Intent.ACTION_BATTERY_OKAY);
statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
statusIntent.putExtra(BatteryManager.EXTRA_SEQUENCE, mSequence);
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL, null,
- mBatteryOptions);
- }
- });
+ if (com.android.server.flags.Flags.consolidateBatteryChangeEvents()) {
+ mHandler.removeMessages(MSG_BROADCAST_BATTERY_LOW_OKAY);
+ final SomeArgs args = SomeArgs.obtain();
+ args.arg1 = mContext;
+ args.arg2 = statusIntent;
+ mHandler.obtainMessage(MSG_BROADCAST_BATTERY_LOW_OKAY, args)
+ .sendToTarget();
+ } else {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL, null,
+ BATTERY_OPTIONS);
+ }
+ });
+ }
}
// We are doing this after sending the above broadcasts, so anything processing
@@ -777,8 +864,16 @@
+ ", info:" + mHealthInfo.toString());
}
- mHandler.post(() -> broadcastBatteryChangedIntent(mContext,
- intent, mBatteryChangedOptions));
+ if (com.android.server.flags.Flags.consolidateBatteryChangeEvents()) {
+ mHandler.removeMessages(MSG_BROADCAST_BATTERY_CHANGED);
+ final SomeArgs args = SomeArgs.obtain();
+ args.arg1 = mContext;
+ args.arg2 = intent;
+ mHandler.obtainMessage(MSG_BROADCAST_BATTERY_CHANGED, args).sendToTarget();
+ } else {
+ mHandler.post(() -> broadcastBatteryChangedIntent(mContext,
+ intent, BATTERY_CHANGED_OPTIONS));
+ }
}
private static void broadcastBatteryChangedIntent(Context context, Intent intent,
@@ -1307,6 +1402,12 @@
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private static void sendBroadcastToAllUsers(Context context, Intent intent,
+ Bundle options) {
+ context.sendBroadcastAsUser(intent, UserHandle.ALL, null, options);
+ }
+
private final class Led {
// must match: config_notificationsBatteryLowBehavior in config.xml
static final int LOW_BATTERY_BEHAVIOR_DEFAULT = 0;
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index dc79ab2..643f330 100644
--- a/services/core/java/com/android/server/audio/SoundDoseHelper.java
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -900,8 +900,10 @@
try {
if (!isAbsoluteVolume) {
- mLogger.enqueue(
- SoundDoseEvent.getAbsVolumeAttenuationEvent(/*attenuation=*/0.f, device));
+ if (mSafeMediaVolumeDevices.indexOfKey(device) >= 0) {
+ mLogger.enqueue(SoundDoseEvent.getAbsVolumeAttenuationEvent(/*attenuation=*/0.f,
+ device));
+ }
// remove any possible previous attenuation
soundDose.updateAttenuation(/* attenuationDB= */0.f, device);
@@ -912,8 +914,12 @@
&& safeDevicesContains(device)) {
float attenuationDb = -AudioSystem.getStreamVolumeDB(AudioSystem.STREAM_MUSIC,
(newIndex + 5) / 10, device);
- mLogger.enqueue(
- SoundDoseEvent.getAbsVolumeAttenuationEvent(attenuationDb, device));
+
+ if (mSafeMediaVolumeDevices.indexOfKey(device) >= 0) {
+ mLogger.enqueue(
+ SoundDoseEvent.getAbsVolumeAttenuationEvent(attenuationDb, device));
+ }
+
soundDose.updateAttenuation(attenuationDb, device);
}
} catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/flags/services.aconfig b/services/core/java/com/android/server/flags/services.aconfig
index c361aee..649678c 100644
--- a/services/core/java/com/android/server/flags/services.aconfig
+++ b/services/core/java/com/android/server/flags/services.aconfig
@@ -45,3 +45,14 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "backstage_power"
+ name: "consolidate_battery_change_events"
+ description: "Optimize battery status updates by delivering only the most recent battery information"
+ bug: "361334584"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/services/core/java/com/android/server/wm/ActionChain.java b/services/core/java/com/android/server/wm/ActionChain.java
new file mode 100644
index 0000000..d63044a
--- /dev/null
+++ b/services/core/java/com/android/server/wm/ActionChain.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Slog;
+
+import com.android.window.flags.Flags;
+
+/**
+ * Represents a chain of WM actions where each action is "caused by" the prior action (except the
+ * first one of course). A whole chain is associated with one Transition (in fact, the purpose
+ * of this object is to communicate, to all callees, which transition they are part of).
+ *
+ * A single action is defined as "one logical thing requested of WM". This usually corresponds to
+ * each ingress-point into the process. For example, when starting an activity:
+ * * the first action is to pause the current/top activity.
+ * At this point, control leaves the process while the activity pauses.
+ * * Then WM receives completePause (a new ingress). This is a new action that gets linked
+ * to the prior action. This action involves resuming the next activity, at which point,
+ * control leaves the process again.
+ * * Eventually, when everything is done, we will have formed a chain of actions.
+ *
+ * We don't technically need to hold onto each prior action in the chain once a new action has
+ * been linked to the same transition; however, keeping the whole chain enables improved
+ * debugging and the ability to detect anomalies.
+ */
+public class ActionChain {
+ private static final String TAG = "TransitionChain";
+
+ /**
+ * Normal link type. This means the action was expected and is properly linked to the
+ * current chain.
+ */
+ static final int TYPE_NORMAL = 0;
+
+ /**
+ * This is the "default" link. It means we haven't done anything to properly track this case
+ * so it may or may not be correct. It represents the behavior as if there was no tracking.
+ *
+ * Any type that has "default" behavior uses the global "collecting transition" if it exists,
+ * otherwise it doesn't use any transition.
+ */
+ static final int TYPE_DEFAULT = 1;
+
+ /**
+ * This means the action was performed via a legacy code-path. These should be removed
+ * eventually. This will have the "default" behavior.
+ */
+ static final int TYPE_LEGACY = 2;
+
+ /** This is for a test. */
+ static final int TYPE_TEST = 3;
+
+ /** This is finishing a transition. Collection isn't supported during this. */
+ static final int TYPE_FINISH = 4;
+
+ /**
+ * Something unexpected happened so this action was started to recover from the unexpected
+ * state. This means that a "real" chain-link couldn't be determined. For now, the behavior of
+ * this is the same as "default".
+ */
+ static final int TYPE_FAILSAFE = 5;
+
+ /**
+ * Types of chain links (ie. how is this action associated with the chain it is linked to).
+ * @hide
+ */
+ @IntDef(prefix = { "TYPE_" }, value = {
+ TYPE_NORMAL,
+ TYPE_DEFAULT,
+ TYPE_LEGACY,
+ TYPE_TEST,
+ TYPE_FINISH,
+ TYPE_FAILSAFE
+ })
+ public @interface LinkType {}
+
+ /** Identifies the entry-point of this action. */
+ @NonNull
+ final String mSource;
+
+ /** Reference to ATMS. TEMPORARY! ONLY USE THIS WHEN tracker_plumbing flag is DISABLED! */
+ @Nullable
+ ActivityTaskManagerService mTmpAtm;
+
+ /** The transition that this chain's changes belong to. */
+ @Nullable
+ Transition mTransition;
+
+ /** The previous action in the chain. */
+ @Nullable
+ ActionChain mPrevious = null;
+
+ /** Classification of how this action is connected to the chain. */
+ @LinkType int mType = TYPE_NORMAL;
+
+ /** When this Action started. */
+ long mCreateTimeMs;
+
+ private ActionChain(String source, @LinkType int type, Transition transit) {
+ mSource = source;
+ mCreateTimeMs = System.currentTimeMillis();
+ mType = type;
+ mTransition = transit;
+ if (mTransition != null) {
+ mTransition.recordChain(this);
+ }
+ }
+
+ private Transition getTransition() {
+ if (!Flags.transitTrackerPlumbing()) {
+ return mTmpAtm.getTransitionController().getCollectingTransition();
+ }
+ return mTransition;
+ }
+
+ boolean isFinishing() {
+ return mType == TYPE_FINISH;
+ }
+
+ /**
+ * Some common checks to determine (and report) whether this chain has a collecting transition.
+ */
+ private boolean expectCollecting() {
+ final Transition transition = getTransition();
+ if (transition == null) {
+ Slog.e(TAG, "Can't collect into a chain with no transition");
+ return false;
+ }
+ if (isFinishing()) {
+ Slog.e(TAG, "Trying to collect into a finished transition");
+ return false;
+ }
+ if (transition.mController.getCollectingTransition() != mTransition) {
+ Slog.e(TAG, "Mismatch between current collecting ("
+ + transition.mController.getCollectingTransition() + ") and chain ("
+ + transition + ")");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Helper to collect a container into the associated transition. This will automatically do
+ * nothing if the chain isn't associated with a collecting transition.
+ */
+ void collect(@NonNull WindowContainer wc) {
+ if (!wc.mTransitionController.isShellTransitionsEnabled()) return;
+ if (!expectCollecting()) return;
+ getTransition().collect(wc);
+ }
+
+ /**
+ * An interface for creating and tracking action chains.
+ */
+ static class Tracker {
+ private final ActivityTaskManagerService mAtm;
+
+ Tracker(ActivityTaskManagerService atm) {
+ mAtm = atm;
+ }
+
+ private ActionChain makeChain(String source, @LinkType int type, Transition transit) {
+ final ActionChain out = new ActionChain(source, type, transit);
+ if (!Flags.transitTrackerPlumbing()) {
+ out.mTmpAtm = mAtm;
+ }
+ return out;
+ }
+
+ private ActionChain makeChain(String source, @LinkType int type) {
+ return makeChain(source, type,
+ mAtm.getTransitionController().getCollectingTransition());
+ }
+
+ /**
+ * Starts tracking a normal action.
+ * @see #TYPE_NORMAL
+ */
+ @NonNull
+ ActionChain start(String source, Transition transit) {
+ return makeChain(source, TYPE_NORMAL, transit);
+ }
+
+ /** @see #TYPE_DEFAULT */
+ @NonNull
+ ActionChain startDefault(String source) {
+ return makeChain(source, TYPE_DEFAULT);
+ }
+
+ /**
+ * Starts tracking an action that finishes a transition.
+ * @see #TYPE_NORMAL
+ */
+ @NonNull
+ ActionChain startFinish(String source, Transition finishTransit) {
+ return makeChain(source, TYPE_FINISH, finishTransit);
+ }
+
+ /** @see #TYPE_LEGACY */
+ @NonNull
+ ActionChain startLegacy(String source) {
+ return makeChain(source, TYPE_LEGACY, null);
+ }
+
+ /** @see #TYPE_FAILSAFE */
+ @NonNull
+ ActionChain startFailsafe(String source) {
+ return makeChain(source, TYPE_FAILSAFE);
+ }
+ }
+
+ /** Helpers for usage in tests. */
+ @NonNull
+ static ActionChain test() {
+ return new ActionChain("test", TYPE_TEST, null /* transition */);
+ }
+
+ @NonNull
+ static ActionChain testFinish(Transition toFinish) {
+ return new ActionChain("test", TYPE_FINISH, toFinish);
+ }
+}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 52447e8..fc087e3 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -795,6 +795,7 @@
WindowOrganizerController mWindowOrganizerController;
TaskOrganizerController mTaskOrganizerController;
TaskFragmentOrganizerController mTaskFragmentOrganizerController;
+ ActionChain.Tracker mChainTracker;
@Nullable
private BackgroundActivityStartCallback mBackgroundActivityStartCallback;
@@ -869,6 +870,7 @@
mInternal = new LocalService();
GL_ES_VERSION = SystemProperties.getInt("ro.opengles.version", GL_ES_VERSION_UNDEFINED);
mWindowOrganizerController = new WindowOrganizerController(this);
+ mChainTracker = new ActionChain.Tracker(this);
mTaskOrganizerController = mWindowOrganizerController.mTaskOrganizerController;
mTaskFragmentOrganizerController =
mWindowOrganizerController.mTaskFragmentOrganizerController;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index e25db7e..82ede7e 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -181,7 +181,7 @@
final @TransitionType int mType;
private int mSyncId = -1;
private @TransitionFlags int mFlags;
- private final TransitionController mController;
+ final TransitionController mController;
private final BLASTSyncEngine mSyncEngine;
private final Token mToken;
@@ -329,6 +329,9 @@
*/
ArrayList<ActivityRecord> mConfigAtEndActivities = null;
+ /** The current head of the chain of actions related to this transition. */
+ ActionChain mChainHead = null;
+
@VisibleForTesting
Transition(@TransitionType int type, @TransitionFlags int flags,
TransitionController controller, BLASTSyncEngine syncEngine) {
@@ -1207,10 +1210,14 @@
* The transition has finished animating and is ready to finalize WM state. This should not
* be called directly; use {@link TransitionController#finishTransition} instead.
*/
- void finishTransition() {
+ void finishTransition(@NonNull ActionChain chain) {
if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER) && mIsPlayerEnabled) {
asyncTraceEnd(System.identityHashCode(this));
}
+ if (!chain.isFinishing()) {
+ throw new IllegalStateException("Can't finish on a non-finishing transition "
+ + chain.mTransition);
+ }
mLogger.mFinishTimeNs = SystemClock.elapsedRealtimeNanos();
mController.mLoggerHandler.post(mLogger::logOnFinish);
mController.mTransitionTracer.logFinishedTransition(this);
@@ -2163,7 +2170,7 @@
if (mFinishTransaction != null) {
mFinishTransaction.apply();
}
- mController.finishTransition(this);
+ mController.finishTransition(mController.mAtm.mChainTracker.startFinish("clean-up", this));
}
private void cleanUpInternal() {
@@ -3379,6 +3386,11 @@
return false;
}
+ void recordChain(@NonNull ActionChain chain) {
+ chain.mPrevious = mChainHead;
+ mChainHead = chain;
+ }
+
@VisibleForTesting
static class ChangeInfo {
private static final int FLAG_NONE = 0;
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 9bbf102..56a24dd 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -52,8 +52,8 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.ProtoLogGroup;
import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.ProtoLogGroup;
import com.android.server.FgThread;
import com.android.window.flags.Flags;
@@ -921,7 +921,12 @@
}
/** @see Transition#finishTransition */
- void finishTransition(Transition record) {
+ void finishTransition(@NonNull ActionChain chain) {
+ if (!chain.isFinishing()) {
+ throw new IllegalStateException("Can't finish on a non-finishing transition "
+ + chain.mTransition);
+ }
+ final Transition record = chain.mTransition;
// It is usually a no-op but make sure that the metric consumer is removed.
mTransitionMetricsReporter.reportAnimationStart(record.getToken(), 0 /* startTime */);
// It is a no-op if the transition did not change the display.
@@ -937,7 +942,7 @@
mTrackCount = 0;
}
updateRunningRemoteAnimation(record, false /* isPlaying */);
- record.finishTransition();
+ record.finishTransition(chain);
for (int i = mAnimatingExitWindows.size() - 1; i >= 0; i--) {
final WindowState w = mAnimatingExitWindows.get(i);
if (w.mAnimatingExit && w.mHasSurface && !w.inTransition()) {
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index e1e64ee..60ccdc7 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -223,7 +223,8 @@
final long ident = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) {
- applyTransaction(t, -1 /*syncId*/, null /*transition*/, caller);
+ final ActionChain chain = mService.mChainTracker.startLegacy("applyTransactLegacy");
+ applyTransaction(t, -1 /*syncId*/, chain, caller);
}
} finally {
Binder.restoreCallingIdentity(ident);
@@ -242,7 +243,8 @@
try {
synchronized (mGlobalLock) {
if (callback == null) {
- applyTransaction(t, -1 /* syncId*/, null /*transition*/, caller);
+ final ActionChain chain = mService.mChainTracker.startLegacy("applySyncLegacy");
+ applyTransaction(t, -1 /* syncId*/, chain, caller);
return -1;
}
@@ -262,13 +264,15 @@
final int syncId = syncGroup.mSyncId;
if (mTransitionController.isShellTransitionsEnabled()) {
mTransitionController.startLegacySyncOrQueue(syncGroup, (deferred) -> {
- applyTransaction(t, syncId, null /* transition */, caller, deferred);
+ applyTransaction(t, syncId, mService.mChainTracker.startLegacy(
+ "applySyncLegacy"), caller, deferred);
setSyncReady(syncId);
});
} else {
if (!mService.mWindowManager.mSyncEngine.hasActiveSync()) {
mService.mWindowManager.mSyncEngine.startSyncSet(syncGroup);
- applyTransaction(t, syncId, null /*transition*/, caller);
+ applyTransaction(t, syncId, mService.mChainTracker.startLegacy(
+ "applySyncLegacy"), caller);
setSyncReady(syncId);
} else {
// Because the BLAST engine only supports one sync at a time, queue the
@@ -276,7 +280,8 @@
mService.mWindowManager.mSyncEngine.queueSyncSet(
() -> mService.mWindowManager.mSyncEngine.startSyncSet(syncGroup),
() -> {
- applyTransaction(t, syncId, null /*transition*/, caller);
+ applyTransaction(t, syncId, mService.mChainTracker.startLegacy(
+ "applySyncLegacy"), caller);
setSyncReady(syncId);
});
}
@@ -313,7 +318,8 @@
throw new IllegalArgumentException("Can't use legacy transitions in"
+ " compatibility mode with no WCT.");
}
- applyTransaction(t, -1 /* syncId */, null, caller);
+ applyTransaction(t, -1 /* syncId */,
+ mService.mChainTracker.startLegacy("wrongLegacyTransit"), caller);
return null;
}
final WindowContainerTransaction wct =
@@ -334,10 +340,11 @@
nextTransition.calcParallelCollectType(wct);
mTransitionController.startCollectOrQueue(nextTransition,
(deferred) -> {
+ final ActionChain chain = mService.mChainTracker.start(
+ "startNewTransit", nextTransition);
nextTransition.start();
nextTransition.mLogger.mStartWCT = wct;
- applyTransaction(wct, -1 /* syncId */, nextTransition, caller,
- deferred);
+ applyTransaction(wct, -1 /* syncId */, chain, caller, deferred);
wctApplied.meet();
if (needsSetReady) {
setAllReadyIfNeeded(nextTransition, wct);
@@ -351,7 +358,9 @@
Slog.e(TAG, "Trying to start a transition that isn't collecting. This probably"
+ " means Shell took too long to respond to a request. WM State may be"
+ " incorrect now, please file a bug");
- applyTransaction(wct, -1 /*syncId*/, null /*transition*/, caller);
+ final ActionChain chain = mService.mChainTracker.startFailsafe("startTransit");
+ chain.mTransition = null;
+ applyTransaction(wct, -1 /*syncId*/, chain, caller);
return transition.getToken();
}
// Currently, application of wct can span multiple looper loops (ie.
@@ -367,16 +376,20 @@
if (transition.shouldApplyOnDisplayThread()) {
mService.mH.post(() -> {
synchronized (mService.mGlobalLock) {
+ final ActionChain chain = mService.mChainTracker.start(
+ "startTransit", transition);
transition.start();
- applyTransaction(wct, -1 /* syncId */, transition, caller);
+ applyTransaction(wct, -1 /* syncId */, chain, caller);
if (wctApplied != null) {
wctApplied.meet();
}
}
});
} else {
+ final ActionChain chain = mService.mChainTracker.start("startTransit",
+ transition);
transition.start();
- applyTransaction(wct, -1 /* syncId */, transition, caller);
+ applyTransaction(wct, -1 /* syncId */, chain, caller);
if (wctApplied != null) {
wctApplied.meet();
}
@@ -475,7 +488,8 @@
dc.mAppTransition.overridePendingAppTransitionRemote(adapter, true /* sync */,
false /* isActivityEmbedding */);
syncId = startSyncWithOrganizer(callback);
- applyTransaction(t, syncId, null /* transition */, caller);
+ applyTransaction(t, syncId, mService.mChainTracker.startLegacy("legacyTransit"),
+ caller);
setSyncReady(syncId);
}
} finally {
@@ -493,6 +507,8 @@
try {
synchronized (mGlobalLock) {
final Transition transition = Transition.fromBinder(transitionToken);
+ final ActionChain chain =
+ mService.mChainTracker.startFinish("finishTransit", transition);
// apply the incoming transaction before finish in case it alters the visibility
// of the participants.
if (t != null) {
@@ -500,9 +516,9 @@
// changes of the transition participants will only set visible-requested
// and still let finishTransition handle the participants.
mTransitionController.mFinishingTransition = transition;
- applyTransaction(t, -1 /* syncId */, null /*transition*/, caller, transition);
+ applyTransaction(t, -1 /* syncId */, chain, caller);
}
- mTransitionController.finishTransition(transition);
+ mTransitionController.finishTransition(chain);
mTransitionController.mFinishingTransition = null;
}
} finally {
@@ -537,9 +553,10 @@
final CallerInfo caller = new CallerInfo();
final long ident = Binder.clearCallingIdentity();
try {
- if (mTransitionController.getTransitionPlayer() == null) {
+ if (!mTransitionController.isShellTransitionsEnabled()) {
// No need to worry about transition when Shell transition is not enabled.
- applyTransaction(wct, -1 /* syncId */, null /* transition */, caller);
+ applyTransaction(wct, -1 /* syncId */,
+ mService.mChainTracker.startLegacy("legacyTFTransact"), caller);
return;
}
@@ -548,8 +565,8 @@
// Although there is an active sync, we want to apply the transaction now.
// TODO(b/232042367) Redesign the organizer update on activity callback so that we
// we will know about the transition explicitly.
- final Transition transition = mTransitionController.getCollectingTransition();
- if (transition == null) {
+ final ActionChain chain = mService.mChainTracker.startDefault("tfTransact");
+ if (chain.mTransition == null) {
// This should rarely happen, and we should try to avoid using
// {@link #applySyncTransaction} with Shell transition.
// We still want to apply and merge the transaction to the active sync
@@ -559,7 +576,7 @@
+ " because there is an ongoing sync for"
+ " applySyncTransaction().");
}
- applyTransaction(wct, -1 /* syncId */, transition, caller);
+ applyTransaction(wct, -1 /* syncId */, chain, caller);
return;
}
@@ -570,8 +587,9 @@
transition.abort();
return;
}
- if (applyTransaction(wct, -1 /* syncId */, transition, caller, deferred)
- == TRANSACT_EFFECTS_NONE && transition.mParticipants.isEmpty()) {
+ final ActionChain chain = mService.mChainTracker.start("tfTransact", transition);
+ final int effects = applyTransaction(wct, -1 /* syncId */, chain, caller, deferred);
+ if (effects == TRANSACT_EFFECTS_NONE && transition.mParticipants.isEmpty()) {
transition.abort();
return;
}
@@ -586,15 +604,10 @@
}
private int applyTransaction(@NonNull WindowContainerTransaction t, int syncId,
- @Nullable Transition transition, @NonNull CallerInfo caller) {
- return applyTransaction(t, syncId, transition, caller, null /* finishTransition */);
- }
-
- private int applyTransaction(@NonNull WindowContainerTransaction t, int syncId,
- @Nullable Transition transition, @NonNull CallerInfo caller, boolean deferred) {
+ @NonNull ActionChain chain, @NonNull CallerInfo caller, boolean deferred) {
if (deferred) {
try {
- return applyTransaction(t, syncId, transition, caller);
+ return applyTransaction(t, syncId, chain, caller);
} catch (RuntimeException e) {
// If the transaction is deferred, the caller could be from TransitionController
// #tryStartCollectFromQueue that executes on system's worker thread rather than
@@ -604,19 +617,17 @@
}
return TRANSACT_EFFECTS_NONE;
}
- return applyTransaction(t, syncId, transition, caller);
+ return applyTransaction(t, syncId, chain, caller);
}
/**
* @param syncId If non-null, this will be a sync-transaction.
- * @param transition A transition to collect changes into.
+ * @param chain A lifecycle-chain to acculumate changes into.
* @param caller Info about the calling process.
- * @param finishTransition The transition that is currently being finished.
* @return The effects of the window container transaction.
*/
private int applyTransaction(@NonNull WindowContainerTransaction t, int syncId,
- @Nullable Transition transition, @NonNull CallerInfo caller,
- @Nullable Transition finishTransition) {
+ @NonNull ActionChain chain, @NonNull CallerInfo caller) {
int effects = TRANSACT_EFFECTS_NONE;
ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Apply window transaction, syncId=%d", syncId);
mService.deferWindowLayout();
@@ -624,20 +635,21 @@
boolean deferResume = true;
mService.mTaskSupervisor.setDeferRootVisibilityUpdate(true /* deferUpdate */);
boolean deferTransitionReady = false;
- if (transition != null && !t.isEmpty()) {
- if (transition.isCollecting()) {
+ if (chain.mTransition != null && !t.isEmpty() && !chain.isFinishing()) {
+ if (chain.mTransition.isCollecting()) {
deferTransitionReady = true;
- transition.deferTransitionReady();
+ chain.mTransition.deferTransitionReady();
} else {
Slog.w(TAG, "Transition is not collecting when applyTransaction."
- + " transition=" + transition + " state=" + transition.getState());
- transition = null;
+ + " transition=" + chain.mTransition + " state="
+ + chain.mTransition.getState());
+ chain.mTransition = null;
}
}
try {
final ArraySet<WindowContainer<?>> haveConfigChanges = new ArraySet<>();
- if (transition != null) {
- transition.applyDisplayChangeIfNeeded(haveConfigChanges);
+ if (chain.mTransition != null) {
+ chain.mTransition.applyDisplayChangeIfNeeded(haveConfigChanges);
if (!haveConfigChanges.isEmpty()) {
effects |= TRANSACT_EFFECTS_CLIENT_CONFIG;
}
@@ -645,7 +657,7 @@
final List<WindowContainerTransaction.HierarchyOp> hops = t.getHierarchyOps();
final int hopSize = hops.size();
Iterator<Map.Entry<IBinder, WindowContainerTransaction.Change>> entries;
- if (transition != null) {
+ if (chain.mTransition != null) {
// Mark any config-at-end containers before applying config changes so that
// the config changes don't dispatch to client.
entries = t.getChanges().entrySet().iterator();
@@ -655,7 +667,7 @@
if (!entry.getValue().getConfigAtTransitionEnd()) continue;
final WindowContainer wc = WindowContainer.fromBinder(entry.getKey());
if (wc == null || !wc.isAttached()) continue;
- transition.setConfigAtEnd(wc);
+ chain.mTransition.setConfigAtEnd(wc);
}
}
entries = t.getChanges().entrySet().iterator();
@@ -672,15 +684,13 @@
if (syncId >= 0) {
addToSyncSet(syncId, wc);
}
- if (transition != null) transition.collect(wc);
+ chain.collect(wc);
if ((entry.getValue().getChangeMask()
& WindowContainerTransaction.Change.CHANGE_FORCE_NO_PIP) != 0) {
// Disable entering pip (eg. when recents pretends to finish itself)
- if (finishTransition != null) {
- finishTransition.setCanPipOnFinish(false /* canPipOnFinish */);
- } else if (transition != null) {
- transition.setCanPipOnFinish(false /* canPipOnFinish */);
+ if (chain.mTransition != null) {
+ chain.mTransition.setCanPipOnFinish(false /* canPipOnFinish */);
}
}
// A bit hacky, but we need to detect "remove PiP" so that we can "wrap" the
@@ -728,9 +738,9 @@
if (hopSize > 0) {
final boolean isInLockTaskMode = mService.isInLockTaskMode();
for (int i = 0; i < hopSize; ++i) {
- effects |= applyHierarchyOp(hops.get(i), effects, syncId, transition,
+ effects |= applyHierarchyOp(hops.get(i), effects, syncId, chain,
isInLockTaskMode, caller, t.getErrorCallbackToken(),
- t.getTaskFragmentOrganizer(), finishTransition);
+ t.getTaskFragmentOrganizer());
}
}
// Queue-up bounds-change transactions for tasks which are now organized. Do
@@ -789,7 +799,7 @@
}
} finally {
if (deferTransitionReady) {
- transition.continueTransitionReady();
+ chain.mTransition.continueTransitionReady();
}
mService.mTaskSupervisor.setDeferRootVisibilityUpdate(false /* deferUpdate */);
if (deferResume) {
@@ -1079,9 +1089,9 @@
}
private int applyHierarchyOp(WindowContainerTransaction.HierarchyOp hop, int effects,
- int syncId, @Nullable Transition transition, boolean isInLockTaskMode,
+ int syncId, @NonNull ActionChain chain, boolean isInLockTaskMode,
@NonNull CallerInfo caller, @Nullable IBinder errorCallbackToken,
- @Nullable ITaskFragmentOrganizer organizer, @Nullable Transition finishTransition) {
+ @Nullable ITaskFragmentOrganizer organizer) {
final int type = hop.getType();
switch (type) {
case HIERARCHY_OP_TYPE_REMOVE_TASK: {
@@ -1151,7 +1161,7 @@
break;
}
case HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT: {
- effects |= reparentChildrenTasksHierarchyOp(hop, transition, syncId,
+ effects |= reparentChildrenTasksHierarchyOp(hop, chain.mTransition, syncId,
isInLockTaskMode);
break;
}
@@ -1204,13 +1214,13 @@
if (syncId >= 0) {
addToSyncSet(syncId, wc);
}
- if (transition != null) {
- transition.collect(wc);
+ if (chain.mTransition != null) {
+ chain.mTransition.collect(wc);
if (hop.isReparent()) {
if (wc.getParent() != null) {
// Collect the current parent. It's visibility may change as
// a result of this reparenting.
- transition.collect(wc.getParent());
+ chain.mTransition.collect(wc.getParent());
}
if (hop.getNewParent() != null) {
final WindowContainer parentWc =
@@ -1219,7 +1229,7 @@
Slog.e(TAG, "Can't resolve parent window from token");
break;
}
- transition.collect(parentWc);
+ chain.mTransition.collect(parentWc);
}
}
}
@@ -1233,8 +1243,8 @@
break;
}
case HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION: {
- effects |= applyTaskFragmentOperation(hop, transition, isInLockTaskMode, caller,
- errorCallbackToken, organizer);
+ effects |= applyTaskFragmentOperation(hop, chain, isInLockTaskMode,
+ caller, errorCallbackToken, organizer);
break;
}
case HIERARCHY_OP_TYPE_PENDING_INTENT: {
@@ -1348,13 +1358,13 @@
break;
}
case HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER: {
- if (finishTransition == null) break;
+ if (!chain.isFinishing()) break;
final WindowContainer container = WindowContainer.fromBinder(hop.getContainer());
if (container == null) break;
final Task thisTask = container.asActivityRecord() != null
? container.asActivityRecord().getTask() : container.asTask();
if (thisTask == null) break;
- final Task restoreAt = finishTransition.getTransientLaunchRestoreTarget(container);
+ final Task restoreAt = chain.mTransition.getTransientLaunchRestoreTarget(container);
if (restoreAt == null) break;
final TaskDisplayArea taskDisplayArea = thisTask.getTaskDisplayArea();
taskDisplayArea.moveRootTaskBehindRootTask(thisTask.getRootTask(), restoreAt);
@@ -1444,7 +1454,7 @@
* {@link #TRANSACT_EFFECTS_LIFECYCLE} or {@link #TRANSACT_EFFECTS_CLIENT_CONFIG}.
*/
private int applyTaskFragmentOperation(@NonNull WindowContainerTransaction.HierarchyOp hop,
- @Nullable Transition transition, boolean isInLockTaskMode, @NonNull CallerInfo caller,
+ @NonNull ActionChain chain, boolean isInLockTaskMode, @NonNull CallerInfo caller,
@Nullable IBinder errorCallbackToken, @Nullable ITaskFragmentOrganizer organizer) {
if (!validateTaskFragmentOperation(hop, errorCallbackToken, organizer)) {
return TRANSACT_EFFECTS_NONE;
@@ -1467,7 +1477,7 @@
break;
}
createTaskFragment(taskFragmentCreationParams, errorCallbackToken, caller,
- transition);
+ chain.mTransition);
break;
}
case OP_TYPE_DELETE_TASK_FRAGMENT: {
@@ -1484,7 +1494,7 @@
break;
}
}
- effects |= deleteTaskFragment(taskFragment, transition);
+ effects |= deleteTaskFragment(taskFragment, chain.mTransition);
break;
}
case OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: {
@@ -1533,14 +1543,14 @@
opType, exception);
break;
}
- if (transition != null) {
- transition.collect(activity);
+ if (chain.mTransition != null) {
+ chain.collect(activity);
if (activity.getParent() != null) {
// Collect the current parent. Its visibility may change as a result of
// this reparenting.
- transition.collect(activity.getParent());
+ chain.collect(activity.getParent());
}
- transition.collect(taskFragment);
+ chain.collect(taskFragment);
}
activity.reparent(taskFragment, POSITION_TOP);
effects |= TRANSACT_EFFECTS_LIFECYCLE;
@@ -1696,8 +1706,8 @@
// If any TaskFragment in the Task is collected by the transition, we make the decor
// surface visible in sync with the TaskFragment transition. Otherwise, we make the
// decor surface visible immediately.
- final TaskFragment syncTaskFragment = transition != null
- ? task.getTaskFragment(transition.mParticipants::contains)
+ final TaskFragment syncTaskFragment = chain.mTransition != null
+ ? task.getTaskFragment(chain.mTransition.mParticipants::contains)
: null;
if (syncTaskFragment != null) {
@@ -1749,7 +1759,7 @@
// The decor surface boost/unboost must be applied after the transition is
// completed. Otherwise, the decor surface could be moved before Shell completes
// the transition, causing flicker.
- runAfterTransition(transition, task::commitDecorSurfaceBoostedState);
+ runAfterTransition(chain.mTransition, task::commitDecorSurfaceBoostedState);
}
break;
}
diff --git a/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java
index 70c66de..d33313e 100644
--- a/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java
+++ b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java
@@ -43,7 +43,7 @@
// All desktop mode related flags to be overridden by developer option toggle will be added here
DESKTOP_WINDOWING_MODE(
Flags::enableDesktopWindowingMode, /* shouldOverrideByDevOption= */ true),
- DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, true);
+ DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, false);
private static final String TAG = "DesktopModeFlagsUtil";
// Function called to obtain aconfig flag value.
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 56fca31..52a80b0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -1251,7 +1251,7 @@
final Transition transition = app.mTransitionController.createTransition(TRANSIT_OPEN);
app.mTransitionController.requestStartTransition(transition, app.getTask(),
null /* remoteTransition */, null /* displayChange */);
- app.mTransitionController.collectExistenceChange(app.getTask());
+ transition.collectExistenceChange(app.getTask());
mDisplayContent.setFixedRotationLaunchingAppUnchecked(app);
final AsyncRotationController asyncRotationController =
mDisplayContent.getAsyncRotationController();
@@ -1416,7 +1416,8 @@
activity1.setVisibleRequested(false);
activity2.setVisibleRequested(true);
- openTransition.finishTransition();
+ final ActionChain chain = ActionChain.testFinish(null);
+ openTransition.finishTransition(chain);
// We finished the openTransition. Even though activity1 is visibleRequested=false, since
// the closeTransition animation hasn't played yet, make sure that we didn't commit
@@ -1429,7 +1430,7 @@
// normally.
mWm.mSyncEngine.abort(closeTransition.getSyncId());
- closeTransition.finishTransition();
+ closeTransition.finishTransition(chain);
assertFalse(activity1.isVisible());
assertTrue(activity2.isVisible());
@@ -1449,7 +1450,7 @@
activity1.setState(ActivityRecord.State.INITIALIZING, "test");
activity1.mLaunchTaskBehind = true;
mWm.mSyncEngine.abort(noChangeTransition.getSyncId());
- noChangeTransition.finishTransition();
+ noChangeTransition.finishTransition(chain);
assertTrue(activity1.mLaunchTaskBehind);
}
@@ -1468,7 +1469,7 @@
// We didn't call abort on the transition itself, so it will still run onTransactionReady
// normally.
mWm.mSyncEngine.abort(transition1.getSyncId());
- transition1.finishTransition();
+ transition1.finishTransition(ActionChain.testFinish(transition1));
verify(transitionEndedListener).run();
@@ -1530,7 +1531,7 @@
verify(taskSnapshotController, times(1)).recordSnapshot(eq(task2));
- controller.finishTransition(openTransition);
+ controller.finishTransition(ActionChain.testFinish(openTransition));
// We are now going to simulate closing task1 to return back to (open) task2.
final Transition closeTransition = createTestTransition(TRANSIT_CLOSE, controller);
@@ -1595,7 +1596,7 @@
doReturn(true).when(task1).isTranslucentForTransition();
assertFalse(controller.canApplyDim(task1));
- controller.finishTransition(closeTransition);
+ controller.finishTransition(ActionChain.testFinish(closeTransition));
assertTrue(wasInFinishingTransition[0]);
assertFalse(calledListenerOnOtherDisplay[0]);
assertNull(controller.mFinishingTransition);
@@ -1651,7 +1652,7 @@
// to avoid the latency to resume the current top, i.e. appB.
assertTrue(controller.isTransientVisible(taskRecent));
// The recent is paused after the transient transition is finished.
- controller.finishTransition(transition);
+ controller.finishTransition(ActionChain.testFinish(transition));
assertFalse(controller.isTransientVisible(taskRecent));
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
index 7652861..9602ae2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -407,7 +407,7 @@
final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
token.finishSync(t, token.getSyncGroup(), false /* cancel */);
transit.onTransactionReady(transit.getSyncId(), t);
- dc.mTransitionController.finishTransition(transit);
+ dc.mTransitionController.finishTransition(ActionChain.testFinish(transit));
assertFalse(wallpaperWindow.isVisible());
assertFalse(token.isVisible());
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index bcf4ebc..a215c0a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -2131,7 +2131,7 @@
}
public void finish() {
- mController.finishTransition(mLastTransit);
+ mController.finishTransition(ActionChain.testFinish(mLastTransit));
}
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java
index 1d567b1..c45b99d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java
@@ -39,8 +39,7 @@
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.Mockito;
import perfetto.protos.PerfettoConfig.WindowManagerConfig.LogFrequency;
@@ -50,9 +49,7 @@
@SmallTest
@Presubmit
public class WindowTracingPerfettoTest {
- @Mock
private WindowManagerService mWmMock;
- @Mock
private Choreographer mChoreographer;
private WindowTracing mWindowTracing;
private PerfettoTraceMonitor mTraceMonitor;
@@ -60,7 +57,10 @@
@Before
public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
+ mWmMock = Mockito.mock(WindowManagerService.class);
+ Mockito.doNothing().when(mWmMock).dumpDebugLocked(Mockito.any(), Mockito.anyInt());
+
+ mChoreographer = Mockito.mock(Choreographer.class);
mWindowTracing = new WindowTracingPerfetto(mWmMock, mChoreographer,
new WindowManagerGlobalLock());
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java
index 381e9e4..46b8e3a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java
@@ -234,11 +234,11 @@
FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS
})
- public void isEnabled_dwFlagOn_overrideOff_featureFlagOn_returnsFalse() {
+ public void isEnabled_dwFlagOn_overrideOff_featureFlagOn_returnsTrue() {
setOverride(OVERRIDE_OFF.getSetting());
// Follow override if they exist, and is not equal to default toggle state (dw flag)
- assertThat(DesktopModeFlagsUtil.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isFalse();
+ assertThat(DesktopModeFlagsUtil.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isTrue();
}
@Test
@@ -296,11 +296,11 @@
FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS
})
- public void isEnabled_dwFlagOff_overrideOn_featureFlagOff_returnTrue() {
+ public void isEnabled_dwFlagOff_overrideOn_featureFlagOff_returnFalse() {
setOverride(OVERRIDE_ON.getSetting());
// Follow override if they exist, and is not equal to default toggle state (dw flag)
- assertThat(DesktopModeFlagsUtil.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isTrue();
+ assertThat(DesktopModeFlagsUtil.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isFalse();
}
@Test