Merge "Adding more logs to keyboard/touchpad tutorial" into main
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 4350545..5db79fe 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -8288,12 +8288,12 @@
}
Context c = null;
ApplicationInfo ai = info.applicationInfo;
- if (context.getPackageName().equals(ai.packageName)) {
+ if (context != null && context.getPackageName().equals(ai.packageName)) {
c = context;
} else if (mInitialApplication != null &&
mInitialApplication.getPackageName().equals(ai.packageName)) {
c = mInitialApplication;
- } else {
+ } else if (context != null) {
try {
c = context.createPackageContext(ai.packageName,
Context.CONTEXT_INCLUDE_CODE);
diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java
index 52c84dc..26f919f 100644
--- a/core/java/android/content/pm/LauncherApps.java
+++ b/core/java/android/content/pm/LauncherApps.java
@@ -778,8 +778,18 @@
public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) {
logErrorForInvalidProfileAccess(user);
try {
- return convertToActivityList(mService.getLauncherActivities(mContext.getPackageName(),
- packageName, user), user);
+ final List<LauncherActivityInfo> activityList = convertToActivityList(
+ mService.getLauncherActivities(
+ mContext.getPackageName(),
+ packageName,
+ user
+ ), user);
+ if (activityList.isEmpty()) {
+ // b/350144057
+ Log.d(TAG, "getActivityList: No launchable activities found for"
+ + "packageName=" + packageName + ", user=" + user);
+ }
+ return activityList;
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java
index 4ddf602..b5fb050 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java
@@ -205,10 +205,6 @@
*/
@SuppressWarnings("AndroidFrameworkCompatChange")
public static boolean isCameraDeviceSetupSupported(CameraCharacteristics chars) {
- if (!Flags.featureCombinationQuery()) {
- return false;
- }
-
Integer queryVersion = chars.get(
CameraCharacteristics.INFO_SESSION_CONFIGURATION_QUERY_VERSION);
return queryVersion != null && queryVersion > Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java
index 9bd4860..50c6b5b 100644
--- a/core/java/android/hardware/camera2/params/SessionConfiguration.java
+++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java
@@ -165,12 +165,10 @@
source.readTypedList(outConfigs, OutputConfiguration.CREATOR);
// Ignore the values for hasSessionParameters and settings because we cannot reconstruct
// the CaptureRequest object.
- if (Flags.featureCombinationQuery()) {
- boolean hasSessionParameters = source.readBoolean();
- if (hasSessionParameters) {
- CameraMetadataNative settings = new CameraMetadataNative();
- settings.readFromParcel(source);
- }
+ boolean hasSessionParameters = source.readBoolean();
+ if (hasSessionParameters) {
+ CameraMetadataNative settings = new CameraMetadataNative();
+ settings.readFromParcel(source);
}
if ((inputWidth > 0) && (inputHeight > 0) && (inputFormat != -1)) {
@@ -212,14 +210,12 @@
dest.writeBoolean(/*isMultiResolution*/ false);
}
dest.writeTypedList(mOutputConfigurations);
- if (Flags.featureCombinationQuery()) {
- if (mSessionParameters != null) {
- dest.writeBoolean(/*hasSessionParameters*/true);
- CameraMetadataNative metadata = mSessionParameters.getNativeCopy();
- metadata.writeToParcel(dest, /*flags*/0);
- } else {
- dest.writeBoolean(/*hasSessionParameters*/false);
- }
+ if (mSessionParameters != null) {
+ dest.writeBoolean(/*hasSessionParameters*/true);
+ CameraMetadataNative metadata = mSessionParameters.getNativeCopy();
+ metadata.writeToParcel(dest, /*flags*/0);
+ } else {
+ dest.writeBoolean(/*hasSessionParameters*/false);
}
}
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 1a309c6..983bbc3 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -119,6 +119,13 @@
}
flag {
+ namespace: "input_native"
+ name: "use_key_gesture_event_handler_multi_press_gestures"
+ description: "Use KeyGestureEvent handler APIs to control multi key press gestures"
+ bug: "358569822"
+}
+
+flag {
name: "keyboard_repeat_keys"
namespace: "input_native"
description: "Allow configurable timeout before key repeat and repeat delay rate for key repeats"
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index a4a7a98..1ca4574 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -3763,7 +3763,8 @@
}
private static final String CACHE_KEY_IS_USER_UNLOCKED_PROPERTY =
- "cache_key.is_user_unlocked";
+ PropertyInvalidatedCache.createPropertyName(
+ PropertyInvalidatedCache.MODULE_SYSTEM, "is_user_unlocked");
private final PropertyInvalidatedCache<Integer, Boolean> mIsUserUnlockedCache =
new PropertyInvalidatedCache<Integer, Boolean>(
@@ -6694,7 +6695,9 @@
}
/* Cache key for anything that assumes that userIds cannot be re-used without rebooting. */
- private static final String CACHE_KEY_STATIC_USER_PROPERTIES = "cache_key.static_user_props";
+ private static final String CACHE_KEY_STATIC_USER_PROPERTIES =
+ PropertyInvalidatedCache.createPropertyName(
+ PropertyInvalidatedCache.MODULE_SYSTEM, "static_user_props");
private final PropertyInvalidatedCache<Integer, String> mProfileTypeCache =
new PropertyInvalidatedCache<Integer, String>(32, CACHE_KEY_STATIC_USER_PROPERTIES) {
@@ -6721,7 +6724,9 @@
}
/* Cache key for UserProperties object. */
- private static final String CACHE_KEY_USER_PROPERTIES = "cache_key.user_properties";
+ private static final String CACHE_KEY_USER_PROPERTIES =
+ PropertyInvalidatedCache.createPropertyName(
+ PropertyInvalidatedCache.MODULE_SYSTEM, "user_properties");
// TODO: It would be better to somehow have this as static, so that it can work cross-context.
private final PropertyInvalidatedCache<Integer, UserProperties> mUserPropertiesCache =
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 77dde5e..d45b24e 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -1046,6 +1046,8 @@
DEFAULT_SUPPRESSED_VISUAL_EFFECTS);
} else if (MANUAL_TAG.equals(tag)) {
rt.manualRule = readRuleXml(parser);
+ // manualRule.enabled can never be false, but it was broken in some builds.
+ rt.manualRule.enabled = true;
// Manual rule may be present prior to modes_ui if it were on, but in that
// case it would not have a set policy, so make note of the need to set
// it up later.
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index ad457ce..384add5 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -70,6 +70,7 @@
import android.hardware.HardwareBuffer;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -370,6 +371,7 @@
private float mDefaultDimAmount = 0.05f;
SurfaceControl mBbqSurfaceControl;
BLASTBufferQueue mBlastBufferQueue;
+ IBinder mBbqApplyToken = new Binder();
private SurfaceControl mScreenshotSurfaceControl;
private Point mScreenshotSize = new Point();
@@ -2390,6 +2392,7 @@
if (mBlastBufferQueue == null) {
mBlastBufferQueue = new BLASTBufferQueue("Wallpaper", mBbqSurfaceControl,
width, height, format);
+ mBlastBufferQueue.setApplyToken(mBbqApplyToken);
// We only return the Surface the first time, as otherwise
// it hasn't changed and there is no need to update.
ret = mBlastBufferQueue.createSurface();
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index f8c97eb..53935e8 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -1341,7 +1341,7 @@
public HdrCapabilities getHdrCapabilities() {
synchronized (mLock) {
updateDisplayInfoLocked();
- if (mDisplayInfo.hdrCapabilities == null) {
+ if (mDisplayInfo.hdrCapabilities == null || mDisplayInfo.isForceSdr) {
return null;
}
int[] supportedHdrTypes;
@@ -1363,6 +1363,7 @@
supportedHdrTypes[index++] = enabledType;
}
}
+
return new HdrCapabilities(supportedHdrTypes,
mDisplayInfo.hdrCapabilities.mMaxLuminance,
mDisplayInfo.hdrCapabilities.mMaxAverageLuminance,
@@ -2087,6 +2088,7 @@
/**
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static String stateToString(int state) {
switch (state) {
case STATE_UNKNOWN:
@@ -2109,6 +2111,7 @@
}
/** @hide */
+ @android.ravenwood.annotation.RavenwoodKeep
public static String stateReasonToString(@StateReason int reason) {
switch (reason) {
case STATE_REASON_UNKNOWN:
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 157cec8..cac3e3c 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -230,6 +230,9 @@
/** The formats disabled by user **/
public int[] userDisabledHdrTypes = {};
+ /** When true, all HDR capabilities are disabled **/
+ public boolean isForceSdr;
+
/**
* Indicates whether the display can be switched into a mode with minimal post
* processing.
@@ -440,6 +443,7 @@
&& colorMode == other.colorMode
&& Arrays.equals(supportedColorModes, other.supportedColorModes)
&& Objects.equals(hdrCapabilities, other.hdrCapabilities)
+ && isForceSdr == other.isForceSdr
&& Arrays.equals(userDisabledHdrTypes, other.userDisabledHdrTypes)
&& minimalPostProcessingSupported == other.minimalPostProcessingSupported
&& logicalDensityDpi == other.logicalDensityDpi
@@ -502,6 +506,7 @@
supportedColorModes = Arrays.copyOf(
other.supportedColorModes, other.supportedColorModes.length);
hdrCapabilities = other.hdrCapabilities;
+ isForceSdr = other.isForceSdr;
userDisabledHdrTypes = other.userDisabledHdrTypes;
minimalPostProcessingSupported = other.minimalPostProcessingSupported;
logicalDensityDpi = other.logicalDensityDpi;
@@ -567,6 +572,7 @@
supportedColorModes[i] = source.readInt();
}
hdrCapabilities = source.readParcelable(null, android.view.Display.HdrCapabilities.class);
+ isForceSdr = source.readBoolean();
minimalPostProcessingSupported = source.readBoolean();
logicalDensityDpi = source.readInt();
physicalXDpi = source.readFloat();
@@ -636,6 +642,7 @@
dest.writeInt(supportedColorModes[i]);
}
dest.writeParcelable(hdrCapabilities, flags);
+ dest.writeBoolean(isForceSdr);
dest.writeBoolean(minimalPostProcessingSupported);
dest.writeInt(logicalDensityDpi);
dest.writeFloat(physicalXDpi);
@@ -874,6 +881,8 @@
sb.append(Arrays.toString(appsSupportedModes));
sb.append(", hdrCapabilities ");
sb.append(hdrCapabilities);
+ sb.append(", isForceSdr ");
+ sb.append(isForceSdr);
sb.append(", userDisabledHdrTypes ");
sb.append(Arrays.toString(userDisabledHdrTypes));
sb.append(", minimalPostProcessingSupported ");
diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java
index dd950e8..b21e85a 100644
--- a/core/java/android/view/PointerIcon.java
+++ b/core/java/android/view/PointerIcon.java
@@ -174,24 +174,26 @@
@IntDef(prefix = {"POINTER_ICON_VECTOR_STYLE_FILL_"}, value = {
POINTER_ICON_VECTOR_STYLE_FILL_BLACK,
POINTER_ICON_VECTOR_STYLE_FILL_GREEN,
- POINTER_ICON_VECTOR_STYLE_FILL_YELLOW,
+ POINTER_ICON_VECTOR_STYLE_FILL_RED,
POINTER_ICON_VECTOR_STYLE_FILL_PINK,
- POINTER_ICON_VECTOR_STYLE_FILL_BLUE
+ POINTER_ICON_VECTOR_STYLE_FILL_BLUE,
+ POINTER_ICON_VECTOR_STYLE_FILL_PURPLE
})
@Retention(RetentionPolicy.SOURCE)
public @interface PointerIconVectorStyleFill {}
/** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_BLACK = 0;
/** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_GREEN = 1;
- /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_YELLOW = 2;
+ /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_RED = 2;
/** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_PINK = 3;
/** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_BLUE = 4;
+ /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_PURPLE = 5;
// If adding a PointerIconVectorStyleFill, update END value for {@link SystemSettingsValidators}
/** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_BEGIN =
POINTER_ICON_VECTOR_STYLE_FILL_BLACK;
/** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_END =
- POINTER_ICON_VECTOR_STYLE_FILL_BLUE;
+ POINTER_ICON_VECTOR_STYLE_FILL_PURPLE;
/** @hide */
@IntDef(prefix = {"POINTER_ICON_VECTOR_STYLE_STROKE_"}, value = {
@@ -712,12 +714,14 @@
com.android.internal.R.style.PointerIconVectorStyleFillBlack;
case POINTER_ICON_VECTOR_STYLE_FILL_GREEN ->
com.android.internal.R.style.PointerIconVectorStyleFillGreen;
- case POINTER_ICON_VECTOR_STYLE_FILL_YELLOW ->
- com.android.internal.R.style.PointerIconVectorStyleFillYellow;
+ case POINTER_ICON_VECTOR_STYLE_FILL_RED ->
+ com.android.internal.R.style.PointerIconVectorStyleFillRed;
case POINTER_ICON_VECTOR_STYLE_FILL_PINK ->
com.android.internal.R.style.PointerIconVectorStyleFillPink;
case POINTER_ICON_VECTOR_STYLE_FILL_BLUE ->
com.android.internal.R.style.PointerIconVectorStyleFillBlue;
+ case POINTER_ICON_VECTOR_STYLE_FILL_PURPLE ->
+ com.android.internal.R.style.PointerIconVectorStyleFillPurple;
default -> com.android.internal.R.style.PointerIconVectorStyleFillBlack;
};
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index e10cc28..9ff5031 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -829,6 +829,7 @@
private final SurfaceControl mSurfaceControl = new SurfaceControl();
private BLASTBufferQueue mBlastBufferQueue;
+ private IBinder mBbqApplyToken = new Binder();
private final HdrRenderState mHdrRenderState = new HdrRenderState(this);
@@ -2743,6 +2744,10 @@
mBlastBufferQueue = new BLASTBufferQueue(mTag, mSurfaceControl,
mSurfaceSize.x, mSurfaceSize.y, mWindowAttributes.format);
mBlastBufferQueue.setTransactionHangCallback(sTransactionHangCallback);
+ // If we create and destroy BBQ without recreating the SurfaceControl, we can end up
+ // queuing buffers on multiple apply tokens causing out of order buffer submissions. We
+ // fix this by setting the same apply token on all BBQs created by this VRI.
+ mBlastBufferQueue.setApplyToken(mBbqApplyToken);
Surface blastSurface;
if (addSchandleToVriSurface()) {
blastSurface = mBlastBufferQueue.createSurfaceWithHandle();
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 1c4b16e..cc5e583 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -117,6 +117,13 @@
}
flag {
+ name: "enable_tile_resizing"
+ namespace: "lse_desktop_experience"
+ description: "Enables drawing a divider bar upon tiling tasks left and right in desktop mode for simultaneous resizing"
+ bug: "351769839"
+}
+
+flag {
name: "respect_orientation_change_for_unresizeable"
namespace: "lse_desktop_experience"
description: "Whether to resize task to respect requested orientation change of unresizeable activity"
diff --git a/core/jni/android_graphics_BLASTBufferQueue.cpp b/core/jni/android_graphics_BLASTBufferQueue.cpp
index 70505a4..b9c3bf7 100644
--- a/core/jni/android_graphics_BLASTBufferQueue.cpp
+++ b/core/jni/android_graphics_BLASTBufferQueue.cpp
@@ -16,16 +16,16 @@
#define LOG_TAG "BLASTBufferQueue"
-#include <nativehelper/JNIHelp.h>
-
#include <android_runtime/AndroidRuntime.h>
#include <android_runtime/android_view_Surface.h>
-#include <utils/Log.h>
-#include <utils/RefBase.h>
-
+#include <android_util_Binder.h>
#include <gui/BLASTBufferQueue.h>
#include <gui/Surface.h>
#include <gui/SurfaceComposerClient.h>
+#include <nativehelper/JNIHelp.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
#include "core_jni_helpers.h"
namespace android {
@@ -209,6 +209,12 @@
reinterpret_cast<jlong>(transaction));
}
+static void nativeSetApplyToken(JNIEnv* env, jclass clazz, jlong ptr, jobject applyTokenObject) {
+ sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(ptr);
+ sp<IBinder> token(ibinderForJavaObject(env, applyTokenObject));
+ return queue->setApplyToken(std::move(token));
+}
+
static const JNINativeMethod gMethods[] = {
/* name, signature, funcPtr */
// clang-format off
@@ -227,6 +233,7 @@
{"nativeSetTransactionHangCallback",
"(JLandroid/graphics/BLASTBufferQueue$TransactionHangCallback;)V",
(void*)nativeSetTransactionHangCallback},
+ {"nativeSetApplyToken", "(JLandroid/os/IBinder;)V", (void*)nativeSetApplyToken},
// clang-format on
};
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index dc99634..579dc91 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -1509,7 +1509,7 @@
</style>
<!-- @hide -->
- <style name="PointerIconVectorStyleFillYellow">
+ <style name="PointerIconVectorStyleFillRed">
<item name="pointerIconVectorFill">#F55E57</item>
<item name="pointerIconVectorFillInverse">#F55E57</item>
</style>
@@ -1527,6 +1527,12 @@
</style>
<!-- @hide -->
+ <style name="PointerIconVectorStyleFillPurple">
+ <item name="pointerIconVectorFill">#AD72FF</item>
+ <item name="pointerIconVectorFillInverse">#AD72FF</item>
+ </style>
+
+ <!-- @hide -->
<style name="PointerIconVectorStyleStrokeWhite">
<item name="pointerIconVectorStroke">@color/white</item>
<item name="pointerIconVectorStrokeInverse">@color/black</item>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 1917ecd..0396659 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1705,9 +1705,10 @@
<java-symbol type="style" name="VectorPointer" />
<java-symbol type="style" name="PointerIconVectorStyleFillBlack" />
<java-symbol type="style" name="PointerIconVectorStyleFillGreen" />
- <java-symbol type="style" name="PointerIconVectorStyleFillYellow" />
+ <java-symbol type="style" name="PointerIconVectorStyleFillRed" />
<java-symbol type="style" name="PointerIconVectorStyleFillPink" />
<java-symbol type="style" name="PointerIconVectorStyleFillBlue" />
+ <java-symbol type="style" name="PointerIconVectorStyleFillPurple" />
<java-symbol type="attr" name="pointerIconVectorFill" />
<java-symbol type="style" name="PointerIconVectorStyleStrokeWhite" />
<java-symbol type="style" name="PointerIconVectorStyleStrokeBlack" />
diff --git a/graphics/java/android/graphics/BLASTBufferQueue.java b/graphics/java/android/graphics/BLASTBufferQueue.java
index c52f700..90723b2 100644
--- a/graphics/java/android/graphics/BLASTBufferQueue.java
+++ b/graphics/java/android/graphics/BLASTBufferQueue.java
@@ -17,6 +17,7 @@
package android.graphics;
import android.annotation.NonNull;
+import android.os.IBinder;
import android.view.Surface;
import android.view.SurfaceControl;
@@ -47,6 +48,7 @@
long frameNumber);
private static native void nativeSetTransactionHangCallback(long ptr,
TransactionHangCallback callback);
+ private static native void nativeSetApplyToken(long ptr, IBinder applyToken);
public interface TransactionHangCallback {
void onTransactionHang(String reason);
@@ -204,4 +206,8 @@
public void setTransactionHangCallback(TransactionHangCallback hangCallback) {
nativeSetTransactionHangCallback(mNativeObject, hangCallback);
}
+
+ public void setApplyToken(IBinder applyToken) {
+ nativeSetApplyToken(mNativeObject, applyToken);
+ }
}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
index 9027bf3..88878c6 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
@@ -40,6 +40,7 @@
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
+import android.app.TaskInfo;
import android.app.WindowConfiguration;
import android.graphics.Rect;
import android.util.ArrayMap;
@@ -339,6 +340,52 @@
return target;
}
+ /**
+ * Creates a new RemoteAnimationTarget from the provided change and leash
+ */
+ public static RemoteAnimationTarget newSyntheticTarget(ActivityManager.RunningTaskInfo taskInfo,
+ SurfaceControl leash, @TransitionInfo.TransitionMode int mode, int order,
+ boolean isTranslucent) {
+ int taskId;
+ boolean isNotInRecents;
+ WindowConfiguration windowConfiguration;
+
+ if (taskInfo != null) {
+ taskId = taskInfo.taskId;
+ isNotInRecents = !taskInfo.isRunning;
+ windowConfiguration = taskInfo.configuration.windowConfiguration;
+ } else {
+ taskId = INVALID_TASK_ID;
+ isNotInRecents = true;
+ windowConfiguration = new WindowConfiguration();
+ }
+
+ Rect localBounds = new Rect();
+ RemoteAnimationTarget target = new RemoteAnimationTarget(
+ taskId,
+ newModeToLegacyMode(mode),
+ // TODO: once we can properly sync transactions across process,
+ // then get rid of this leash.
+ leash,
+ isTranslucent,
+ null,
+ // TODO(shell-transitions): we need to send content insets? evaluate how its used.
+ new Rect(0, 0, 0, 0),
+ order,
+ null,
+ localBounds,
+ new Rect(),
+ windowConfiguration,
+ isNotInRecents,
+ null,
+ new Rect(),
+ taskInfo,
+ false,
+ INVALID_WINDOW_TYPE
+ );
+ return target;
+ }
+
private static RemoteAnimationTarget getDividerTarget(TransitionInfo.Change change,
SurfaceControl leash) {
return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index 452d12a..7e6f434 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -46,7 +46,6 @@
import android.util.SparseArray;
import android.view.SurfaceControl;
import android.window.ITaskOrganizerController;
-import android.window.ScreenCapture;
import android.window.StartingWindowInfo;
import android.window.StartingWindowRemovalInfo;
import android.window.TaskAppearedInfo;
@@ -55,7 +54,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.util.FrameworkStatsLog;
-import com.android.wm.shell.common.ScreenshotUtils;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.compatui.CompatUIController;
import com.android.wm.shell.compatui.api.CompatUIHandler;
@@ -74,7 +72,6 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
-import java.util.function.Consumer;
/**
* Unified task organizer for all components in the shell.
@@ -561,19 +558,6 @@
mRecentTasks.ifPresent(recentTasks -> recentTasks.onTaskAdded(info.getTaskInfo()));
}
- /**
- * Take a screenshot of a task.
- */
- public void screenshotTask(RunningTaskInfo taskInfo, Rect crop,
- Consumer<ScreenCapture.ScreenshotHardwareBuffer> consumer) {
- final TaskAppearedInfo info = mTasks.get(taskInfo.taskId);
- if (info == null) {
- return;
- }
- ScreenshotUtils.captureLayer(info.getLeash(), crop, consumer);
- }
-
-
@Override
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
synchronized (mLock) {
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 b151c8b..05a70d8 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
@@ -487,10 +487,11 @@
@Provides
static RecentsTransitionHandler provideRecentsTransitionHandler(
ShellInit shellInit,
+ ShellTaskOrganizer shellTaskOrganizer,
Transitions transitions,
Optional<RecentTasksController> recentTasksController,
HomeTransitionObserver homeTransitionObserver) {
- return new RecentsTransitionHandler(shellInit, transitions,
+ return new RecentsTransitionHandler(shellInit, shellTaskOrganizer, transitions,
recentTasksController.orElse(null), homeTransitionObserver);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index c660000..8077aee 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -20,9 +20,12 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS;
import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
+import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_PIP;
import static android.view.WindowManager.TRANSIT_SLEEP;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
@@ -41,6 +44,7 @@
import android.content.Intent;
import android.graphics.Color;
import android.graphics.Rect;
+import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
@@ -64,6 +68,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.IResultReceiver;
import com.android.internal.protolog.ProtoLog;
+import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -79,10 +84,15 @@
* Handles the Recents (overview) animation. Only one of these can run at a time. A recents
* transition must be created via {@link #startRecentsTransition}. Anything else will be ignored.
*/
-public class RecentsTransitionHandler implements Transitions.TransitionHandler {
+public class RecentsTransitionHandler implements Transitions.TransitionHandler,
+ Transitions.TransitionObserver {
private static final String TAG = "RecentsTransitionHandler";
+ // A placeholder for a synthetic transition that isn't backed by a true system transition
+ public static final IBinder SYNTHETIC_TRANSITION = new Binder();
+
private final Transitions mTransitions;
+ private final ShellTaskOrganizer mShellTaskOrganizer;
private final ShellExecutor mExecutor;
@Nullable
private final RecentTasksController mRecentTasksController;
@@ -99,19 +109,26 @@
private final HomeTransitionObserver mHomeTransitionObserver;
private @Nullable Color mBackgroundColor;
- public RecentsTransitionHandler(ShellInit shellInit, Transitions transitions,
+ public RecentsTransitionHandler(
+ @NonNull ShellInit shellInit,
+ @NonNull ShellTaskOrganizer shellTaskOrganizer,
+ @NonNull Transitions transitions,
@Nullable RecentTasksController recentTasksController,
- HomeTransitionObserver homeTransitionObserver) {
+ @NonNull HomeTransitionObserver homeTransitionObserver) {
+ mShellTaskOrganizer = shellTaskOrganizer;
mTransitions = transitions;
mExecutor = transitions.getMainExecutor();
mRecentTasksController = recentTasksController;
mHomeTransitionObserver = homeTransitionObserver;
if (!Transitions.ENABLE_SHELL_TRANSITIONS) return;
if (recentTasksController == null) return;
- shellInit.addInitCallback(() -> {
- recentTasksController.setTransitionHandler(this);
- transitions.addHandler(this);
- }, this);
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ private void onInit() {
+ mRecentTasksController.setTransitionHandler(this);
+ mTransitions.addHandler(this);
+ mTransitions.registerObserver(this);
}
/** Register a mixer handler. {@see RecentsMixedHandler}*/
@@ -138,17 +155,59 @@
mBackgroundColor = color;
}
+ /**
+ * Starts a new real/synthetic recents transition.
+ */
@VisibleForTesting
public IBinder startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options,
IApplicationThread appThread, IRecentsAnimationRunner listener) {
+ // only care about latest one.
+ mAnimApp = appThread;
+
+ // TODO(b/366021931): Formalize this later
+ final boolean isSyntheticRequest = options.containsKey("is_synthetic_recents_transition");
+ if (isSyntheticRequest) {
+ return startSyntheticRecentsTransition(listener);
+ } else {
+ return startRealRecentsTransition(intent, fillIn, options, listener);
+ }
+ }
+
+ /**
+ * Starts a synthetic recents transition that is not backed by a real WM transition.
+ */
+ private IBinder startSyntheticRecentsTransition(@NonNull IRecentsAnimationRunner listener) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "RecentsTransitionHandler.startRecentsTransition(synthetic)");
+ final RecentsController lastController = getLastController();
+ if (lastController != null) {
+ lastController.cancel(lastController.isSyntheticTransition()
+ ? "existing_running_synthetic_transition"
+ : "existing_running_transition");
+ return null;
+ }
+
+ // Create a new synthetic transition and start it immediately
+ final RecentsController controller = new RecentsController(listener);
+ controller.startSyntheticTransition();
+ mControllers.add(controller);
+ return SYNTHETIC_TRANSITION;
+ }
+
+ /**
+ * Starts a real WM-backed recents transition.
+ */
+ private IBinder startRealRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options,
+ IRecentsAnimationRunner listener) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"RecentsTransitionHandler.startRecentsTransition");
- // only care about latest one.
- mAnimApp = appThread;
- WindowContainerTransaction wct = new WindowContainerTransaction();
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
wct.sendPendingIntent(intent, fillIn, options);
- final RecentsController controller = new RecentsController(listener);
+
+ // Find the mixed handler which should handle this request (if we are in a state where a
+ // mixed handler is needed). This is slightly convoluted because starting the transition
+ // requires the handler, but the mixed handler also needs a reference to the transition.
RecentsMixedHandler mixer = null;
Consumer<IBinder> setTransitionForMixer = null;
for (int i = 0; i < mMixers.size(); ++i) {
@@ -160,12 +219,11 @@
}
final IBinder transition = mTransitions.startTransition(TRANSIT_TO_FRONT, wct,
mixer == null ? this : mixer);
- for (int i = 0; i < mStateListeners.size(); i++) {
- mStateListeners.get(i).onTransitionStarted(transition);
- }
if (mixer != null) {
setTransitionForMixer.accept(transition);
}
+
+ final RecentsController controller = new RecentsController(listener);
if (transition != null) {
controller.setTransition(transition);
mControllers.add(controller);
@@ -187,11 +245,28 @@
return null;
}
- private int findController(IBinder transition) {
+ /**
+ * Returns if there is currently a pending or active recents transition.
+ */
+ @Nullable
+ private RecentsController getLastController() {
+ return !mControllers.isEmpty() ? mControllers.getLast() : null;
+ }
+
+ /**
+ * Finds an existing controller for the provided {@param transition}, or {@code null} if none
+ * exists.
+ */
+ @Nullable
+ @VisibleForTesting
+ RecentsController findController(@NonNull IBinder transition) {
for (int i = mControllers.size() - 1; i >= 0; --i) {
- if (mControllers.get(i).mTransition == transition) return i;
+ final RecentsController controller = mControllers.get(i);
+ if (controller.mTransition == transition) {
+ return controller;
+ }
}
- return -1;
+ return null;
}
@Override
@@ -199,13 +274,12 @@
SurfaceControl.Transaction startTransaction,
SurfaceControl.Transaction finishTransaction,
Transitions.TransitionFinishCallback finishCallback) {
- final int controllerIdx = findController(transition);
- if (controllerIdx < 0) {
+ final RecentsController controller = findController(transition);
+ if (controller == null) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"RecentsTransitionHandler.startAnimation: no controller found");
return false;
}
- final RecentsController controller = mControllers.get(controllerIdx);
final IApplicationThread animApp = mAnimApp;
mAnimApp = null;
if (!controller.start(info, startTransaction, finishTransaction, finishCallback)) {
@@ -221,13 +295,12 @@
public void mergeAnimation(IBinder transition, TransitionInfo info,
SurfaceControl.Transaction t, IBinder mergeTarget,
Transitions.TransitionFinishCallback finishCallback) {
- final int targetIdx = findController(mergeTarget);
- if (targetIdx < 0) {
+ final RecentsController controller = findController(mergeTarget);
+ if (controller == null) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"RecentsTransitionHandler.mergeAnimation: no controller found");
return;
}
- final RecentsController controller = mControllers.get(targetIdx);
controller.merge(info, t, finishCallback);
}
@@ -244,8 +317,21 @@
}
}
+ @Override
+ public void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ RecentsController controller = findController(SYNTHETIC_TRANSITION);
+ if (controller != null) {
+ // Cancel the existing synthetic transition if there is one
+ controller.cancel("incoming_transition");
+ }
+ }
+
/** There is only one of these and it gets reset on finish. */
- private class RecentsController extends IRecentsAnimationController.Stub {
+ @VisibleForTesting
+ class RecentsController extends IRecentsAnimationController.Stub {
+
private final int mInstanceId;
private IRecentsAnimationRunner mListener;
@@ -307,7 +393,8 @@
mDeathHandler = () -> {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.DeathRecipient: binder died", mInstanceId);
- finish(mWillFinishToHome, false /* leaveHint */, null /* finishCb */);
+ finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */,
+ "deathRecipient");
};
try {
mListener.asBinder().linkToDeath(mDeathHandler, 0 /* flags */);
@@ -317,6 +404,9 @@
}
}
+ /**
+ * Sets the started transition for this instance of the recents transition.
+ */
void setTransition(IBinder transition) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.setTransition: id=%s", mInstanceId, transition);
@@ -330,6 +420,10 @@
}
void cancel(boolean toHome, boolean withScreenshots, String reason) {
+ if (cancelSyntheticTransition(reason)) {
+ return;
+ }
+
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.cancel: toHome=%b reason=%s",
mInstanceId, toHome, reason);
@@ -341,7 +435,7 @@
}
}
if (mFinishCB != null) {
- finishInner(toHome, false /* userLeave */, null /* finishCb */);
+ finishInner(toHome, false /* userLeave */, null /* finishCb */, "cancel");
} else {
cleanUp();
}
@@ -436,6 +530,91 @@
}
}
+ /**
+ * Starts a new transition that is not backed by a system transition.
+ */
+ void startSyntheticTransition() {
+ mTransition = SYNTHETIC_TRANSITION;
+
+ // TODO(b/366021931): Update mechanism for pulling the home task, for now add home as
+ // both opening and closing since there's some pre-existing
+ // dependencies on having a closing task
+ final ActivityManager.RunningTaskInfo homeTask =
+ mShellTaskOrganizer.getRunningTasks(DEFAULT_DISPLAY).stream()
+ .filter(task -> task.getActivityType() == ACTIVITY_TYPE_HOME)
+ .findFirst()
+ .get();
+ final RemoteAnimationTarget openingTarget = TransitionUtil.newSyntheticTarget(
+ homeTask, mShellTaskOrganizer.getHomeTaskOverlayContainer(), TRANSIT_OPEN,
+ 0, true /* isTranslucent */);
+ final RemoteAnimationTarget closingTarget = TransitionUtil.newSyntheticTarget(
+ homeTask, mShellTaskOrganizer.getHomeTaskOverlayContainer(), TRANSIT_CLOSE,
+ 0, true /* isTranslucent */);
+ final ArrayList<RemoteAnimationTarget> apps = new ArrayList<>();
+ apps.add(openingTarget);
+ apps.add(closingTarget);
+ try {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.start: calling onAnimationStart with %d apps",
+ mInstanceId, apps.size());
+ mListener.onAnimationStart(this,
+ apps.toArray(new RemoteAnimationTarget[apps.size()]),
+ new RemoteAnimationTarget[0],
+ new Rect(0, 0, 0, 0), new Rect(), new Bundle());
+ for (int i = 0; i < mStateListeners.size(); i++) {
+ mStateListeners.get(i).onAnimationStateChanged(true);
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error starting recents animation", e);
+ cancel("startSynthetricTransition() failed");
+ }
+ }
+
+ /**
+ * Returns whether this transition is backed by a real system transition or not.
+ */
+ boolean isSyntheticTransition() {
+ return mTransition == SYNTHETIC_TRANSITION;
+ }
+
+ /**
+ * Called when a synthetic transition is canceled.
+ */
+ boolean cancelSyntheticTransition(String reason) {
+ if (!isSyntheticTransition()) {
+ return false;
+ }
+
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.cancelSyntheticTransition reason=%s",
+ mInstanceId, reason);
+ try {
+ // TODO(b/366021931): Notify the correct tasks once we build actual targets, and
+ // clean up leashes accordingly
+ mListener.onAnimationCanceled(new int[0], new TaskSnapshot[0]);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error canceling previous recents animation", e);
+ }
+ cleanUp();
+ return true;
+ }
+
+ /**
+ * Called when a synthetic transition is finished.
+ * @return
+ */
+ boolean finishSyntheticTransition() {
+ if (!isSyntheticTransition()) {
+ return false;
+ }
+
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.finishSyntheticTransition", mInstanceId);
+ // TODO(b/366021931): Clean up leashes accordingly
+ cleanUp();
+ return true;
+ }
+
boolean start(TransitionInfo info, SurfaceControl.Transaction t,
SurfaceControl.Transaction finishT, Transitions.TransitionFinishCallback finishCB) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
@@ -662,7 +841,7 @@
// Set the callback once again so we can finish correctly.
mFinishCB = finishCB;
finishInner(true /* toHome */, false /* userLeave */,
- null /* finishCb */);
+ null /* finishCb */, "takeOverAnimation");
}, updatedStates);
});
}
@@ -810,7 +989,7 @@
sendCancelWithSnapshots();
mExecutor.executeDelayed(
() -> finishInner(true /* toHome */, false /* userLeaveHint */,
- null /* finishCb */), 0);
+ null /* finishCb */, "merge"), 0);
return;
}
if (recentsOpening != null) {
@@ -1005,7 +1184,7 @@
return;
}
final int displayId = mInfo.getRootCount() > 0 ? mInfo.getRoot(0).getDisplayId()
- : Display.DEFAULT_DISPLAY;
+ : DEFAULT_DISPLAY;
// transient launches don't receive focus automatically. Since we are taking over
// the gesture now, take focus explicitly.
// This also moves recents back to top if the user gestured before a switch
@@ -1038,11 +1217,16 @@
@Override
@SuppressLint("NewApi")
public void finish(boolean toHome, boolean sendUserLeaveHint, IResultReceiver finishCb) {
- mExecutor.execute(() -> finishInner(toHome, sendUserLeaveHint, finishCb));
+ mExecutor.execute(() -> finishInner(toHome, sendUserLeaveHint, finishCb,
+ "requested"));
}
private void finishInner(boolean toHome, boolean sendUserLeaveHint,
- IResultReceiver runnerFinishCb) {
+ IResultReceiver runnerFinishCb, String reason) {
+ if (finishSyntheticTransition()) {
+ return;
+ }
+
if (mFinishCB == null) {
Slog.e(TAG, "Duplicate call to finish");
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java
index e8733eb..95874c8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java
@@ -24,7 +24,4 @@
/** Notifies whether the recents animation is running. */
default void onAnimationStateChanged(boolean running) {
}
-
- /** Notifies that a recents shell transition has started. */
- default void onTransitionStarted(IBinder transition) {}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 2c02d4f..d03832d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -1501,16 +1501,16 @@
* transition animation. The Transition system will apply it when
* finishCallback is called by the transition handler.
*/
- void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ default void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction);
+ @NonNull SurfaceControl.Transaction finishTransaction) {}
/**
* Called when the transition is starting to play. It isn't called for merged transitions.
*
* @param transition the unique token of this transition
*/
- void onTransitionStarting(@NonNull IBinder transition);
+ default void onTransitionStarting(@NonNull IBinder transition) {}
/**
* Called when a transition is merged into another transition. There won't be any following
@@ -1519,7 +1519,7 @@
* @param merged the unique token of the transition that's merged to another one
* @param playing the unique token of the transition that accepts the merge
*/
- void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing);
+ default void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {}
/**
* Called when the transition is finished. This isn't called for merged transitions.
@@ -1527,7 +1527,7 @@
* @param transition the unique token of this transition
* @param aborted {@code true} if this transition is aborted; {@code false} otherwise.
*/
- void onTransitionFinished(@NonNull IBinder transition, boolean aborted);
+ default void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {}
}
@BinderThread
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index d43ee44..b1fc55f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -753,9 +753,12 @@
final ActivityInfo activityInfo = pm.getActivityInfo(baseActivity, 0 /* flags */);
final IconProvider provider = new IconProvider(mContext);
final Drawable appIconDrawable = provider.getIcon(activityInfo);
+ final Drawable badgedAppIconDrawable = pm.getUserBadgedIcon(appIconDrawable,
+ UserHandle.of(mTaskInfo.userId));
final BaseIconFactory headerIconFactory = createIconFactory(mContext,
R.dimen.desktop_mode_caption_icon_radius);
- mAppIconBitmap = headerIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT);
+ mAppIconBitmap = headerIconFactory.createIconBitmap(badgedAppIconDrawable,
+ 1f /* scale */);
final BaseIconFactory resizeVeilIconFactory = createIconFactory(mContext,
R.dimen.desktop_mode_resize_veil_icon_size);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 8258890..b47201e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -18,7 +18,9 @@
import android.app.ActivityManager.RecentTaskInfo
import android.app.ActivityManager.RunningTaskInfo
+import android.app.ActivityOptions
import android.app.KeyguardManager
+import android.app.PendingIntent
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
@@ -43,6 +45,7 @@
import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
import android.view.Display.DEFAULT_DISPLAY
+import android.view.DragEvent
import android.view.Gravity
import android.view.SurfaceControl
import android.view.WindowManager
@@ -107,6 +110,7 @@
import com.android.wm.shell.transition.Transitions.TransitionHandler
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import java.util.function.Consumer
import java.util.Optional
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
@@ -131,8 +135,8 @@
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.atLeastOnce
-import org.mockito.kotlin.eq
import org.mockito.kotlin.capture
+import org.mockito.kotlin.eq
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
@@ -3082,6 +3086,95 @@
assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
}
+
+ @Test
+ fun onUnhandledDrag_newFreeformIntent() {
+ testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR,
+ PointF(1200f, 700f),
+ Rect(240, 700, 2160, 1900))
+ }
+
+ @Test
+ fun onUnhandledDrag_newFreeformIntentSplitLeft() {
+ testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR,
+ PointF(50f, 700f),
+ Rect(0, 0, 500, 1000))
+ }
+
+ @Test
+ fun onUnhandledDrag_newFreeformIntentSplitRight() {
+ testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR,
+ PointF(2500f, 700f),
+ Rect(500, 0, 1000, 1000))
+ }
+
+ @Test
+ fun onUnhandledDrag_newFullscreenIntent() {
+ testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR,
+ PointF(1200f, 50f),
+ Rect())
+ }
+
+ /**
+ * Assert that an unhandled drag event launches a PendingIntent with the
+ * windowing mode and bounds we are expecting.
+ */
+ private fun testOnUnhandledDrag(
+ indicatorType: DesktopModeVisualIndicator.IndicatorType,
+ inputCoordinate: PointF,
+ expectedBounds: Rect
+ ) {
+ setUpLandscapeDisplay()
+ val task = setUpFreeformTask()
+ markTaskVisible(task)
+ task.isFocused = true
+ val runningTasks = ArrayList<RunningTaskInfo>()
+ runningTasks.add(task)
+ val spyController = spy(controller)
+ val mockPendingIntent = mock(PendingIntent::class.java)
+ val mockDragEvent = mock(DragEvent::class.java)
+ val mockCallback = mock(Consumer::class.java)
+ val b = SurfaceControl.Builder()
+ b.setName("test surface")
+ val dragSurface = b.build()
+ whenever(shellTaskOrganizer.runningTasks).thenReturn(runningTasks)
+ whenever(mockDragEvent.dragSurface).thenReturn(dragSurface)
+ whenever(mockDragEvent.x).thenReturn(inputCoordinate.x)
+ whenever(mockDragEvent.y).thenReturn(inputCoordinate.y)
+ whenever(multiInstanceHelper.supportsMultiInstanceSplit(anyOrNull())).thenReturn(true)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ doReturn(indicatorType)
+ .whenever(spyController).updateVisualIndicator(
+ eq(task),
+ anyOrNull(),
+ anyOrNull(),
+ anyOrNull(),
+ eq(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT)
+ )
+
+ spyController.onUnhandledDrag(
+ mockPendingIntent,
+ mockDragEvent,
+ mockCallback as Consumer<Boolean>
+ )
+ val arg: ArgumentCaptor<WindowContainerTransaction> =
+ ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ var expectedWindowingMode: Int
+ if (indicatorType == DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) {
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN
+ // Fullscreen launches currently use default transitions
+ verify(transitions).startTransition(any(), capture(arg), anyOrNull())
+ } else {
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM
+ // All other launches use a special handler.
+ verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg))
+ }
+ assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions)
+ .launchWindowingMode).isEqualTo(expectedWindowingMode)
+ assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions)
+ .launchBounds).isEqualTo(expectedBounds)
+ }
+
private val desktopWallpaperIntent: Intent
get() = Intent(context, DesktopWallpaperActivity::class.java)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
new file mode 100644
index 0000000..769acf7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2021 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.recents;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityTaskManager;
+import android.app.IApplicationThread;
+import android.app.KeyguardManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.HomeTransitionObserver;
+import com.android.wm.shell.transition.Transitions;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.quality.Strictness;
+
+import java.util.Optional;
+
+/**
+ * Tests for {@link RecentTasksController}
+ *
+ * Usage: atest WMShellUnitTests:RecentsTransitionHandlerTest
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RecentsTransitionHandlerTest extends ShellTestCase {
+
+ @Mock
+ private Context mContext;
+ @Mock
+ private TaskStackListenerImpl mTaskStackListener;
+ @Mock
+ private ShellCommandHandler mShellCommandHandler;
+ @Mock
+ private DesktopModeTaskRepository mDesktopModeTaskRepository;
+ @Mock
+ private ActivityTaskManager mActivityTaskManager;
+ @Mock
+ private DisplayInsetsController mDisplayInsetsController;
+ @Mock
+ private IRecentTasksListener mRecentTasksListener;
+ @Mock
+ private TaskStackTransitionObserver mTaskStackTransitionObserver;
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private ShellTaskOrganizer mShellTaskOrganizer;
+ private RecentTasksController mRecentTasksController;
+ private RecentTasksController mRecentTasksControllerReal;
+ private RecentsTransitionHandler mRecentsTransitionHandler;
+ private ShellInit mShellInit;
+ private ShellController mShellController;
+ private TestShellExecutor mMainExecutor;
+ private static StaticMockitoSession sMockitoSession;
+
+ @Before
+ public void setUp() {
+ sMockitoSession = mockitoSession().initMocks(this).strictness(Strictness.LENIENT)
+ .mockStatic(DesktopModeStatus.class).startMocking();
+ ExtendedMockito.doReturn(true)
+ .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+
+ mMainExecutor = new TestShellExecutor();
+ when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
+ when(mContext.getSystemService(KeyguardManager.class))
+ .thenReturn(mock(KeyguardManager.class));
+ mShellInit = spy(new ShellInit(mMainExecutor));
+ mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler,
+ mDisplayInsetsController, mMainExecutor));
+ mRecentTasksControllerReal = new RecentTasksController(mContext, mShellInit,
+ mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager,
+ Optional.of(mDesktopModeTaskRepository), mTaskStackTransitionObserver,
+ mMainExecutor);
+ mRecentTasksController = spy(mRecentTasksControllerReal);
+ mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler,
+ null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController),
+ mMainExecutor);
+
+ final Transitions transitions = mock(Transitions.class);
+ doReturn(mMainExecutor).when(transitions).getMainExecutor();
+ mRecentsTransitionHandler = new RecentsTransitionHandler(mShellInit, mShellTaskOrganizer,
+ transitions, mRecentTasksController, mock(HomeTransitionObserver.class));
+
+ mShellInit.init();
+ }
+
+ @After
+ public void tearDown() {
+ sMockitoSession.finishMocking();
+ }
+
+ @Test
+ public void testStartSyntheticRecentsTransition_callsOnAnimationStart() throws Exception {
+ final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class);
+ doReturn(new Binder()).when(runner).asBinder();
+ Bundle options = new Bundle();
+ options.putBoolean("is_synthetic_recents_transition", true);
+ IBinder transition = mRecentsTransitionHandler.startRecentsTransition(
+ mock(PendingIntent.class), new Intent(), options, mock(IApplicationThread.class),
+ runner);
+ verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any());
+
+ // Finish and verify no transition remains
+ mRecentsTransitionHandler.findController(transition).finish(true /* toHome */,
+ false /* sendUserLeaveHint */, null /* finishCb */);
+ mMainExecutor.flushAll();
+ assertNull(mRecentsTransitionHandler.findController(transition));
+ }
+
+ @Test
+ public void testStartSyntheticRecentsTransition_callsOnAnimationCancel() throws Exception {
+ final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class);
+ doReturn(new Binder()).when(runner).asBinder();
+ Bundle options = new Bundle();
+ options.putBoolean("is_synthetic_recents_transition", true);
+ IBinder transition = mRecentsTransitionHandler.startRecentsTransition(
+ mock(PendingIntent.class), new Intent(), options, mock(IApplicationThread.class),
+ runner);
+ verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any());
+
+ mRecentsTransitionHandler.findController(transition).cancel("test");
+ mMainExecutor.flushAll();
+ verify(runner).onAnimationCanceled(any(), any());
+ assertNull(mRecentsTransitionHandler.findController(transition));
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index 61a725f..fec9e3e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -1211,7 +1211,7 @@
mTransactionPool, createTestDisplayController(), mMainExecutor,
mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
final RecentsTransitionHandler recentsHandler =
- new RecentsTransitionHandler(shellInit, transitions,
+ new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions,
mock(RecentTasksController.class), mock(HomeTransitionObserver.class));
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
shellInit.init();
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index 25c063d6..202535d 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -273,6 +273,7 @@
ASurfaceTransaction_fromJava; # introduced=34
ASurfaceTransaction_reparent; # introduced=29
ASurfaceTransaction_setBuffer; # introduced=29
+ ASurfaceTransaction_setBufferWithRelease; # introduced=36
ASurfaceTransaction_setBufferAlpha; # introduced=29
ASurfaceTransaction_setBufferDataSpace; # introduced=29
ASurfaceTransaction_setBufferTransparency; # introduced=29
diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp
index 6ce83cd..e46db6b 100644
--- a/native/android/surface_control.cpp
+++ b/native/android/surface_control.cpp
@@ -416,6 +416,35 @@
transaction->setBuffer(surfaceControl, graphic_buffer, fence);
}
+void ASurfaceTransaction_setBufferWithRelease(
+ ASurfaceTransaction* aSurfaceTransaction, ASurfaceControl* aSurfaceControl,
+ AHardwareBuffer* buffer, int acquire_fence_fd, void* _Null_unspecified context,
+ ASurfaceTransaction_OnBufferRelease aReleaseCallback) {
+ CHECK_NOT_NULL(aSurfaceTransaction);
+ CHECK_NOT_NULL(aSurfaceControl);
+ CHECK_NOT_NULL(aReleaseCallback);
+
+ sp<SurfaceControl> surfaceControl = ASurfaceControl_to_SurfaceControl(aSurfaceControl);
+ Transaction* transaction = ASurfaceTransaction_to_Transaction(aSurfaceTransaction);
+
+ sp<GraphicBuffer> graphic_buffer(GraphicBuffer::fromAHardwareBuffer(buffer));
+
+ std::optional<sp<Fence>> fence = std::nullopt;
+ if (acquire_fence_fd != -1) {
+ fence = new Fence(acquire_fence_fd);
+ }
+
+ ReleaseBufferCallback releaseBufferCallback =
+ [context,
+ aReleaseCallback](const ReleaseCallbackId&, const sp<Fence>& releaseFence,
+ std::optional<uint32_t> /* currentMaxAcquiredBufferCount */) {
+ (*aReleaseCallback)(context, (releaseFence) ? releaseFence->dup() : -1);
+ };
+
+ transaction->setBuffer(surfaceControl, graphic_buffer, fence, /* frameNumber */ std::nullopt,
+ /* producerId */ 0, releaseBufferCallback);
+}
+
void ASurfaceTransaction_setGeometry(ASurfaceTransaction* aSurfaceTransaction,
ASurfaceControl* aSurfaceControl, const ARect& source,
const ARect& destination, int32_t transform) {
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index feee89a..0b094a2 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1409,6 +1409,8 @@
<string name="media_transfer_this_device_name">This phone</string>
<!-- Name of the tablet device. [CHAR LIMIT=30] -->
<string name="media_transfer_this_device_name_tablet">This tablet</string>
+ <!-- Name of the internal speaker. [CHAR LIMIT=30] -->
+ <string name="media_transfer_this_device_name_desktop">This computer (internal)</string>
<!-- Name of the default media output of the TV. [CHAR LIMIT=30] -->
<string name="media_transfer_this_device_name_tv">@string/tv_media_transfer_default</string>
<!-- Name of the internal mic. [CHAR LIMIT=30] -->
@@ -1639,6 +1641,12 @@
<!-- Name of the 3.5mm and usb audio device. [CHAR LIMIT=50] -->
<string name="media_transfer_wired_usb_device_name">Wired headphone</string>
+ <!-- Name of the 3.5mm headphone, used in desktop devices. [CHAR LIMIT=50] -->
+ <string name="media_transfer_headphone_name">Headphone</string>
+
+ <!-- Name of the usb audio device speaker, used in desktop devices. [CHAR LIMIT=50] -->
+ <string name="media_transfer_usb_speaker_name">USB speaker</string>
+
<!-- Name of the 3.5mm audio device mic. [CHAR LIMIT=50] -->
<string name="media_transfer_wired_device_mic_name">Mic jack</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
index 548eb3f..874e030 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
@@ -57,7 +57,7 @@
}
};
- /* package */ InputRouteManager(@NonNull Context context, @NonNull AudioManager audioManager) {
+ public InputRouteManager(@NonNull Context context, @NonNull AudioManager audioManager) {
mContext = context;
mAudioManager = audioManager;
Handler handler = new Handler(context.getMainLooper());
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
index 9eaf8d3..116de56 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
@@ -72,6 +72,8 @@
return context.getString(R.string.media_transfer_this_device_name_tv);
} else if (isTablet()) {
return context.getString(R.string.media_transfer_this_device_name_tablet);
+ } else if (inputRoutingEnabledAndIsDesktop()) {
+ return context.getString(R.string.media_transfer_this_device_name_desktop);
} else {
return context.getString(R.string.media_transfer_this_device_name);
}
@@ -85,10 +87,18 @@
switch (routeInfo.getType()) {
case TYPE_WIRED_HEADSET:
case TYPE_WIRED_HEADPHONES:
+ name =
+ inputRoutingEnabledAndIsDesktop()
+ ? context.getString(R.string.media_transfer_headphone_name)
+ : context.getString(R.string.media_transfer_wired_usb_device_name);
+ break;
case TYPE_USB_DEVICE:
case TYPE_USB_HEADSET:
case TYPE_USB_ACCESSORY:
- name = context.getString(R.string.media_transfer_wired_usb_device_name);
+ name =
+ inputRoutingEnabledAndIsDesktop()
+ ? context.getString(R.string.media_transfer_usb_speaker_name)
+ : context.getString(R.string.media_transfer_wired_usb_device_name);
break;
case TYPE_DOCK:
name = context.getString(R.string.media_transfer_dock_speaker_device_name);
@@ -139,6 +149,16 @@
.contains("tablet");
}
+ static boolean isDesktop() {
+ return Arrays.asList(SystemProperties.get("ro.build.characteristics").split(","))
+ .contains("desktop");
+ }
+
+ static boolean inputRoutingEnabledAndIsDesktop() {
+ return com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl()
+ && isDesktop();
+ }
+
// MediaRoute2Info.getType was made public on API 34, but exists since API 30.
@SuppressWarnings("NewApi")
@Override
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
index e2d58d6..23cfc01 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
@@ -47,6 +47,7 @@
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowSystemProperties;
@RunWith(RobolectricTestRunner.class)
public class PhoneMediaDeviceTest {
@@ -114,6 +115,31 @@
when(mInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
+ assertThat(mPhoneMediaDevice.getName()).isEqualTo(getMediaTransferThisDeviceName(mContext));
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+ @Test
+ public void getName_returnCorrectName_desktop() {
+ ShadowSystemProperties.override("ro.build.characteristics", "desktop");
+
+ when(mInfo.getType()).thenReturn(TYPE_WIRED_HEADPHONES);
+
+ assertThat(mPhoneMediaDevice.getName())
+ .isEqualTo(mContext.getString(R.string.media_transfer_headphone_name));
+
+ when(mInfo.getType()).thenReturn(TYPE_WIRED_HEADSET);
+
+ assertThat(mPhoneMediaDevice.getName())
+ .isEqualTo(mContext.getString(R.string.media_transfer_headphone_name));
+
+ when(mInfo.getType()).thenReturn(TYPE_USB_DEVICE);
+
+ assertThat(mPhoneMediaDevice.getName())
+ .isEqualTo(mContext.getString(R.string.media_transfer_usb_speaker_name));
+
+ when(mInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
+
assertThat(mPhoneMediaDevice.getName())
.isEqualTo(getMediaTransferThisDeviceName(mContext));
}
diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp
index c107ff5..1a99d25 100644
--- a/packages/SettingsProvider/Android.bp
+++ b/packages/SettingsProvider/Android.bp
@@ -36,6 +36,7 @@
"aconfig_new_storage_flags_lib",
"aconfigd_java_utils",
"aconfig_demo_flags_java_lib",
+ "configinfra_framework_flags_java_lib",
"device_config_service_flags_java",
"libaconfig_java_proto_lite",
"SettingsLibDeviceStateRotationLock",
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
index c9ad5a5..fbce6ca 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
@@ -99,13 +99,23 @@
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- pw.print("SyncDisabledForTests: ");
- MyShellCommand.getSyncDisabledForTests(pw, pw);
+ if (android.provider.flags.Flags.dumpImprovements()) {
+ pw.print("SyncDisabledForTests: ");
+ MyShellCommand.getSyncDisabledForTests(pw, pw);
- pw.print("Is mainline: ");
- pw.println(UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService());
+ pw.print("UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService(): ");
+ pw.println(UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService());
- final IContentProvider iprovider = mProvider.getIContentProvider();
+ pw.println("DeviceConfig provider: ");
+ try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(fd)) {
+ DeviceConfig.dump(pfd, pw, /* prefix= */ " ", args);
+ } catch (IOException e) {
+ pw.print("IOException creating ParcelFileDescriptor: ");
+ pw.println(e);
+ }
+ }
+
+ IContentProvider iprovider = mProvider.getIContentProvider();
pw.println("DeviceConfig flags:");
for (String line : MyShellCommand.listAll(iprovider)) {
pw.println(line);
@@ -251,22 +261,13 @@
public static HashMap<String, String> getAllFlags(IContentProvider provider) {
HashMap<String, String> allFlags = new HashMap<String, String>();
- try {
- Bundle args = new Bundle();
- args.putInt(Settings.CALL_METHOD_USER_KEY,
- ActivityManager.getService().getCurrentUser().id);
- Bundle b = provider.call(new AttributionSource(Process.myUid(),
- resolveCallingPackage(), null), Settings.AUTHORITY,
- Settings.CALL_METHOD_LIST_CONFIG, null, args);
- if (b != null) {
- Map<String, String> flagsToValues =
- (HashMap) b.getSerializable(Settings.NameValueTable.VALUE);
- allFlags.putAll(flagsToValues);
+ for (DeviceConfig.Properties properties : DeviceConfig.getAllProperties()) {
+ List<String> keys = new ArrayList<>(properties.getKeyset());
+ for (String flagName : properties.getKeyset()) {
+ String fullName = properties.getNamespace() + "/" + flagName;
+ allFlags.put(fullName, properties.getString(flagName, null));
}
- } catch (RemoteException e) {
- throw new RuntimeException("Failed in IPC", e);
}
-
return allFlags;
}
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index f59eab0..cd16af7 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -452,7 +452,7 @@
"tests/src/**/systemui/clipboardoverlay/ClipboardListenerTest.java",
"tests/src/**/systemui/doze/DozeScreenStateTest.java",
"tests/src/**/systemui/keyguard/WorkLockActivityControllerTest.java",
- "tests/src/**/systemui/media/dialog/MediaOutputControllerTest.java",
+ "tests/src/**/systemui/media/dialog/MediaSwitchingControllerTest.java",
"tests/src/**/systemui/navigationbar/views/NavigationBarTest.java",
"tests/src/**/systemui/power/PowerNotificationWarningsTest.java",
"tests/src/**/systemui/power/PowerUITest.java",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 7974f92..f3a28ca 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -149,6 +149,16 @@
}
flag {
+ name: "modes_dialog_single_rows"
+ namespace: "systemui"
+ description: "[Experiment] Display one entry per grid row in the Modes Dialog."
+ bug: "366034002"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "pss_app_selector_recents_split_screen"
namespace: "systemui"
description: "Allows recent apps selected for partial screenshare to be launched in split screen mode"
@@ -538,13 +548,6 @@
}
flag {
- name: "haptic_volume_slider"
- namespace: "systemui"
- description: "Adds haptic feedback to the volume slider."
- bug: "316953430"
-}
-
-flag {
name: "new_volume_panel"
namespace: "systemui"
description: "Switches to the new volume panel (without Slices)."
@@ -668,13 +671,6 @@
}
flag {
- name: "compose_lockscreen"
- namespace: "systemui"
- description: "Enables the compose version of lockscreen that runs standalone, outside of Flexiglass."
- bug: "301968149"
-}
-
-flag {
name: "enable_contextual_tip_for_power_off"
namespace: "systemui"
description: "Enables on-screen contextual tip about how to power off or restart phone"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt
index 296fc27..dcf32b2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt
@@ -16,15 +16,10 @@
package com.android.systemui.common.ui.compose.windowinsets
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.asPaddingValues
-import androidx.compose.foundation.layout.displayCutout
-import androidx.compose.foundation.layout.systemBars
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -36,9 +31,6 @@
/** The corner radius in px of the current display. */
val LocalScreenCornerRadius = staticCompositionLocalOf { 0.dp }
-/** The screen height in px without accounting for any screen insets (cutouts, status/nav bars) */
-val LocalRawScreenHeight = staticCompositionLocalOf { 0f }
-
@Composable
fun ScreenDecorProvider(
displayCutout: StateFlow<DisplayCutout>,
@@ -48,22 +40,9 @@
val cutout by displayCutout.collectAsStateWithLifecycle()
val screenCornerRadiusDp = with(LocalDensity.current) { screenCornerRadius.toDp() }
- val density = LocalDensity.current
- val navBarHeight =
- with(density) { WindowInsets.systemBars.asPaddingValues().calculateBottomPadding().toPx() }
- val statusBarHeight = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
- val displayCutoutHeight = WindowInsets.displayCutout.asPaddingValues().calculateTopPadding()
- val screenHeight =
- with(density) {
- (LocalConfiguration.current.screenHeightDp.dp +
- maxOf(statusBarHeight, displayCutoutHeight))
- .toPx()
- } + navBarHeight
-
CompositionLocalProvider(
LocalScreenCornerRadius provides screenCornerRadiusDp,
LocalDisplayCutout provides cutout,
- LocalRawScreenHeight provides screenHeight,
) {
content()
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
index 897a861..a2ae8bb 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
@@ -24,9 +24,11 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
-import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight
import kotlin.math.max
import kotlin.math.roundToInt
import kotlin.math.tanh
@@ -36,9 +38,10 @@
@Composable
fun Modifier.stackVerticalOverscroll(
coroutineScope: CoroutineScope,
- canScrollForward: () -> Boolean
+ canScrollForward: () -> Boolean,
): Modifier {
- val screenHeight = LocalRawScreenHeight.current
+ val screenHeight =
+ with(LocalDensity.current) { LocalConfiguration.current.screenHeightDp.dp.toPx() }
val overscrollOffset = remember { Animatable(0f) }
val stackNestedScrollConnection = remember {
NotificationStackNestedScrollConnection(
@@ -60,10 +63,10 @@
overscrollOffset.animateTo(
targetValue = 0f,
initialVelocity = velocityAvailable,
- animationSpec = tween()
+ animationSpec = tween(),
)
}
- }
+ },
)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 91ecfc1..1b99a96 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -19,6 +19,7 @@
import android.util.Log
import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
@@ -29,6 +30,8 @@
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.absoluteOffset
@@ -36,9 +39,11 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.imeAnimationTarget
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
@@ -68,6 +73,7 @@
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInWindow
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.Dp
@@ -81,7 +87,6 @@
import com.android.compose.animation.scene.NestedScrollBehavior
import com.android.compose.animation.scene.SceneScope
import com.android.compose.modifiers.thenIf
-import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight
import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.res.R
import com.android.systemui.scene.session.ui.composable.SaveableSession
@@ -96,6 +101,7 @@
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
object Notifications {
@@ -171,7 +177,7 @@
setCurrent = { scrollOffset = it },
min = minScrollOffset,
max = maxScrollOffset,
- delta
+ delta,
)
}
@@ -209,8 +215,8 @@
calculateHeadsUpPlaceholderYOffset(
scrollOffset.roundToInt(),
minScrollOffset.roundToInt(),
- stackScrollView.topHeadsUpHeight
- )
+ stackScrollView.topHeadsUpHeight,
+ ),
)
}
.thenIf(isHeadsUp) {
@@ -218,11 +224,8 @@
bottomBehavior = NestedScrollBehavior.EdgeAlways
)
.nestedScroll(nestedScrollConnection)
- .scrollable(
- orientation = Orientation.Vertical,
- state = scrollableState,
- )
- }
+ .scrollable(orientation = Orientation.Vertical, state = scrollableState)
+ },
)
}
@@ -259,6 +262,7 @@
* Adds the space where notification stack should appear in the scene, with a scrim and nested
* scrolling.
*/
+@OptIn(ExperimentalLayoutApi::class)
@Composable
fun SceneScope.NotificationScrollingStack(
shadeSession: SaveableSession,
@@ -291,7 +295,7 @@
val navBarHeight = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()
val bottomPadding = if (shouldReserveSpaceForNavBar) navBarHeight else 0.dp
- val screenHeight = LocalRawScreenHeight.current
+ val screenHeight = with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() }
/**
* The height in px of the contents of notification stack. Depending on the number of
@@ -325,6 +329,14 @@
screenHeight - maxScrimTop() - with(density) { navBarHeight.toPx() }
}
+ val isRemoteInputActive by viewModel.isRemoteInputActive.collectAsStateWithLifecycle(false)
+
+ // The bottom Y bound of the currently focused remote input notification.
+ val remoteInputRowBottom by viewModel.remoteInputRowBottomBound.collectAsStateWithLifecycle(0f)
+
+ // The top y bound of the IME.
+ val imeTop = remember { mutableFloatStateOf(0f) }
+
// we are not scrolled to the top unless the scrim is at its maximum offset.
LaunchedEffect(viewModel, scrimOffset) {
snapshotFlow { scrimOffset.value >= 0f }
@@ -342,15 +354,34 @@
LaunchedEffect(syntheticScroll, scrimOffset, scrollState) {
snapshotFlow { syntheticScroll.value }
.collect { delta ->
- val minOffset = minScrimOffset()
- if (scrimOffset.value > minOffset) {
- val remainingDelta = (minOffset - (scrimOffset.value - delta)).coerceAtLeast(0f)
- scrimOffset.snapTo((scrimOffset.value - delta).coerceAtLeast(minOffset))
- if (remainingDelta > 0f) {
- scrollState.scrollBy(remainingDelta)
- }
- } else {
- scrollState.scrollTo(delta.roundToInt())
+ scrollNotificationStack(
+ scope = coroutineScope,
+ delta = delta,
+ animate = false,
+ scrimOffset = scrimOffset,
+ minScrimOffset = minScrimOffset,
+ scrollState = scrollState,
+ )
+ }
+ }
+
+ // if remote input state changes, compare the row and IME's overlap and offset the scrim and
+ // placeholder accordingly.
+ LaunchedEffect(isRemoteInputActive, remoteInputRowBottom, imeTop) {
+ imeTop.floatValue = 0f
+ snapshotFlow { imeTop.floatValue }
+ .collect { imeTopValue ->
+ // only scroll the stack if ime value has been populated (ime placeholder has been
+ // composed at least once), and our remote input row overlaps with the ime bounds.
+ if (isRemoteInputActive && imeTopValue > 0f && remoteInputRowBottom > imeTopValue) {
+ scrollNotificationStack(
+ scope = coroutineScope,
+ delta = remoteInputRowBottom - imeTopValue,
+ animate = true,
+ scrimOffset = scrimOffset,
+ minScrimOffset = minScrimOffset,
+ scrollState = scrollState,
+ )
}
}
}
@@ -394,12 +425,12 @@
scrimOffset.value < 0 &&
layoutState.isTransitioning(
from = Scenes.Shade,
- to = Scenes.QuickSettings
+ to = Scenes.QuickSettings,
)
) {
IntOffset(
x = 0,
- y = (scrimOffset.value * (1 - shadeToQsFraction)).roundToInt()
+ y = (scrimOffset.value * (1 - shadeToQsFraction)).roundToInt(),
)
} else {
IntOffset(x = 0, y = scrimOffset.value.roundToInt())
@@ -458,13 +489,11 @@
.thenIf(shouldFillMaxSize) { Modifier.fillMaxSize() }
.debugBackground(viewModel, DEBUG_BOX_COLOR)
) {
- NotificationPlaceholder(
- stackScrollView = stackScrollView,
- viewModel = viewModel,
+ Column(
modifier =
Modifier.verticalNestedScrollToScene(
topBehavior = NestedScrollBehavior.EdgeWithPreview,
- isExternalOverscrollGesture = { isCurrentGestureOverscroll.value }
+ isExternalOverscrollGesture = { isCurrentGestureOverscroll.value },
)
.thenIf(shadeMode == ShadeMode.Single) {
Modifier.nestedScroll(scrimNestedScrollConnection)
@@ -473,18 +502,31 @@
.verticalScroll(scrollState)
.padding(top = topPadding)
.fillMaxWidth()
- .notificationStackHeight(
- view = stackScrollView,
- totalVerticalPadding = topPadding + bottomPadding,
- )
- .onSizeChanged { size -> stackHeight.intValue = size.height },
- )
+ ) {
+ NotificationPlaceholder(
+ stackScrollView = stackScrollView,
+ viewModel = viewModel,
+ modifier =
+ Modifier.notificationStackHeight(
+ view = stackScrollView,
+ totalVerticalPadding = topPadding + bottomPadding,
+ )
+ .onSizeChanged { size -> stackHeight.intValue = size.height },
+ )
+ Spacer(
+ modifier =
+ Modifier.windowInsetsBottomHeight(WindowInsets.imeAnimationTarget)
+ .onGloballyPositioned { coordinates: LayoutCoordinates ->
+ imeTop.floatValue = screenHeight - coordinates.size.height
+ }
+ )
+ }
}
if (shouldIncludeHeadsUpSpace) {
HeadsUpNotificationSpace(
stackScrollView = stackScrollView,
viewModel = viewModel,
- modifier = Modifier.padding(top = topPadding)
+ modifier = Modifier.padding(top = topPadding),
)
}
}
@@ -572,6 +614,42 @@
)
}
+private suspend fun scrollNotificationStack(
+ scope: CoroutineScope,
+ delta: Float,
+ animate: Boolean,
+ scrimOffset: Animatable<Float, AnimationVector1D>,
+ minScrimOffset: () -> Float,
+ scrollState: ScrollState,
+) {
+ val minOffset = minScrimOffset()
+ if (scrimOffset.value > minOffset) {
+ val remainingDelta =
+ (minOffset - (scrimOffset.value - delta)).coerceAtLeast(0f).roundToInt()
+ if (remainingDelta > 0) {
+ if (animate) {
+ // launch a new coroutine for the remainder animation so that it doesn't suspend the
+ // scrim animation, allowing both to play simultaneously.
+ scope.launch { scrollState.animateScrollTo(remainingDelta) }
+ } else {
+ scrollState.scrollTo(remainingDelta)
+ }
+ }
+ val newScrimOffset = (scrimOffset.value - delta).coerceAtLeast(minOffset)
+ if (animate) {
+ scrimOffset.animateTo(newScrimOffset)
+ } else {
+ scrimOffset.snapTo(newScrimOffset)
+ }
+ } else {
+ if (animate) {
+ scrollState.animateScrollBy(delta)
+ } else {
+ scrollState.scrollBy(delta)
+ }
+ }
+}
+
private fun calculateCornerRadius(
scrimCornerRadius: Dp,
screenCornerRadius: Dp,
@@ -618,7 +696,7 @@
setCurrent: (Float) -> Unit,
min: Float,
max: Float,
- delta: Float
+ delta: Float,
): Float {
return if (delta < 0 && current > min) {
val remainder = (current + delta - min).coerceAtMost(0f)
@@ -631,10 +709,7 @@
} else 0f
}
-private inline fun debugLog(
- viewModel: NotificationsPlaceholderViewModel,
- msg: () -> Any,
-) {
+private inline fun debugLog(viewModel: NotificationsPlaceholderViewModel, msg: () -> Any) {
if (viewModel.isDebugLoggingEnabled) {
Log.d(TAG, msg().toString())
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index fa92bef34..0c1c165 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -61,6 +61,7 @@
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.colorResource
@@ -79,7 +80,6 @@
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
-import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.lifecycle.ExclusiveActivatable
@@ -229,17 +229,16 @@
}
.thenIf(cutoutLocation != CutoutLocation.CENTER) { Modifier.displayCutoutPadding() }
) {
+ val density = LocalDensity.current
val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsStateWithLifecycle()
val isCustomizerShowing by
viewModel.qsSceneAdapter.isCustomizerShowing.collectAsStateWithLifecycle()
val customizingAnimationDuration by
viewModel.qsSceneAdapter.customizerAnimationDuration.collectAsStateWithLifecycle()
- val screenHeight = LocalRawScreenHeight.current
+ val screenHeight = with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() }
BackHandler(enabled = isCustomizing) { viewModel.qsSceneAdapter.requestCloseCustomizer() }
- val collapsedHeaderHeight =
- with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() }
val lifecycleOwner = LocalLifecycleOwner.current
val footerActionsViewModel =
remember(lifecycleOwner, viewModel) {
@@ -268,7 +267,6 @@
val navBarBottomHeight =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
- val density = LocalDensity.current
val bottomPadding by
animateDpAsState(
targetValue = if (isCustomizing) 0.dp else navBarBottomHeight,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
index b85523b..6c4edf4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
@@ -18,6 +18,7 @@
package com.android.systemui.shade.ui.composable
+import android.view.HapticFeedbackConstants
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@@ -39,17 +40,20 @@
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.LowestZIndexContentPicker
import com.android.compose.animation.scene.SceneScope
import com.android.compose.windowsizeclass.LocalWindowSizeClass
+import com.android.systemui.scene.shared.model.Scenes
/** Renders a lightweight shade UI container, as an overlay. */
@Composable
@@ -58,6 +62,13 @@
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
+ val view = LocalView.current
+ LaunchedEffect(Unit) {
+ if (layoutState.currentTransition?.fromContent == Scenes.Gone) {
+ view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START)
+ }
+ }
+
Box(modifier) {
Scrim(onClicked = onScrimClicked)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index fb9dde3..0bb1d92 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -51,6 +51,7 @@
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastSumBy
import com.android.compose.ui.util.SpaceVectorConverter
@@ -234,8 +235,15 @@
pointersDown == 0 -> {
startedPosition = null
- val lastPointerUp = changes.single { it.id == velocityPointerId }
- velocityTracker.addPointerInputChange(lastPointerUp)
+ // In case of multiple events with 0 pointers down (not pressed) we may have
+ // already removed the velocityPointer
+ val lastPointerUp = changes.fastFilter { it.id == velocityPointerId }
+ check(lastPointerUp.isEmpty() || lastPointerUp.size == 1) {
+ "There are ${lastPointerUp.size} pointers up: $lastPointerUp"
+ }
+ if (lastPointerUp.size == 1) {
+ velocityTracker.addPointerInputChange(lastPointerUp.first())
+ }
}
// The first pointer down, startedPosition was not set.
diff --git a/packages/SystemUI/docs/scene.md b/packages/SystemUI/docs/scene.md
index 0ac15c5..234c7a0 100644
--- a/packages/SystemUI/docs/scene.md
+++ b/packages/SystemUI/docs/scene.md
@@ -68,15 +68,13 @@
1. Set a collection of **aconfig flags** to `true` by running the following
commands:
```console
- $ adb shell device_config override systemui com.android.systemui.scene_container true
- $ adb shell device_config override systemui com.android.systemui.compose_lockscreen true
$ adb shell device_config override systemui com.android.systemui.keyguard_bottom_area_refactor true
$ adb shell device_config override systemui com.android.systemui.keyguard_wm_state_refactor true
- $ adb shell device_config override systemui com.android.systemui.media_in_scene_container true
$ adb shell device_config override systemui com.android.systemui.migrate_clocks_to_blueprint true
- $ adb shell device_config override systemui com.android.systemui.notifications_heads_up_refactor true
+ $ adb shell device_config override systemui com.android.systemui.notification_avalanche_throttle_hun true
$ adb shell device_config override systemui com.android.systemui.predictive_back_sysui true
$ adb shell device_config override systemui com.android.systemui.device_entry_udfps_refactor true
+ $ adb shell device_config override systemui com.android.systemui.scene_container true
```
2. **Restart** System UI by issuing the following command:
```console
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 156e068..312e62d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -196,6 +196,28 @@
}
@Test
+ public void testUserSwitcherToOneHandedRemovesViews() {
+ // Can happen when a SIM is inserted into a large screen device
+ initMode(MODE_USER_SWITCHER);
+ {
+ View view1 = mKeyguardSecurityContainer.findViewById(
+ R.id.keyguard_bouncer_user_switcher);
+ View view2 = mKeyguardSecurityContainer.findViewById(R.id.user_switcher_header);
+ assertThat(view1).isNotNull();
+ assertThat(view2).isNotNull();
+ }
+
+ initMode(MODE_ONE_HANDED);
+ {
+ View view1 = mKeyguardSecurityContainer.findViewById(
+ R.id.keyguard_bouncer_user_switcher);
+ View view2 = mKeyguardSecurityContainer.findViewById(R.id.user_switcher_header);
+ assertThat(view1).isNull();
+ assertThat(view2).isNull();
+ }
+ }
+
+ @Test
public void updatePosition_movesKeyguard() {
setupForUpdateKeyguardPosition(/* oneHandedMode= */ true);
mKeyguardSecurityContainer.updatePositionByTouchX(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
index c2acc5f..160865d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
@@ -31,8 +31,11 @@
import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.fakeTrustRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.AuthenticationFlags
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
@@ -211,7 +214,7 @@
val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
kosmos.fakeUserRepository.setSelectedUserInfo(
primaryUser,
- SelectionStatus.SELECTION_COMPLETE
+ SelectionStatus.SELECTION_COMPLETE,
)
kosmos.fakeTrustRepository.setCurrentUserTrusted(true)
@@ -240,6 +243,49 @@
}
@Test
+ fun deviceUnlockStatus_becomesUnlocked_whenFingerprintUnlocked_whileDeviceAsleepInAod() =
+ testScope.runTest {
+ val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
+ assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+
+ kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ testScope = this,
+ )
+ kosmos.powerInteractor.setAsleepForTest()
+ runCurrent()
+
+ assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+ assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+ }
+
+ @Test
+ fun deviceUnlockStatus_staysLocked_whenFingerprintUnlocked_whileDeviceAsleep() =
+ testScope.runTest {
+ val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
+ assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+ assertThat(kosmos.keyguardTransitionInteractor.getCurrentState())
+ .isEqualTo(KeyguardState.LOCKSCREEN)
+
+ kosmos.powerInteractor.setAsleepForTest()
+ runCurrent()
+
+ assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+ assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+ }
+
+ @Test
fun deviceEntryRestrictionReason_whenFaceOrFingerprintOrTrust_alwaysNull() =
testScope.runTest {
kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
@@ -273,7 +319,7 @@
LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
DeviceEntryRestrictionReason.UserLockdown,
LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
- DeviceEntryRestrictionReason.PolicyLockdown
+ DeviceEntryRestrictionReason.PolicyLockdown,
)
}
@@ -285,7 +331,7 @@
kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
kosmos.fakeSystemPropertiesHelper.set(
DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP,
- "not mainline reboot"
+ "not mainline reboot",
)
runCurrent()
@@ -321,7 +367,7 @@
kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
kosmos.fakeSystemPropertiesHelper.set(
DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP,
- "not mainline reboot"
+ "not mainline reboot",
)
runCurrent()
@@ -358,7 +404,7 @@
kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
kosmos.fakeSystemPropertiesHelper.set(
DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP,
- "not mainline reboot"
+ "not mainline reboot",
)
runCurrent()
@@ -394,12 +440,12 @@
collectLastValue(underTest.deviceEntryRestrictionReason)
kosmos.fakeSystemPropertiesHelper.set(
DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP,
- DeviceUnlockedInteractor.REBOOT_MAINLINE_UPDATE
+ DeviceUnlockedInteractor.REBOOT_MAINLINE_UPDATE,
)
kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags(
AuthenticationFlags(
userId = 1,
- flag = LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
+ flag = LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT,
)
)
runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt
index 50b727c..9cfd328 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt
@@ -25,6 +25,7 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.education.data.model.EduDeviceConnectionTime
import com.android.systemui.education.data.model.GestureEduModel
+import com.android.systemui.education.domain.interactor.mockEduInputManager
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
@@ -62,7 +63,13 @@
// Create TestContext here because TemporaryFolder.create() is called in @Before. It is
// needed before calling TemporaryFolder.newFolder().
val testContext = TestContext(context, tmpFolder.newFolder())
- underTest = UserContextualEducationRepository(testContext, dsScopeProvider)
+ underTest =
+ UserContextualEducationRepository(
+ testContext,
+ dsScopeProvider,
+ kosmos.mockEduInputManager,
+ kosmos.testDispatcher
+ )
underTest.setUser(testUserId)
}
@@ -99,7 +106,8 @@
lastShortcutTriggeredTime = kosmos.fakeEduClock.instant(),
lastEducationTime = kosmos.fakeEduClock.instant(),
usageSessionStartTime = kosmos.fakeEduClock.instant(),
- userId = testUserId
+ userId = testUserId,
+ gestureType = BACK
)
underTest.updateGestureEduModel(BACK) { newModel }
val model by collectLastValue(underTest.readGestureEduModelFlow(BACK))
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 64915fb..8201bbe 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,15 +17,13 @@
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
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.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.education.data.model.GestureEduModel
@@ -40,20 +38,21 @@
import com.google.common.truth.Truth.assertThat
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Assume.assumeTrue
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
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(ParameterizedAndroidJunit4::class)
@kotlinx.coroutines.ExperimentalCoroutinesApi
-class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
+class KeyboardTouchpadEduInteractorTest(private val gestureType: GestureType) : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val contextualEduInteractor = kosmos.contextualEducationInteractor
@@ -71,21 +70,27 @@
underTest.start()
contextualEduInteractor.start()
userRepository.setUserInfos(USER_INFOS)
+ testScope.launch {
+ contextualEduInteractor.updateKeyboardFirstConnectionTime()
+ contextualEduInteractor.updateTouchpadFirstConnectionTime()
+ }
}
@Test
fun newEducationInfoOnMaxSignalCountReached() =
testScope.runTest {
- triggerMaxEducationSignals(BACK)
+ triggerMaxEducationSignals(gestureType)
val model by collectLastValue(underTest.educationTriggered)
- assertThat(model?.gestureType).isEqualTo(BACK)
+
+ assertThat(model?.gestureType).isEqualTo(gestureType)
}
@Test
fun newEducationToastOn1stEducation() =
testScope.runTest {
val model by collectLastValue(underTest.educationTriggered)
- triggerMaxEducationSignals(BACK)
+ triggerMaxEducationSignals(gestureType)
+
assertThat(model?.educationUiType).isEqualTo(EducationUiType.Toast)
}
@@ -93,12 +98,12 @@
fun newEducationNotificationOn2ndEducation() =
testScope.runTest {
val model by collectLastValue(underTest.educationTriggered)
- triggerMaxEducationSignals(BACK)
+ triggerMaxEducationSignals(gestureType)
// runCurrent() to trigger 1st education
runCurrent()
eduClock.offset(minDurationForNextEdu)
- triggerMaxEducationSignals(BACK)
+ triggerMaxEducationSignals(gestureType)
assertThat(model?.educationUiType).isEqualTo(EducationUiType.Notification)
}
@@ -106,7 +111,7 @@
@Test
fun noEducationInfoBeforeMaxSignalCountReached() =
testScope.runTest {
- contextualEduInteractor.incrementSignalCount(BACK)
+ contextualEduInteractor.incrementSignalCount(gestureType)
val model by collectLastValue(underTest.educationTriggered)
assertThat(model).isNull()
}
@@ -115,8 +120,8 @@
fun noEducationInfoWhenShortcutTriggeredPreviously() =
testScope.runTest {
val model by collectLastValue(underTest.educationTriggered)
- contextualEduInteractor.updateShortcutTriggerTime(BACK)
- triggerMaxEducationSignals(BACK)
+ contextualEduInteractor.updateShortcutTriggerTime(gestureType)
+ triggerMaxEducationSignals(gestureType)
assertThat(model).isNull()
}
@@ -124,12 +129,12 @@
fun no2ndEducationBeforeMinEduIntervalReached() =
testScope.runTest {
val models by collectValues(underTest.educationTriggered)
- triggerMaxEducationSignals(BACK)
+ triggerMaxEducationSignals(gestureType)
runCurrent()
// Offset a duration that is less than the required education interval
eduClock.offset(1.seconds)
- triggerMaxEducationSignals(BACK)
+ triggerMaxEducationSignals(gestureType)
runCurrent()
assertThat(models.filterNotNull().size).isEqualTo(1)
@@ -140,15 +145,15 @@
testScope.runTest {
val models by collectValues(underTest.educationTriggered)
// Trigger 2 educations
- triggerMaxEducationSignals(BACK)
+ triggerMaxEducationSignals(gestureType)
runCurrent()
eduClock.offset(minDurationForNextEdu)
- triggerMaxEducationSignals(BACK)
+ triggerMaxEducationSignals(gestureType)
runCurrent()
// Try triggering 3rd education
eduClock.offset(minDurationForNextEdu)
- triggerMaxEducationSignals(BACK)
+ triggerMaxEducationSignals(gestureType)
assertThat(models.filterNotNull().size).isEqualTo(2)
}
@@ -157,18 +162,21 @@
fun startNewUsageSessionWhen2ndSignalReceivedAfterSessionDeadline() =
testScope.runTest {
val model by
- collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK))
- contextualEduInteractor.incrementSignalCount(BACK)
+ collectLastValue(
+ kosmos.contextualEducationRepository.readGestureEduModelFlow(gestureType)
+ )
+ contextualEduInteractor.incrementSignalCount(gestureType)
eduClock.offset(KeyboardTouchpadEduInteractor.usageSessionDuration.plus(1.seconds))
val secondSignalReceivedTime = eduClock.instant()
- contextualEduInteractor.incrementSignalCount(BACK)
+ contextualEduInteractor.incrementSignalCount(gestureType)
assertThat(model)
.isEqualTo(
GestureEduModel(
signalCount = 1,
usageSessionStartTime = secondSignalReceivedTime,
- userId = 0
+ userId = 0,
+ gestureType = gestureType
)
)
}
@@ -252,22 +260,9 @@
@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 allAppsKeyGestureEvent =
- KeyGestureEvent.Builder()
- .setDeviceId(1)
- .setModifierState(KeyEvent.META_META_ON)
- .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS)
- .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
- .build()
- listenerCaptor.value.onKeyGestureEvent(allAppsKeyGestureEvent)
+ // Only All Apps needs to update the keyboard shortcut
+ assumeTrue(gestureType == ALL_APPS)
+ kosmos.contextualEducationRepository.setKeyboardShortcutTriggered(ALL_APPS)
val model by
collectLastValue(
@@ -293,10 +288,18 @@
runCurrent()
}
+ private suspend fun setUpForDeviceConnection() {
+ contextualEduInteractor.updateKeyboardFirstConnectionTime()
+ contextualEduInteractor.updateTouchpadFirstConnectionTime()
+ }
+
companion object {
- private val USER_INFOS =
- listOf(
- UserInfo(101, "Second User", 0),
- )
+ private val USER_INFOS = listOf(UserInfo(101, "Second User", 0))
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getGestureTypes(): List<GestureType> {
+ return listOf(BACK, HOME, OVERVIEW, ALL_APPS)
+ }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
index c4ac585..ab33269 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
@@ -37,6 +37,7 @@
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -69,6 +70,11 @@
@Before
fun setUp() {
+ testScope.launch {
+ interactor.updateKeyboardFirstConnectionTime()
+ interactor.updateTouchpadFirstConnectionTime()
+ }
+
val viewModel =
ContextualEduViewModel(
kosmos.applicationContext.resources,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
index 686b518..366b55d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
@@ -23,6 +23,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.classifier.falsingManager
import com.android.systemui.haptics.fakeVibratorHelper
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.core.FakeLogBuffer
@@ -68,11 +69,13 @@
vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_SPIN] = spinDuration
whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(true)
+ kosmos.falsingManager.setFalseLongTap(false)
longPressEffect =
QSLongPressEffect(
vibratorHelper,
kosmos.keyguardStateController,
+ kosmos.falsingManager,
FakeLogBuffer.Factory.create(),
)
longPressEffect.callback = callback
@@ -180,11 +183,7 @@
// THEN the expected texture is played
val reverseHaptics =
- LongPressHapticBuilder.createReversedEffect(
- progress,
- lowTickDuration,
- effectDuration,
- )
+ LongPressHapticBuilder.createReversedEffect(progress, lowTickDuration, effectDuration)
assertThat(reverseHaptics).isNotNull()
assertThat(vibratorHelper.hasVibratedWithEffects(reverseHaptics!!)).isTrue()
}
@@ -224,6 +223,20 @@
}
@Test
+ fun onAnimationComplete_isFalseLongClick_effectEndsInIdleWithReset() =
+ testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) {
+ // GIVEN that the long-click is false
+ kosmos.falsingManager.setFalseLongTap(true)
+
+ // GIVEN that the animation completes
+ longPressEffect.handleAnimationComplete()
+
+ // THEN the long-press effect ends in the idle state and the properties are reset
+ assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
+ verify(callback, times(1)).onResetProperties()
+ }
+
+ @Test
fun onAnimationComplete_whenRunningBackwardsFromUp_endsWithFinishedReversingAndClick() =
testWhileInState(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_UP) {
// GIVEN that the animation completes
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
index 6c3c7ef..fcf4662 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
@@ -16,7 +16,10 @@
*/
package com.android.systemui.keyguard.data.quickaffordance
+import android.app.Flags
import android.net.Uri
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.provider.Settings
import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
import android.provider.Settings.Global.ZEN_MODE_OFF
@@ -25,6 +28,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.notification.modes.EnableZenModeDialog
+import com.android.settingslib.notification.modes.TestModeBuilder
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.ContentDescription
@@ -35,7 +39,11 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
import com.android.systemui.statusbar.policy.ZenModeController
+import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
+import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq
@@ -43,6 +51,7 @@
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
+import java.time.Duration
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -66,8 +75,13 @@
private val kosmos = testKosmos()
private val testDispatcher = kosmos.testDispatcher
private val testScope = kosmos.testScope
+
private val settings = kosmos.fakeSettings
+ private val zenModeRepository = kosmos.fakeZenModeRepository
+ private val deviceProvisioningRepository = kosmos.fakeDeviceProvisioningRepository
+ private val secureSettingsRepository = kosmos.secureSettingsRepository
+
@Mock private lateinit var zenModeController: ZenModeController
@Mock private lateinit var userTracker: UserTracker
@Mock private lateinit var conditionUri: Uri
@@ -85,17 +99,36 @@
DoNotDisturbQuickAffordanceConfig(
context,
zenModeController,
+ kosmos.zenModeInteractor,
settings,
userTracker,
testDispatcher,
+ testScope.backgroundScope,
conditionUri,
enableZenModeDialog,
)
}
@Test
+ @EnableFlags(Flags.FLAG_MODES_UI)
fun dndNotAvailable_pickerStateHidden() =
testScope.runTest {
+ deviceProvisioningRepository.setDeviceProvisioned(false)
+ runCurrent()
+
+ val result = underTest.getPickerScreenState()
+ runCurrent()
+
+ assertEquals(
+ KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice,
+ result,
+ )
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_MODES_UI)
+ fun controllerDndNotAvailable_pickerStateHidden() =
+ testScope.runTest {
// given
whenever(zenModeController.isZenAvailable).thenReturn(false)
@@ -105,13 +138,33 @@
// then
assertEquals(
KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice,
- result
+ result,
)
}
@Test
+ @EnableFlags(Flags.FLAG_MODES_UI)
fun dndAvailable_pickerStateVisible() =
testScope.runTest {
+ deviceProvisioningRepository.setDeviceProvisioned(true)
+ runCurrent()
+
+ val result = underTest.getPickerScreenState()
+ runCurrent()
+
+ assertThat(result)
+ .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java)
+ val defaultPickerState =
+ result as KeyguardQuickAffordanceConfig.PickerScreenState.Default
+ assertThat(defaultPickerState.configureIntent).isNotNull()
+ assertThat(defaultPickerState.configureIntent?.action)
+ .isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_MODES_UI)
+ fun controllerDndAvailable_pickerStateVisible() =
+ testScope.runTest {
// given
whenever(zenModeController.isZenAvailable).thenReturn(true)
@@ -129,7 +182,27 @@
}
@Test
- fun onTriggered_dndModeIsNotZEN_MODE_OFF_setToZEN_MODE_OFF() =
+ @EnableFlags(Flags.FLAG_MODES_UI)
+ fun onTriggered_dndModeIsNotOff_setToOff() =
+ testScope.runTest {
+ val currentModes by collectLastValue(zenModeRepository.modes)
+
+ zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE)
+ secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -2)
+ collectLastValue(underTest.lockScreenState)
+ runCurrent()
+
+ val result = underTest.onTriggered(null)
+ runCurrent()
+
+ val dndMode = currentModes!!.single()
+ assertThat(dndMode.isActive).isFalse()
+ assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_MODES_UI)
+ fun onTriggered_controllerDndModeIsNotZEN_MODE_OFF_setToZEN_MODE_OFF() =
testScope.runTest {
// given
whenever(zenModeController.isZenAvailable).thenReturn(true)
@@ -140,11 +213,12 @@
// when
val result = underTest.onTriggered(null)
+
verify(zenModeController)
.setZen(
spyZenMode.capture(),
spyConditionId.capture(),
- eq(DoNotDisturbQuickAffordanceConfig.TAG)
+ eq(DoNotDisturbQuickAffordanceConfig.TAG),
)
// then
@@ -154,7 +228,28 @@
}
@Test
- fun onTriggered_dndModeIsZEN_MODE_OFF_settingFOREVER_setZenWithoutCondition() =
+ @EnableFlags(Flags.FLAG_MODES_UI)
+ fun onTriggered_dndModeIsOff_settingFOREVER_setZenWithoutCondition() =
+ testScope.runTest {
+ val currentModes by collectLastValue(zenModeRepository.modes)
+
+ zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
+ secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_FOREVER)
+ collectLastValue(underTest.lockScreenState)
+ runCurrent()
+
+ val result = underTest.onTriggered(null)
+ runCurrent()
+
+ val dndMode = currentModes!!.single()
+ assertThat(dndMode.isActive).isTrue()
+ assertThat(zenModeRepository.getModeActiveDuration(dndMode.id)).isNull()
+ assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_MODES_UI)
+ fun onTriggered_controllerDndModeIsZEN_MODE_OFF_settingFOREVER_setZenWithoutCondition() =
testScope.runTest {
// given
whenever(zenModeController.isZenAvailable).thenReturn(true)
@@ -169,7 +264,7 @@
.setZen(
spyZenMode.capture(),
spyConditionId.capture(),
- eq(DoNotDisturbQuickAffordanceConfig.TAG)
+ eq(DoNotDisturbQuickAffordanceConfig.TAG),
)
// then
@@ -179,7 +274,27 @@
}
@Test
- fun onTriggered_dndZEN_MODE_OFF_settingNotFOREVERorPROMPT_zenWithCondition() =
+ @EnableFlags(Flags.FLAG_MODES_UI)
+ fun onTriggered_dndModeIsOff_settingNotFOREVERorPROMPT_dndWithDuration() =
+ testScope.runTest {
+ val currentModes by collectLastValue(zenModeRepository.modes)
+ zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
+ secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -900)
+ runCurrent()
+
+ val result = underTest.onTriggered(null)
+ runCurrent()
+
+ assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+ val dndMode = currentModes!!.single()
+ assertThat(dndMode.isActive).isTrue()
+ assertThat(zenModeRepository.getModeActiveDuration(dndMode.id))
+ .isEqualTo(Duration.ofMinutes(-900))
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_MODES_UI)
+ fun onTriggered_controllerDndZEN_MODE_OFF_settingNotFOREVERorPROMPT_zenWithCondition() =
testScope.runTest {
// given
whenever(zenModeController.isZenAvailable).thenReturn(true)
@@ -194,7 +309,7 @@
.setZen(
spyZenMode.capture(),
spyConditionId.capture(),
- eq(DoNotDisturbQuickAffordanceConfig.TAG)
+ eq(DoNotDisturbQuickAffordanceConfig.TAG),
)
// then
@@ -204,7 +319,28 @@
}
@Test
- fun onTriggered_dndModeIsZEN_MODE_OFF_settingIsPROMPT_showDialog() =
+ @EnableFlags(Flags.FLAG_MODES_UI)
+ fun onTriggered_dndModeIsOff_settingIsPROMPT_showDialog() =
+ testScope.runTest {
+ val expandable: Expandable = mock()
+ zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
+ secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_PROMPT)
+ whenever(enableZenModeDialog.createDialog()).thenReturn(mock())
+ collectLastValue(underTest.lockScreenState)
+ runCurrent()
+
+ val result = underTest.onTriggered(expandable)
+
+ assertTrue(result is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog)
+ assertEquals(
+ expandable,
+ (result as KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog).expandable,
+ )
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_MODES_UI)
+ fun onTriggered_controllerDndModeIsZEN_MODE_OFF_settingIsPROMPT_showDialog() =
testScope.runTest {
// given
val expandable: Expandable = mock()
@@ -222,13 +358,31 @@
assertTrue(result is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog)
assertEquals(
expandable,
- (result as KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog).expandable
+ (result as KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog).expandable,
)
}
@Test
+ @EnableFlags(Flags.FLAG_MODES_UI)
fun lockScreenState_dndAvailableStartsAsTrue_changeToFalse_StateIsHidden() =
testScope.runTest {
+ deviceProvisioningRepository.setDeviceProvisioned(true)
+ val valueSnapshot = collectLastValue(underTest.lockScreenState)
+ val secondLastValue = valueSnapshot()
+ runCurrent()
+
+ deviceProvisioningRepository.setDeviceProvisioned(false)
+ runCurrent()
+ val lastValue = valueSnapshot()
+
+ assertTrue(secondLastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
+ assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_MODES_UI)
+ fun lockScreenState_controllerDndAvailableStartsAsTrue_changeToFalse_StateIsHidden() =
+ testScope.runTest {
// given
whenever(zenModeController.isZenAvailable).thenReturn(true)
val callbackCaptor: ArgumentCaptor<ZenModeController.Callback> = argumentCaptor()
@@ -246,7 +400,44 @@
}
@Test
- fun lockScreenState_dndModeStartsAsZEN_MODE_OFF_changeToNotOFF_StateVisible() =
+ @EnableFlags(Flags.FLAG_MODES_UI)
+ fun lockScreenState_dndModeStartsAsOff_changeToOn_StateVisible() =
+ testScope.runTest {
+ val lockScreenState by collectLastValue(underTest.lockScreenState)
+
+ zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
+ runCurrent()
+
+ assertThat(lockScreenState)
+ .isEqualTo(
+ KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+ Icon.Resource(
+ R.drawable.qs_dnd_icon_off,
+ ContentDescription.Resource(R.string.dnd_is_off),
+ ),
+ ActivationState.Inactive,
+ )
+ )
+
+ zenModeRepository.removeMode(TestModeBuilder.MANUAL_DND_INACTIVE.id)
+ zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE)
+ runCurrent()
+
+ assertThat(lockScreenState)
+ .isEqualTo(
+ KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+ Icon.Resource(
+ R.drawable.qs_dnd_icon_on,
+ ContentDescription.Resource(R.string.dnd_is_on),
+ ),
+ ActivationState.Active,
+ )
+ )
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_MODES_UI)
+ fun lockScreenState_controllerDndModeStartsAsZEN_MODE_OFF_changeToNotOFF_StateVisible() =
testScope.runTest {
// given
whenever(zenModeController.isZenAvailable).thenReturn(true)
@@ -265,9 +456,9 @@
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
Icon.Resource(
R.drawable.qs_dnd_icon_off,
- ContentDescription.Resource(R.string.dnd_is_off)
+ ContentDescription.Resource(R.string.dnd_is_off),
),
- ActivationState.Inactive
+ ActivationState.Inactive,
),
secondLastValue,
)
@@ -275,9 +466,9 @@
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
Icon.Resource(
R.drawable.qs_dnd_icon_on,
- ContentDescription.Resource(R.string.dnd_is_on)
+ ContentDescription.Resource(R.string.dnd_is_on),
),
- ActivationState.Active
+ ActivationState.Active,
),
lastValue,
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
index 59f16d7..84b7f5c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
@@ -17,17 +17,16 @@
package com.android.systemui.keyguard.domain.interactor
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
import com.android.systemui.keyguard.data.repository.keyguardBlueprintRepository
import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
@@ -59,8 +58,8 @@
class KeyguardBlueprintInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val underTest = kosmos.keyguardBlueprintInteractor
- private val keyguardBlueprintRepository = kosmos.keyguardBlueprintRepository
+ private val underTest by lazy { kosmos.keyguardBlueprintInteractor }
+ private val keyguardBlueprintRepository by lazy { kosmos.keyguardBlueprintRepository }
private val clockRepository by lazy { kosmos.fakeKeyguardClockRepository }
private val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
private val fingerprintPropertyRepository by lazy { kosmos.fakeFingerprintPropertyRepository }
@@ -75,7 +74,7 @@
sensorId = 1,
strength = SensorStrength.STRONG,
sensorType = FingerprintSensorType.POWER_BUTTON,
- sensorLocations = mapOf()
+ sensorLocations = mapOf(),
)
}
@@ -93,7 +92,7 @@
}
@Test
- @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
+ @DisableSceneContainer
fun testAppliesSplitShadeBlueprint() {
testScope.runTest {
val blueprintId by collectLastValue(underTest.blueprintId)
@@ -107,7 +106,7 @@
}
@Test
- @EnableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
+ @EnableSceneContainer
fun testDoesNotApplySplitShadeBlueprint() {
testScope.runTest {
val blueprintId by collectLastValue(underTest.blueprintId)
@@ -122,7 +121,7 @@
}
@Test
- @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
+ @DisableSceneContainer
fun fingerprintPropertyInitialized_updatesBlueprint() {
testScope.runTest {
underTest.start()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
index 41c5b73..ff6ea3a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
@@ -25,6 +25,8 @@
import com.android.systemui.Flags as AConfigFlags
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
@@ -44,6 +46,7 @@
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.runTest
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Answers
@@ -71,10 +74,8 @@
private val burnInFlow = MutableStateFlow(BurnInModel())
@Before
- @DisableFlags(
- AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
- AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
- )
+ @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
+ @DisableSceneContainer
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(burnInInteractor.burnIn(anyInt(), anyInt())).thenReturn(burnInFlow)
@@ -112,18 +113,13 @@
from = KeyguardState.AOD,
to = KeyguardState.LOCKSCREEN,
value = 1f,
- transitionState = TransitionState.FINISHED
+ transitionState = TransitionState.FINISHED,
),
validateStep = false,
)
// Trigger a change to the burn-in model
- burnInFlow.value =
- BurnInModel(
- translationX = 20,
- translationY = 30,
- scale = 0.5f,
- )
+ burnInFlow.value = BurnInModel(translationX = 20, translationY = 30, scale = 0.5f)
assertThat(movement?.translationX).isEqualTo(0)
assertThat(movement?.translationY).isEqualTo(0)
@@ -143,17 +139,12 @@
from = KeyguardState.GONE,
to = KeyguardState.AOD,
value = 1f,
- transitionState = TransitionState.FINISHED
+ transitionState = TransitionState.FINISHED,
),
validateStep = false,
)
// Trigger a change to the burn-in model
- burnInFlow.value =
- BurnInModel(
- translationX = 20,
- translationY = 30,
- scale = 0.5f,
- )
+ burnInFlow.value = BurnInModel(translationX = 20, translationY = 30, scale = 0.5f)
assertThat(movement?.translationX).isEqualTo(20)
assertThat(movement?.translationY).isEqualTo(30)
@@ -166,7 +157,7 @@
from = KeyguardState.GONE,
to = KeyguardState.AOD,
value = 0f,
- transitionState = TransitionState.STARTED
+ transitionState = TransitionState.STARTED,
),
validateStep = false,
)
@@ -180,11 +171,7 @@
@DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun translationAndScale_whenFullyDozing_MigrationFlagOff_staysOutOfTopInset() =
testScope.runTest {
- burnInParameters =
- burnInParameters.copy(
- minViewY = 100,
- topInset = 80,
- )
+ burnInParameters = burnInParameters.copy(minViewY = 100, topInset = 80)
val movement by collectLastValue(underTest.movement(burnInParameters))
// Set to dozing (on AOD)
@@ -193,18 +180,13 @@
from = KeyguardState.GONE,
to = KeyguardState.AOD,
value = 1f,
- transitionState = TransitionState.FINISHED
+ transitionState = TransitionState.FINISHED,
),
validateStep = false,
)
// Trigger a change to the burn-in model
- burnInFlow.value =
- BurnInModel(
- translationX = 20,
- translationY = -30,
- scale = 0.5f,
- )
+ burnInFlow.value = BurnInModel(translationX = 20, translationY = -30, scale = 0.5f)
assertThat(movement?.translationX).isEqualTo(20)
// -20 instead of -30, due to inset of 80
assertThat(movement?.translationY).isEqualTo(-20)
@@ -217,7 +199,7 @@
from = KeyguardState.GONE,
to = KeyguardState.AOD,
value = 0f,
- transitionState = TransitionState.STARTED
+ transitionState = TransitionState.STARTED,
),
validateStep = false,
)
@@ -231,11 +213,7 @@
@EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun translationAndScale_whenFullyDozing_MigrationFlagOn_staysOutOfTopInset() =
testScope.runTest {
- burnInParameters =
- burnInParameters.copy(
- minViewY = 100,
- topInset = 80,
- )
+ burnInParameters = burnInParameters.copy(minViewY = 100, topInset = 80)
val movement by collectLastValue(underTest.movement(burnInParameters))
// Set to dozing (on AOD)
@@ -244,18 +222,13 @@
from = KeyguardState.GONE,
to = KeyguardState.AOD,
value = 1f,
- transitionState = TransitionState.FINISHED
+ transitionState = TransitionState.FINISHED,
),
validateStep = false,
)
// Trigger a change to the burn-in model
- burnInFlow.value =
- BurnInModel(
- translationX = 20,
- translationY = -30,
- scale = 0.5f,
- )
+ burnInFlow.value = BurnInModel(translationX = 20, translationY = -30, scale = 0.5f)
assertThat(movement?.translationX).isEqualTo(20)
// -20 instead of -30, due to inset of 80
assertThat(movement?.translationY).isEqualTo(-20)
@@ -268,7 +241,7 @@
from = KeyguardState.GONE,
to = KeyguardState.AOD,
value = 0f,
- transitionState = TransitionState.STARTED
+ transitionState = TransitionState.STARTED,
),
validateStep = false,
)
@@ -291,18 +264,13 @@
from = KeyguardState.GONE,
to = KeyguardState.AOD,
value = 1f,
- transitionState = TransitionState.FINISHED
+ transitionState = TransitionState.FINISHED,
),
validateStep = false,
)
// Trigger a change to the burn-in model
- burnInFlow.value =
- BurnInModel(
- translationX = 20,
- translationY = 30,
- scale = 0.5f,
- )
+ burnInFlow.value = BurnInModel(translationX = 20, translationY = 30, scale = 0.5f)
assertThat(movement?.translationX).isEqualTo(20)
assertThat(movement?.translationY).isEqualTo(30)
@@ -311,9 +279,9 @@
}
@Test
- @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+ @DisableSceneContainer
@EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
- fun translationAndScale_composeFlagOff_weatherLargeClock() =
+ fun translationAndScale_sceneContainerOff_weatherLargeClock() =
testBurnInViewModelForClocks(
isSmallClock = false,
isWeatherClock = true,
@@ -321,9 +289,9 @@
)
@Test
- @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+ @DisableSceneContainer
@EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
- fun translationAndScale_composeFlagOff_weatherSmallClock() =
+ fun translationAndScale_sceneContainerOff_weatherSmallClock() =
testBurnInViewModelForClocks(
isSmallClock = true,
isWeatherClock = true,
@@ -331,9 +299,9 @@
)
@Test
- @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+ @DisableSceneContainer
@EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
- fun translationAndScale_composeFlagOff_nonWeatherLargeClock() =
+ fun translationAndScale_sceneContainerOff_nonWeatherLargeClock() =
testBurnInViewModelForClocks(
isSmallClock = false,
isWeatherClock = false,
@@ -341,9 +309,9 @@
)
@Test
- @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+ @DisableSceneContainer
@EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
- fun translationAndScale_composeFlagOff_nonWeatherSmallClock() =
+ fun translationAndScale_sceneContainerOff_nonWeatherSmallClock() =
testBurnInViewModelForClocks(
isSmallClock = true,
isWeatherClock = false,
@@ -351,11 +319,9 @@
)
@Test
- @EnableFlags(
- AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
- AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
- )
- fun translationAndScale_composeFlagOn_weatherLargeClock() =
+ @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
+ @EnableSceneContainer
+ fun translationAndScale_sceneContainerOn_weatherLargeClock() =
testBurnInViewModelForClocks(
isSmallClock = false,
isWeatherClock = true,
@@ -363,11 +329,9 @@
)
@Test
- @EnableFlags(
- AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
- AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
- )
- fun translationAndScale_composeFlagOn_weatherSmallClock() =
+ @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
+ @EnableSceneContainer
+ fun translationAndScale_sceneContainerOn_weatherSmallClock() =
testBurnInViewModelForClocks(
isSmallClock = true,
isWeatherClock = true,
@@ -375,11 +339,9 @@
)
@Test
- @EnableFlags(
- AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
- AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
- )
- fun translationAndScale_composeFlagOn_nonWeatherLargeClock() =
+ @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
+ @EnableSceneContainer
+ fun translationAndScale_sceneContainerOn_nonWeatherLargeClock() =
testBurnInViewModelForClocks(
isSmallClock = false,
isWeatherClock = false,
@@ -387,11 +349,10 @@
)
@Test
- @EnableFlags(
- AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
- AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
- )
- fun translationAndScale_composeFlagOn_nonWeatherSmallClock() =
+ @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
+ @EnableSceneContainer
+ @Ignore("b/367659687")
+ fun translationAndScale_sceneContainerOn_nonWeatherSmallClock() =
testBurnInViewModelForClocks(
isSmallClock = true,
isWeatherClock = false,
@@ -421,18 +382,13 @@
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.AOD,
value = 1f,
- transitionState = TransitionState.FINISHED
+ transitionState = TransitionState.FINISHED,
),
validateStep = false,
)
// Trigger a change to the burn-in model
- burnInFlow.value =
- BurnInModel(
- translationX = 20,
- translationY = 30,
- scale = 0.5f,
- )
+ burnInFlow.value = BurnInModel(translationX = 20, translationY = 30, scale = 0.5f)
assertThat(movement?.translationX).isEqualTo(20)
assertThat(movement?.translationY).isEqualTo(30)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
index 17e1b53..05a6b87 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
@@ -16,14 +16,13 @@
package com.android.systemui.keyguard.ui.viewmodel
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.BrokenWithSceneContainer
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.andSceneContainer
import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
import com.android.systemui.keyguard.data.repository.keyguardClockRepository
@@ -229,8 +228,8 @@
}
@Test
- @EnableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
- fun testSmallClockTop_splitShade_composeLockscreenOn() =
+ @EnableSceneContainer
+ fun testSmallClockTop_splitShade_sceneContainerOn() =
testScope.runTest {
with(kosmos) {
shadeRepository.setShadeLayoutWide(true)
@@ -244,8 +243,8 @@
}
@Test
- @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
- fun testSmallClockTop_splitShade_composeLockscreenOff() =
+ @DisableSceneContainer
+ fun testSmallClockTop_splitShade_sceneContainerOff() =
testScope.runTest {
with(kosmos) {
shadeRepository.setShadeLayoutWide(true)
@@ -257,8 +256,8 @@
}
@Test
- @EnableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
- fun testSmallClockTop_nonSplitShade_composeLockscreenOn() =
+ @EnableSceneContainer
+ fun testSmallClockTop_nonSplitShade_sceneContainerOn() =
testScope.runTest {
with(kosmos) {
shadeRepository.setShadeLayoutWide(false)
@@ -270,8 +269,8 @@
}
@Test
- @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
- fun testSmallClockTop_nonSplitShade_composeLockscreenOff() =
+ @DisableSceneContainer
+ fun testSmallClockTop_nonSplitShade_sceneContainerOff() =
testScope.runTest {
with(kosmos) {
shadeRepository.setShadeLayoutWide(false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt
deleted file mode 100644
index 42db96e..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt
+++ /dev/null
@@ -1,183 +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.systemui.qs.panels.domain.interactor
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository
-import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
-import com.android.systemui.qs.panels.data.repository.gridLayoutTypeRepository
-import com.android.systemui.qs.panels.shared.model.GridLayoutType
-import com.android.systemui.qs.panels.shared.model.InfiniteGridLayoutType
-import com.android.systemui.qs.pipeline.data.repository.tileSpecRepository
-import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class GridConsistencyInteractorTest : SysuiTestCase() {
-
- data object NoopGridLayoutType : GridLayoutType
-
- private val kosmos =
- testKosmos().apply {
- defaultLargeTilesRepository =
- object : DefaultLargeTilesRepository {
- override val defaultLargeTiles =
- setOf(
- TileSpec.create("largeA"),
- TileSpec.create("largeB"),
- TileSpec.create("largeC"),
- TileSpec.create("largeD"),
- )
- }
- gridConsistencyInteractorsMap =
- mapOf(
- Pair(NoopGridLayoutType, noopGridConsistencyInteractor),
- Pair(InfiniteGridLayoutType, infiniteGridConsistencyInteractor)
- )
- }
-
- private val underTest = with(kosmos) { gridConsistencyInteractor }
-
- @Before
- fun setUp() {
- // Mostly testing InfiniteGridConsistencyInteractor because it reorders tiles
- with(kosmos) { gridLayoutTypeRepository.setLayout(InfiniteGridLayoutType) }
- underTest.start()
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- @Test
- fun changeLayoutType_usesCorrectGridConsistencyInteractor() =
- with(kosmos) {
- testScope.runTest {
- // Using the no-op grid consistency interactor
- gridLayoutTypeRepository.setLayout(NoopGridLayoutType)
-
- // Setting an invalid layout with holes
- // [ Large A ] [ sa ]
- // [ Large B ] [ Large C ]
- // [ sb ] [ Large D ]
- val newTiles =
- listOf(
- TileSpec.create("largeA"),
- TileSpec.create("smallA"),
- TileSpec.create("largeB"),
- TileSpec.create("largeC"),
- TileSpec.create("smallB"),
- TileSpec.create("largeD"),
- )
- tileSpecRepository.setTiles(0, newTiles)
-
- runCurrent()
-
- val tiles = currentTilesInteractor.currentTiles.value
- val tileSpecs = tiles.map { it.spec }
-
- // Saved tiles should be unchanged
- assertThat(tileSpecs).isEqualTo(newTiles)
- }
- }
-
- @Test
- fun validTilesWithInfiniteGridConsistencyInteractor_unchangedList() =
- with(kosmos) {
- testScope.runTest {
- // Setting a valid layout with holes
- // [ Large A ] [ sa ][ sb ]
- // [ Large B ] [ Large C ]
- // [ Large D ]
- val newTiles =
- listOf(
- TileSpec.create("largeA"),
- TileSpec.create("smallA"),
- TileSpec.create("smallB"),
- TileSpec.create("largeB"),
- TileSpec.create("largeC"),
- TileSpec.create("largeD"),
- )
- tileSpecRepository.setTiles(0, newTiles)
-
- runCurrent()
-
- val tiles = currentTilesInteractor.currentTiles.value
- val tileSpecs = tiles.map { it.spec }
-
- // Saved tiles should be unchanged
- assertThat(tileSpecs).isEqualTo(newTiles)
- }
- }
-
- @Test
- fun invalidTilesWithInfiniteGridConsistencyInteractor_savesNewList() =
- with(kosmos) {
- testScope.runTest {
- // Setting an invalid layout with holes
- // [ sa ] [ Large A ]
- // [ Large B ] [ sb ] [ sc ]
- // [ sd ] [ se ] [ Large C ]
- val newTiles =
- listOf(
- TileSpec.create("smallA"),
- TileSpec.create("largeA"),
- TileSpec.create("largeB"),
- TileSpec.create("smallB"),
- TileSpec.create("smallC"),
- TileSpec.create("smallD"),
- TileSpec.create("smallE"),
- TileSpec.create("largeC"),
- )
- tileSpecRepository.setTiles(0, newTiles)
-
- runCurrent()
-
- val tiles = currentTilesInteractor.currentTiles.value
- val tileSpecs = tiles.map { it.spec }
-
- // Expected grid
- // [ sa ] [ Large A ] [ sb ]
- // [ Large B ] [ sc ] [ sd ]
- // [ se ] [ Large C ]
- val expectedTiles =
- listOf(
- TileSpec.create("smallA"),
- TileSpec.create("largeA"),
- TileSpec.create("smallB"),
- TileSpec.create("largeB"),
- TileSpec.create("smallC"),
- TileSpec.create("smallD"),
- TileSpec.create("smallE"),
- TileSpec.create("largeC"),
- )
-
- // Saved tiles should be unchanged
- assertThat(tileSpecs).isEqualTo(expectedTiles)
- }
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt
deleted file mode 100644
index ea51398..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt
+++ /dev/null
@@ -1,187 +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.systemui.qs.panels.domain.interactor
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository
-import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class InfiniteGridConsistencyInteractorTest : SysuiTestCase() {
-
- private val kosmos =
- testKosmos().apply {
- defaultLargeTilesRepository =
- object : DefaultLargeTilesRepository {
- override val defaultLargeTiles: Set<TileSpec> =
- setOf(
- TileSpec.create("largeA"),
- TileSpec.create("largeB"),
- TileSpec.create("largeC"),
- TileSpec.create("largeD"),
- )
- }
- }
- private val underTest = with(kosmos) { infiniteGridConsistencyInteractor }
-
- @Test
- fun validTiles_returnsUnchangedList() =
- with(kosmos) {
- testScope.runTest {
- // Original grid
- // [ Large A ] [ sa ][ sb ]
- // [ Large B ] [ Large C ]
- // [ Large D ]
- val tiles =
- listOf(
- TileSpec.create("largeA"),
- TileSpec.create("smallA"),
- TileSpec.create("smallB"),
- TileSpec.create("largeB"),
- TileSpec.create("largeC"),
- TileSpec.create("largeD"),
- )
-
- val newTiles = underTest.reconcileTiles(tiles)
-
- assertThat(newTiles).isEqualTo(tiles)
- }
- }
-
- @Test
- fun invalidTiles_moveIconTileForward() =
- with(kosmos) {
- testScope.runTest {
- // Original grid
- // [ Large A ] [ sa ]
- // [ Large B ] [ Large C ]
- // [ sb ] [ Large D ]
- val tiles =
- listOf(
- TileSpec.create("largeA"),
- TileSpec.create("smallA"),
- TileSpec.create("largeB"),
- TileSpec.create("largeC"),
- TileSpec.create("smallB"),
- TileSpec.create("largeD"),
- )
- // Expected grid
- // [ Large A ] [ sa ][ sb ]
- // [ Large B ] [ Large C ]
- // [ Large D ]
- val expectedTiles =
- listOf(
- TileSpec.create("largeA"),
- TileSpec.create("smallA"),
- TileSpec.create("smallB"),
- TileSpec.create("largeB"),
- TileSpec.create("largeC"),
- TileSpec.create("largeD"),
- )
-
- val newTiles = underTest.reconcileTiles(tiles)
-
- assertThat(newTiles).isEqualTo(expectedTiles)
- }
- }
-
- @Test
- fun invalidTiles_moveIconTileBack() =
- with(kosmos) {
- testScope.runTest {
- // Original grid
- // [ sa ] [ Large A ]
- // [ Large B ] [ Large C ]
- // [ Large D ]
- val tiles =
- listOf(
- TileSpec.create("smallA"),
- TileSpec.create("largeA"),
- TileSpec.create("largeB"),
- TileSpec.create("largeC"),
- TileSpec.create("largeD"),
- )
- // Expected grid
- // [ Large A ] [ Large B ]
- // [ Large C ] [ Large D ]
- // [ sa ]
- val expectedTiles =
- listOf(
- TileSpec.create("largeA"),
- TileSpec.create("largeB"),
- TileSpec.create("largeC"),
- TileSpec.create("largeD"),
- TileSpec.create("smallA"),
- )
-
- val newTiles = underTest.reconcileTiles(tiles)
-
- assertThat(newTiles).isEqualTo(expectedTiles)
- }
- }
-
- @Test
- fun invalidTiles_multipleCorrections() =
- with(kosmos) {
- testScope.runTest {
- // Original grid
- // [ sa ] [ Large A ]
- // [ Large B ] [ sb ] [ sc ]
- // [ sd ] [ se ] [ Large C ]
- val tiles =
- listOf(
- TileSpec.create("smallA"),
- TileSpec.create("largeA"),
- TileSpec.create("largeB"),
- TileSpec.create("smallB"),
- TileSpec.create("smallC"),
- TileSpec.create("smallD"),
- TileSpec.create("smallE"),
- TileSpec.create("largeC"),
- )
- // Expected grid
- // [ sa ] [ Large A ] [ sb ]
- // [ Large B ] [ sc ] [ sd ]
- // [ se ] [ Large C ]
- val expectedTiles =
- listOf(
- TileSpec.create("smallA"),
- TileSpec.create("largeA"),
- TileSpec.create("smallB"),
- TileSpec.create("largeB"),
- TileSpec.create("smallC"),
- TileSpec.create("smallD"),
- TileSpec.create("smallE"),
- TileSpec.create("largeC"),
- )
-
- val newTiles = underTest.reconcileTiles(tiles)
-
- assertThat(newTiles).isEqualTo(expectedTiles)
- }
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt
index 53384af..9e90090 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt
@@ -22,6 +22,7 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository
import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout
import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
@@ -44,12 +45,7 @@
}
private val underTest =
- with(kosmos) {
- InfiniteGridLayout(
- iconTilesViewModel,
- fixedColumnsSizeViewModel,
- )
- }
+ with(kosmos) { InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel) }
@Test
fun correctPagination_underOnePage_sameOrder() =
@@ -65,7 +61,7 @@
smallTile(),
largeTile(),
largeTile(),
- smallTile()
+ smallTile(),
)
val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 763a1a9..3850891 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -27,6 +27,7 @@
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.OverlayKey
import com.android.compose.animation.scene.SceneKey
import com.android.internal.logging.uiEventLoggerFake
import com.android.internal.policy.IKeyguardDismissCallback
@@ -88,9 +89,11 @@
import com.android.systemui.scene.data.repository.Transition
import com.android.systemui.scene.domain.interactor.sceneBackInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.shared.system.QuickStepContract
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
@@ -161,6 +164,7 @@
}
@Test
+ @DisableFlags(DualShade.FLAG_NAME)
fun hydrateVisibility() =
testScope.runTest {
val currentDesiredSceneKey by collectLastValue(sceneInteractor.currentScene)
@@ -221,6 +225,87 @@
}
@Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun hydrateVisibility_dualShade() =
+ testScope.runTest {
+ val currentDesiredSceneKey by collectLastValue(sceneInteractor.currentScene)
+ val currentDesiredOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ val isVisible by collectLastValue(sceneInteractor.isVisible)
+ val transitionStateFlow =
+ prepareState(
+ authenticationMethod = AuthenticationMethodModel.Pin,
+ isDeviceUnlocked = true,
+ initialSceneKey = Scenes.Gone,
+ )
+ assertThat(currentDesiredSceneKey).isEqualTo(Scenes.Gone)
+ assertThat(currentDesiredOverlays).isEmpty()
+ assertThat(isVisible).isTrue()
+
+ underTest.start()
+ assertThat(isVisible).isFalse()
+
+ // Expand the notifications shade.
+ fakeSceneDataSource.pause()
+ sceneInteractor.showOverlay(Overlays.NotificationsShade, "reason")
+ transitionStateFlow.value =
+ ObservableTransitionState.Transition.ShowOrHideOverlay(
+ overlay = Overlays.NotificationsShade,
+ fromContent = Scenes.Gone,
+ toContent = Overlays.NotificationsShade,
+ currentScene = Scenes.Gone,
+ currentOverlays = flowOf(emptySet()),
+ progress = flowOf(0.5f),
+ isInitiatedByUserInput = false,
+ isUserInputOngoing = flowOf(false),
+ previewProgress = flowOf(0f),
+ isInPreviewStage = flowOf(false),
+ )
+ assertThat(isVisible).isTrue()
+ fakeSceneDataSource.unpause(expectedScene = Scenes.Gone)
+ transitionStateFlow.value =
+ ObservableTransitionState.Idle(
+ currentScene = Scenes.Gone,
+ currentOverlays = setOf(Overlays.NotificationsShade),
+ )
+ assertThat(isVisible).isTrue()
+
+ // Collapse the notifications shade.
+ fakeSceneDataSource.pause()
+ sceneInteractor.hideOverlay(Overlays.NotificationsShade, "reason")
+ transitionStateFlow.value =
+ ObservableTransitionState.Transition.ShowOrHideOverlay(
+ overlay = Overlays.NotificationsShade,
+ fromContent = Overlays.NotificationsShade,
+ toContent = Scenes.Gone,
+ currentScene = Scenes.Gone,
+ currentOverlays = flowOf(setOf(Overlays.NotificationsShade)),
+ progress = flowOf(0.5f),
+ isInitiatedByUserInput = false,
+ isUserInputOngoing = flowOf(false),
+ previewProgress = flowOf(0f),
+ isInPreviewStage = flowOf(false),
+ )
+ assertThat(isVisible).isTrue()
+ fakeSceneDataSource.unpause(expectedScene = Scenes.Gone)
+ transitionStateFlow.value =
+ ObservableTransitionState.Idle(
+ currentScene = Scenes.Gone,
+ currentOverlays = emptySet(),
+ )
+ assertThat(isVisible).isFalse()
+
+ kosmos.headsUpNotificationRepository.setNotifications(
+ buildNotificationRows(isPinned = true)
+ )
+ assertThat(isVisible).isTrue()
+
+ kosmos.headsUpNotificationRepository.setNotifications(
+ buildNotificationRows(isPinned = false)
+ )
+ assertThat(isVisible).isFalse()
+ }
+
+ @Test
fun hydrateVisibility_basedOnDeviceProvisioning() =
testScope.runTest {
val isVisible by collectLastValue(sceneInteractor.isVisible)
@@ -1621,6 +1706,7 @@
}
@Test
+ @DisableFlags(DualShade.FLAG_NAME)
fun hydrateInteractionState_whileLocked() =
testScope.runTest {
val transitionStateFlow = prepareState(initialSceneKey = Scenes.Lockscreen)
@@ -1707,6 +1793,7 @@
}
@Test
+ @DisableFlags(DualShade.FLAG_NAME)
fun hydrateInteractionState_whileUnlocked() =
testScope.runTest {
val transitionStateFlow =
@@ -1795,6 +1882,186 @@
}
@Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun hydrateInteractionState_dualShade_whileLocked() =
+ testScope.runTest {
+ val currentDesiredOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ val transitionStateFlow = prepareState(initialSceneKey = Scenes.Lockscreen)
+ underTest.start()
+ runCurrent()
+ verify(centralSurfaces).setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true)
+ assertThat(currentDesiredOverlays).isEmpty()
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = Scenes.Bouncer,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces)
+ .setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false)
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = Scenes.Lockscreen,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces).setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true)
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateOverlayTransition(
+ transitionStateFlow = transitionStateFlow,
+ toOverlay = Overlays.NotificationsShade,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces)
+ .setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false)
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = Scenes.Lockscreen,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces).setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true)
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateOverlayTransition(
+ transitionStateFlow = transitionStateFlow,
+ toOverlay = Overlays.QuickSettingsShade,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ )
+ }
+
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun hydrateInteractionState_dualShade_whileUnlocked() =
+ testScope.runTest {
+ val currentDesiredOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ val transitionStateFlow =
+ prepareState(
+ authenticationMethod = AuthenticationMethodModel.Pin,
+ isDeviceUnlocked = true,
+ initialSceneKey = Scenes.Gone,
+ )
+ underTest.start()
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ assertThat(currentDesiredOverlays).isEmpty()
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = Scenes.Bouncer,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = Scenes.Lockscreen,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = Scenes.Shade,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = Scenes.Lockscreen,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = Scenes.QuickSettings,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ )
+ }
+
+ @Test
fun respondToFalsingDetections() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
@@ -2131,19 +2398,40 @@
verifyAfterTransition: (() -> Unit)? = null,
) {
val fromScene = sceneInteractor.currentScene.value
+ val fromOverlays = sceneInteractor.currentOverlays.value
sceneInteractor.changeScene(toScene, "reason")
runCurrent()
verifyBeforeTransition?.invoke()
transitionStateFlow.value =
- ObservableTransitionState.Transition(
- fromScene = fromScene,
- toScene = toScene,
- currentScene = flowOf(fromScene),
- progress = flowOf(0.5f),
- isInitiatedByUserInput = true,
- isUserInputOngoing = flowOf(true),
- )
+ if (fromOverlays.isEmpty()) {
+ // Regular scene-to-scene transition.
+ ObservableTransitionState.Transition.ChangeScene(
+ fromScene = fromScene,
+ toScene = toScene,
+ currentScene = flowOf(fromScene),
+ currentOverlays = fromOverlays,
+ progress = flowOf(0.5f),
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true),
+ previewProgress = flowOf(0f),
+ isInPreviewStage = flowOf(false),
+ )
+ } else {
+ // An overlay is present; hide it.
+ ObservableTransitionState.Transition.ShowOrHideOverlay(
+ overlay = fromOverlays.first(),
+ fromContent = fromOverlays.first(),
+ toContent = toScene,
+ currentScene = fromScene,
+ currentOverlays = sceneInteractor.currentOverlays,
+ progress = flowOf(0.5f),
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true),
+ previewProgress = flowOf(0f),
+ isInPreviewStage = flowOf(false),
+ )
+ }
runCurrent()
verifyDuringTransition?.invoke()
@@ -2152,6 +2440,60 @@
verifyAfterTransition?.invoke()
}
+ private fun TestScope.emulateOverlayTransition(
+ transitionStateFlow: MutableStateFlow<ObservableTransitionState>,
+ toOverlay: OverlayKey,
+ verifyBeforeTransition: (() -> Unit)? = null,
+ verifyDuringTransition: (() -> Unit)? = null,
+ verifyAfterTransition: (() -> Unit)? = null,
+ ) {
+ val fromScene = sceneInteractor.currentScene.value
+ val fromOverlays = sceneInteractor.currentOverlays.value
+ sceneInteractor.showOverlay(toOverlay, "reason")
+ runCurrent()
+ verifyBeforeTransition?.invoke()
+
+ transitionStateFlow.value =
+ if (fromOverlays.isEmpty()) {
+ // Show a new overlay.
+ ObservableTransitionState.Transition.ShowOrHideOverlay(
+ overlay = toOverlay,
+ fromContent = fromScene,
+ toContent = toOverlay,
+ currentScene = fromScene,
+ currentOverlays = sceneInteractor.currentOverlays,
+ progress = flowOf(0.5f),
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true),
+ previewProgress = flowOf(0f),
+ isInPreviewStage = flowOf(false),
+ )
+ } else {
+ // Overlay-to-overlay transition.
+ ObservableTransitionState.Transition.ReplaceOverlay(
+ fromOverlay = fromOverlays.first(),
+ toOverlay = toOverlay,
+ currentScene = fromScene,
+ currentOverlays = sceneInteractor.currentOverlays,
+ progress = flowOf(0.5f),
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true),
+ previewProgress = flowOf(0f),
+ isInPreviewStage = flowOf(false),
+ )
+ }
+ runCurrent()
+ verifyDuringTransition?.invoke()
+
+ transitionStateFlow.value =
+ ObservableTransitionState.Idle(
+ currentScene = fromScene,
+ currentOverlays = setOf(toOverlay),
+ )
+ runCurrent()
+ verifyAfterTransition?.invoke()
+ }
+
private fun TestScope.prepareState(
isDeviceUnlocked: Boolean = false,
isBypassEnabled: Boolean = false,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt
index 4d69f0d..f86337e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt
@@ -19,8 +19,8 @@
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_COMPOSE_LOCKSCREEN
import com.android.systemui.Flags.FLAG_EXAMPLE_FLAG
+import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.andSceneContainer
@@ -66,7 +66,7 @@
@Test
fun oneDependencyAndSceneContainer() {
- val dependentFlag = FLAG_COMPOSE_LOCKSCREEN
+ val dependentFlag = FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
val result = FlagsParameterization.allCombinationsOf(dependentFlag).andSceneContainer()
Truth.assertThat(result).hasSize(3)
Truth.assertThat(result[0].mOverrides[dependentFlag]).isFalse()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index fb32855..0f6dc07 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -17,7 +17,9 @@
package com.android.systemui.statusbar.policy.domain.interactor
import android.app.AutomaticZenRule
+import android.app.Flags
import android.app.NotificationManager.Policy
+import android.platform.test.annotations.EnableFlags
import android.provider.Settings
import android.provider.Settings.Secure.ZEN_DURATION
import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
@@ -32,6 +34,7 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
+import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -50,10 +53,31 @@
private val testScope = kosmos.testScope
private val zenModeRepository = kosmos.fakeZenModeRepository
private val settingsRepository = kosmos.secureSettingsRepository
+ private val deviceProvisioningRepository = kosmos.fakeDeviceProvisioningRepository
private val underTest = kosmos.zenModeInteractor
@Test
+ fun isZenAvailable_off() =
+ testScope.runTest {
+ val isZenAvailable by collectLastValue(underTest.isZenAvailable)
+ deviceProvisioningRepository.setDeviceProvisioned(false)
+ runCurrent()
+
+ assertThat(isZenAvailable).isFalse()
+ }
+
+ @Test
+ fun isZenAvailable_on() =
+ testScope.runTest {
+ val isZenAvailable by collectLastValue(underTest.isZenAvailable)
+ deviceProvisioningRepository.setDeviceProvisioned(true)
+ runCurrent()
+
+ assertThat(isZenAvailable).isTrue()
+ }
+
+ @Test
fun isZenModeEnabled_off() =
testScope.runTest {
val enabled by collectLastValue(underTest.isZenModeEnabled)
@@ -337,4 +361,22 @@
runCurrent()
assertThat(mainActiveMode).isNull()
}
+
+ @Test
+ @EnableFlags(Flags.FLAG_MODES_UI)
+ fun dndMode_flows() =
+ testScope.runTest {
+ val dndMode by collectLastValue(underTest.dndMode)
+
+ zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
+ runCurrent()
+
+ assertThat(dndMode!!.isActive).isFalse()
+
+ zenModeRepository.removeMode(TestModeBuilder.MANUAL_DND_INACTIVE.id)
+ zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE)
+ runCurrent()
+
+ assertThat(dndMode!!.isActive).isTrue()
+ }
}
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index f9c2aef..ba3822b 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3114,6 +3114,10 @@
<string name="media_output_group_title_speakers_and_displays">Speakers & Displays</string>
<!-- Title for Suggested Devices group. [CHAR LIMIT=NONE] -->
<string name="media_output_group_title_suggested_device">Suggested Devices</string>
+ <!-- Title for input device group. [CHAR LIMIT=NONE] -->
+ <string name="media_input_group_title">Input</string>
+ <!-- Title for output device group. [CHAR LIMIT=NONE] -->
+ <string name="media_output_group_title">Output</string>
<!-- Summary for end session dialog. [CHAR LIMIT=NONE] -->
<string name="media_output_end_session_dialog_summary">Stop your shared session to move media to another device</string>
<!-- Button text for stopping session [CHAR LIMIT=60] -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 7efe2dd..ffbc85c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -968,6 +968,15 @@
constraintSet.constrainWidth(mViewFlipper.getId(), MATCH_CONSTRAINT);
constraintSet.applyTo(mView);
}
+
+ @Override
+ public void onDestroy() {
+ if (mView == null) return;
+ ConstraintSet constraintSet = new ConstraintSet();
+ constraintSet.clone(mView);
+ constraintSet.clear(mViewFlipper.getId());
+ constraintSet.applyTo(mView);
+ }
}
/**
@@ -1043,12 +1052,20 @@
@Override
public void onDensityOrFontScaleChanged() {
mView.removeView(mUserSwitcherViewGroup);
+ mView.removeView(mUserSwitcher);
inflateUserSwitcher();
}
@Override
public void onDestroy() {
- mUserSwitcherController.removeUserSwitchCallback(mUserSwitchCallback);
+ ConstraintSet constraintSet = new ConstraintSet();
+ constraintSet.clone(mView);
+ constraintSet.clear(mUserSwitcherViewGroup.getId());
+ constraintSet.clear(mViewFlipper.getId());
+ constraintSet.applyTo(mView);
+
+ mView.removeView(mUserSwitcherViewGroup);
+ mView.removeView(mUserSwitcher);
}
private Drawable findLargeUserIcon(int userId) {
@@ -1344,5 +1361,13 @@
constraintSet.constrainPercentWidth(mViewFlipper.getId(), 0.5f);
constraintSet.applyTo(mView);
}
+
+ @Override
+ public void onDestroy() {
+ ConstraintSet constraintSet = new ConstraintSet();
+ constraintSet.clone(mView);
+ constraintSet.clear(mViewFlipper.getId());
+ constraintSet.applyTo(mView);
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
index c7a47b1..1ada56d 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
@@ -30,6 +30,7 @@
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Build;
+import android.os.UserHandle;
import android.provider.Settings;
import android.util.Log;
@@ -37,6 +38,7 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.user.utils.UserScopedService;
import javax.inject.Inject;
import javax.inject.Provider;
@@ -67,13 +69,13 @@
public ClipboardListener(Context context,
Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
ClipboardToast clipboardToast,
- ClipboardManager clipboardManager,
+ UserScopedService<ClipboardManager> clipboardManager,
KeyguardManager keyguardManager,
UiEventLogger uiEventLogger) {
mContext = context;
mOverlayProvider = clipboardOverlayControllerProvider;
mClipboardToast = clipboardToast;
- mClipboardManager = clipboardManager;
+ mClipboardManager = clipboardManager.forUser(UserHandle.CURRENT);
mKeyguardManager = keyguardManager;
mUiEventLogger = uiEventLogger;
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 8818c3a..8f913ff 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -733,9 +733,8 @@
}
@Provides
- @Singleton
- static ClipboardManager provideClipboardManager(Context context) {
- return context.getSystemService(ClipboardManager.class);
+ static UserScopedService<ClipboardManager> provideClipboardManager(Context context) {
+ return new UserScopedServiceImpl<>(context, ClipboardManager.class);
}
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
index e17e530..5259c5d 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
@@ -26,7 +26,9 @@
import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource
import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus
import com.android.systemui.flags.SystemPropertiesHelper
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.TrustInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import javax.inject.Inject
@@ -36,6 +38,7 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
@@ -57,6 +60,7 @@
private val powerInteractor: PowerInteractor,
private val biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor,
private val systemPropertiesHelper: SystemPropertiesHelper,
+ keyguardTransitionInteractor: KeyguardTransitionInteractor,
) {
private val deviceUnlockSource =
@@ -74,7 +78,7 @@
trustInteractor.isTrusted.filter { it }.map { DeviceUnlockSource.TrustAgent },
authenticationInteractor.onAuthenticationResult
.filter { it }
- .map { DeviceUnlockSource.BouncerInput }
+ .map { DeviceUnlockSource.BouncerInput },
)
private val faceEnrolledAndEnabled = biometricSettingsInteractor.isFaceAuthEnrolledAndEnabled
@@ -170,10 +174,20 @@
combine(
powerInteractor.isAsleep,
isInLockdown,
- ::Pair,
+ keyguardTransitionInteractor
+ .transitionValue(KeyguardState.AOD)
+ .map { it == 1f }
+ .distinctUntilChanged(),
+ ::Triple,
)
- .flatMapLatestConflated { (isAsleep, isInLockdown) ->
- if (isAsleep || isInLockdown) {
+ .flatMapLatestConflated { (isAsleep, isInLockdown, isAod) ->
+ val isForceLocked =
+ when {
+ isAsleep && !isAod -> true
+ isInLockdown -> true
+ else -> false
+ }
+ if (isForceLocked) {
flowOf(DeviceUnlockStatus(false, null))
} else {
deviceUnlockSource.map { DeviceUnlockStatus(true, it) }
diff --git a/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt b/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt
index 1daaa11..500c5b3 100644
--- a/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.education.data.model
+import com.android.systemui.contextualeducation.GestureType
import java.time.Instant
/**
@@ -23,6 +24,7 @@
* gesture stores its own model separately.
*/
data class GestureEduModel(
+ val gestureType: GestureType,
val signalCount: Int = 0,
val educationShownCount: Int = 0,
val lastShortcutTriggeredTime: Instant? = null,
diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
index 01f838f..2978595 100644
--- a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
@@ -17,6 +17,8 @@
package com.android.systemui.education.data.repository
import android.content.Context
+import android.hardware.input.InputManager
+import android.hardware.input.KeyGestureEvent
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.MutablePreferences
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
@@ -25,23 +27,31 @@
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.preferencesDataStoreFile
+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.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.education.dagger.ContextualEducationModule.EduDataStoreScope
import com.android.systemui.education.data.model.EduDeviceConnectionTime
import com.android.systemui.education.data.model.GestureEduModel
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import java.time.Instant
+import java.util.concurrent.Executor
import javax.inject.Inject
import javax.inject.Provider
import kotlin.properties.Delegates.notNull
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
/**
@@ -64,6 +74,8 @@
suspend fun updateEduDeviceConnectionTime(
transform: (EduDeviceConnectionTime) -> EduDeviceConnectionTime
)
+
+ val keyboardShortcutTriggered: Flow<GestureType>
}
/**
@@ -75,9 +87,13 @@
@Inject
constructor(
@Application private val applicationContext: Context,
- @EduDataStoreScope private val dataStoreScopeProvider: Provider<CoroutineScope>
+ @EduDataStoreScope private val dataStoreScopeProvider: Provider<CoroutineScope>,
+ private val inputManager: InputManager,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
) : ContextualEducationRepository {
companion object {
+ const val TAG = "UserContextualEducationRepository"
+
const val SIGNAL_COUNT_SUFFIX = "_SIGNAL_COUNT"
const val NUMBER_OF_EDU_SHOWN_SUFFIX = "_NUMBER_OF_EDU_SHOWN"
const val LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX = "_LAST_SHORTCUT_TRIGGERED_TIME"
@@ -98,6 +114,30 @@
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
private val prefData: Flow<Preferences> = datastore.filterNotNull().flatMapLatest { it.data }
+ override val keyboardShortcutTriggered: Flow<GestureType> =
+ conflatedCallbackFlow {
+ val listener =
+ InputManager.KeyGestureEventListener { event ->
+ // Only store keyboard shortcut time for gestures providing keyboard
+ // education
+ val shortcutType =
+ when (event.keyGestureType) {
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS,
+ 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) }
+ }
+ .flowOn(backgroundDispatcher)
+
override fun setUser(userId: Int) {
dataStoreScope?.cancel()
val newDsScope = dataStoreScopeProvider.get()
@@ -136,7 +176,8 @@
preferences[getLastEducationTimeKey(gestureType)]?.let {
Instant.ofEpochSecond(it)
},
- userId = userId
+ userId = userId,
+ gestureType = gestureType,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
index 10be26e..c88b364 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
@@ -18,7 +18,10 @@
import com.android.systemui.CoreStartable
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
@@ -53,6 +56,13 @@
) : CoreStartable {
val backGestureModelFlow = readEduModelsOnSignalCountChanged(BACK)
+ val homeGestureModelFlow = readEduModelsOnSignalCountChanged(HOME)
+ val overviewGestureModelFlow = readEduModelsOnSignalCountChanged(OVERVIEW)
+ val allAppsGestureModelFlow = readEduModelsOnSignalCountChanged(ALL_APPS)
+ val eduDeviceConnectionTimeFlow =
+ repository.readEduDeviceConnectionTime().distinctUntilChanged()
+
+ val keyboardShortcutTriggered = repository.keyboardShortcutTriggered
override fun start() {
backgroundScope.launch {
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 43855d9..faee326 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,15 +16,8 @@
package com.android.systemui.education.domain.interactor
-import android.hardware.input.InputManager
-import android.hardware.input.InputManager.KeyGestureEventListener
-import android.hardware.input.KeyGestureEvent
import android.os.SystemProperties
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.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.education.dagger.ContextualEducationModule.EduClock
@@ -32,19 +25,19 @@
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
import kotlin.time.Duration.Companion.days
import kotlin.time.DurationUnit
import kotlin.time.toDuration
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
/** Allow listening to new contextual education triggered */
@@ -55,7 +48,6 @@
@Background private val backgroundScope: CoroutineScope,
private val contextualEducationInteractor: ContextualEducationInteractor,
private val userInputDeviceRepository: UserInputDeviceRepository,
- private val inputManager: InputManager,
@EduClock private val clock: Clock,
) : CoreStartable {
@@ -82,34 +74,32 @@
private val _educationTriggered = MutableStateFlow<EducationInfo?>(null)
val educationTriggered = _educationTriggered.asStateFlow()
- private val keyboardShortcutTriggered: Flow<GestureType> = conflatedCallbackFlow {
- val listener = KeyGestureEventListener { event ->
- // Only store keyboard shortcut time for gestures providing keyboard education
- val shortcutType =
- when (event.keyGestureType) {
- 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) }
- }
-
+ @OptIn(ExperimentalCoroutinesApi::class)
override fun start() {
backgroundScope.launch {
- contextualEducationInteractor.backGestureModelFlow.collect {
- if (isUsageSessionExpired(it)) {
- contextualEducationInteractor.startNewUsageSession(BACK)
- } else if (isEducationNeeded(it)) {
- _educationTriggered.value = EducationInfo(BACK, getEduType(it), it.userId)
- contextualEducationInteractor.updateOnEduTriggered(BACK)
+ contextualEducationInteractor.eduDeviceConnectionTimeFlow
+ .flatMapLatest {
+ val gestureFlows = mutableListOf<Flow<GestureEduModel>>()
+ if (it.touchpadFirstConnectionTime != null) {
+ gestureFlows.add(contextualEducationInteractor.backGestureModelFlow)
+ gestureFlows.add(contextualEducationInteractor.homeGestureModelFlow)
+ gestureFlows.add(contextualEducationInteractor.overviewGestureModelFlow)
+ }
+
+ if (it.keyboardFirstConnectionTime != null) {
+ gestureFlows.add(contextualEducationInteractor.allAppsGestureModelFlow)
+ }
+ gestureFlows.merge()
}
- }
+ .collect {
+ if (isUsageSessionExpired(it)) {
+ contextualEducationInteractor.startNewUsageSession(it.gestureType)
+ } else if (isEducationNeeded(it)) {
+ _educationTriggered.value =
+ EducationInfo(it.gestureType, getEduType(it), it.userId)
+ contextualEducationInteractor.updateOnEduTriggered(it.gestureType)
+ }
+ }
}
backgroundScope.launch {
@@ -139,7 +129,7 @@
}
backgroundScope.launch {
- keyboardShortcutTriggered.collect {
+ contextualEducationInteractor.keyboardShortcutTriggered.collect {
contextualEducationInteractor.updateShortcutTriggerTime(it)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 6318dc0..0b775ab 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -31,9 +31,7 @@
import com.android.systemui.Flags.statusBarScreenSharingChips
import com.android.systemui.Flags.statusBarUseReposForCallChip
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
import com.android.systemui.keyguard.MigrateClocksToBlueprint
-import com.android.systemui.keyguard.shared.ComposeLockscreen
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
@@ -62,10 +60,6 @@
// SceneContainer dependencies
SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
- // ComposeLockscreen dependencies
- ComposeLockscreen.token dependsOn KeyguardBottomAreaRefactor.token
- ComposeLockscreen.token dependsOn MigrateClocksToBlueprint.token
-
// CommunalHub dependencies
communalHub dependsOn MigrateClocksToBlueprint.token
@@ -99,7 +93,7 @@
get() =
FlagToken(
FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON,
- statusBarCallChipNotificationIcon()
+ statusBarCallChipNotificationIcon(),
)
private inline val statusBarScreenSharingChipsToken
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
index e50c05c..8966209 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
@@ -29,6 +29,7 @@
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.dagger.QSLog
+import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -50,6 +51,7 @@
constructor(
private val vibratorHelper: VibratorHelper?,
private val keyguardStateController: KeyguardStateController,
+ private val falsingManager: FalsingManager,
@QSLog private val logBuffer: LogBuffer,
) {
@@ -72,7 +74,7 @@
private val durations =
vibratorHelper?.getPrimitiveDurations(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
- VibrationEffect.Composition.PRIMITIVE_SPIN
+ VibrationEffect.Composition.PRIMITIVE_SPIN,
)
private var longPressHint: VibrationEffect? = null
@@ -152,15 +154,27 @@
logEvent(qsTile?.tileSpec, state, "animation completed")
when (state) {
State.RUNNING_FORWARD -> {
- vibrate(snapEffect)
- if (keyguardStateController.isUnlocked) {
- setState(State.LONG_CLICKED)
- } else {
+ val wasFalseLongTap = falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)
+ if (wasFalseLongTap) {
callback?.onResetProperties()
setState(State.IDLE)
+ logEvent(qsTile?.tileSpec, state, "false long click. No action triggered")
+ } else if (keyguardStateController.isUnlocked) {
+ vibrate(snapEffect)
+ setState(State.LONG_CLICKED)
+ qsTile?.longClick(expandable)
+ logEvent(qsTile?.tileSpec, state, "long click action triggered")
+ } else {
+ vibrate(snapEffect)
+ callback?.onResetProperties()
+ setState(State.IDLE)
+ qsTile?.longClick(expandable)
+ logEvent(
+ qsTile?.tileSpec,
+ state,
+ "properties reset and long click action triggered",
+ )
}
- logEvent(qsTile?.tileSpec, state, "long click action triggered")
- qsTile?.longClick(expandable)
}
State.RUNNING_BACKWARDS_FROM_UP -> {
callback?.onEffectFinishedReversing()
@@ -236,7 +250,7 @@
LongPressHapticBuilder.createLongPressHint(
durations?.get(0) ?: LongPressHapticBuilder.INVALID_DURATION,
durations?.get(1) ?: LongPressHapticBuilder.INVALID_DURATION,
- effectDuration
+ effectDuration,
)
setState(State.IDLE)
return true
@@ -265,7 +279,7 @@
}
override fun dialogTransitionController(
- cuj: DialogCuj?,
+ cuj: DialogCuj?
): DialogTransitionAnimator.Controller? =
DialogTransitionAnimator.Controller.fromView(view, cuj)
}
@@ -298,7 +312,7 @@
str2 = event
str3 = state.name
},
- { "[long-press effect on $str1 tile] $str2 on state: $str3" }
+ { "[long-press effect on $str1 tile] $str2 on state: $str3" },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index df0f10a..416eaba 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -24,12 +24,6 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
-import androidx.constraintlayout.widget.ConstraintSet
-import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
-import androidx.constraintlayout.widget.ConstraintSet.END
-import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
-import androidx.constraintlayout.widget.ConstraintSet.START
-import androidx.constraintlayout.widget.ConstraintSet.TOP
import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayout
@@ -47,7 +41,6 @@
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
-import com.android.systemui.keyguard.shared.ComposeLockscreen
import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
import com.android.systemui.keyguard.ui.binder.KeyguardBlueprintViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
@@ -128,7 +121,7 @@
keyguardStatusViewComponentFactory.build(
LayoutInflater.from(context).inflate(R.layout.keyguard_status_view, null)
as KeyguardStatusView,
- context.display
+ context.display,
)
val controller = statusViewComponent.keyguardStatusViewController
controller.init()
@@ -143,29 +136,12 @@
initializeViews()
if (!SceneContainerFlag.isEnabled) {
- if (ComposeLockscreen.isEnabled) {
- val composeView =
- createLockscreen(
- context = context,
- viewModelFactory = lockscreenContentViewModelFactory,
- blueprints = lockscreenSceneBlueprintsLazy.get(),
- )
- composeView.id = View.generateViewId()
- val cs = ConstraintSet()
- cs.clone(keyguardRootView)
- cs.connect(composeView.id, START, PARENT_ID, START)
- cs.connect(composeView.id, END, PARENT_ID, END)
- cs.connect(composeView.id, TOP, PARENT_ID, TOP)
- cs.connect(composeView.id, BOTTOM, PARENT_ID, BOTTOM)
- keyguardRootView.addView(composeView)
- } else {
- KeyguardBlueprintViewBinder.bind(
- keyguardRootView,
- keyguardBlueprintViewModel,
- keyguardClockViewModel,
- smartspaceViewModel,
- )
- }
+ KeyguardBlueprintViewBinder.bind(
+ keyguardRootView,
+ keyguardBlueprintViewModel,
+ keyguardClockViewModel,
+ smartspaceViewModel,
+ )
}
if (deviceEntryUnlockTrackerViewBinder.isPresent) {
deviceEntryUnlockTrackerViewBinder.get().bind(keyguardRootView)
@@ -247,7 +223,7 @@
LockscreenContent(
viewModelFactory = viewModelFactory,
blueprints = sceneBlueprints,
- clockInteractor = clockInteractor
+ clockInteractor = clockInteractor,
)
) {
Content(modifier = Modifier.fillMaxSize())
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt
index 406b9f6..be87334 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt
@@ -25,7 +25,9 @@
import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
import android.service.notification.ZenModeConfig
+import android.util.Log
import com.android.settingslib.notification.modes.EnableZenModeDialog
+import com.android.settingslib.notification.modes.ZenMode
import com.android.settingslib.notification.modes.ZenModeDialogMetricsLogger
import com.android.systemui.animation.Expandable
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
@@ -35,30 +37,38 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
+import com.android.systemui.modes.shared.ModesUi
import com.android.systemui.res.R
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.ZenModeController
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
@SysUISingleton
class DoNotDisturbQuickAffordanceConfig
constructor(
private val context: Context,
private val controller: ZenModeController,
+ private val interactor: ZenModeInteractor,
private val secureSettings: SecureSettings,
private val userTracker: UserTracker,
@Background private val backgroundDispatcher: CoroutineDispatcher,
+ @Background private val backgroundScope: CoroutineScope,
private val testConditionId: Uri?,
testDialog: EnableZenModeDialog?,
) : KeyguardQuickAffordanceConfig {
@@ -67,15 +77,45 @@
constructor(
context: Context,
controller: ZenModeController,
+ interactor: ZenModeInteractor,
secureSettings: SecureSettings,
userTracker: UserTracker,
@Background backgroundDispatcher: CoroutineDispatcher,
- ) : this(context, controller, secureSettings, userTracker, backgroundDispatcher, null, null)
+ @Background backgroundScope: CoroutineScope,
+ ) : this(
+ context,
+ controller,
+ interactor,
+ secureSettings,
+ userTracker,
+ backgroundDispatcher,
+ backgroundScope,
+ null,
+ null,
+ )
- private var dndMode: Int = 0
- private var isAvailable = false
+ private var zenMode: Int = 0
+ private var oldIsAvailable = false
private var settingsValue: Int = 0
+ private val dndMode: StateFlow<ZenMode?> by lazy {
+ ModesUi.assertInNewMode()
+ interactor.dndMode.stateIn(
+ scope = backgroundScope,
+ started = SharingStarted.Eagerly,
+ initialValue = null,
+ )
+ }
+
+ private val isAvailable: StateFlow<Boolean> by lazy {
+ ModesUi.assertInNewMode()
+ interactor.isZenAvailable.stateIn(
+ scope = backgroundScope,
+ started = SharingStarted.Eagerly,
+ initialValue = false,
+ )
+ }
+
private val conditionUri: Uri
get() =
testConditionId
@@ -104,42 +144,68 @@
override val pickerIconResourceId: Int = R.drawable.ic_do_not_disturb
override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
- combine(
- conflatedCallbackFlow {
- val callback =
- object : ZenModeController.Callback {
- override fun onZenChanged(zen: Int) {
- dndMode = zen
- trySendWithFailureLogging(updateState(), TAG)
+ if (ModesUi.isEnabled) {
+ combine(isAvailable, dndMode) { isAvailable, dndMode ->
+ if (!isAvailable) {
+ KeyguardQuickAffordanceConfig.LockScreenState.Hidden
+ } else if (dndMode?.isActive == true) {
+ KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+ Icon.Resource(
+ R.drawable.qs_dnd_icon_on,
+ ContentDescription.Resource(R.string.dnd_is_on),
+ ),
+ ActivationState.Active,
+ )
+ } else {
+ KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+ Icon.Resource(
+ R.drawable.qs_dnd_icon_off,
+ ContentDescription.Resource(R.string.dnd_is_off),
+ ),
+ ActivationState.Inactive,
+ )
+ }
+ }
+ } else {
+ combine(
+ conflatedCallbackFlow {
+ val callback =
+ object : ZenModeController.Callback {
+ override fun onZenChanged(zen: Int) {
+ zenMode = zen
+ trySendWithFailureLogging(updateState(), TAG)
+ }
+
+ override fun onZenAvailableChanged(available: Boolean) {
+ oldIsAvailable = available
+ trySendWithFailureLogging(updateState(), TAG)
+ }
}
- override fun onZenAvailableChanged(available: Boolean) {
- isAvailable = available
- trySendWithFailureLogging(updateState(), TAG)
- }
- }
+ zenMode = controller.zen
+ oldIsAvailable = controller.isZenAvailable
+ trySendWithFailureLogging(updateState(), TAG)
- dndMode = controller.zen
- isAvailable = controller.isZenAvailable
- trySendWithFailureLogging(updateState(), TAG)
+ controller.addCallback(callback)
- controller.addCallback(callback)
-
- awaitClose { controller.removeCallback(callback) }
- },
- secureSettings
- .observerFlow(userTracker.userId, Settings.Secure.ZEN_DURATION)
- .onStart { emit(Unit) }
- .map { secureSettings.getInt(Settings.Secure.ZEN_DURATION, ZEN_MODE_OFF) }
- .flowOn(backgroundDispatcher)
- .distinctUntilChanged()
- .onEach { settingsValue = it }
- ) { callbackFlowValue, _ ->
- callbackFlowValue
+ awaitClose { controller.removeCallback(callback) }
+ },
+ secureSettings
+ .observerFlow(userTracker.userId, Settings.Secure.ZEN_DURATION)
+ .onStart { emit(Unit) }
+ .map { secureSettings.getInt(Settings.Secure.ZEN_DURATION, ZEN_MODE_OFF) }
+ .flowOn(backgroundDispatcher)
+ .distinctUntilChanged()
+ .onEach { settingsValue = it },
+ ) { callbackFlowValue, _ ->
+ callbackFlowValue
+ }
}
override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
- return if (controller.isZenAvailable) {
+ val isZenAvailable = if (ModesUi.isEnabled) isAvailable.value else controller.isZenAvailable
+
+ return if (isZenAvailable) {
KeyguardQuickAffordanceConfig.PickerScreenState.Default(
configureIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS)
)
@@ -151,32 +217,63 @@
override fun onTriggered(
expandable: Expandable?
): KeyguardQuickAffordanceConfig.OnTriggeredResult {
- return when {
- !isAvailable -> KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
- dndMode != ZEN_MODE_OFF -> {
- controller.setZen(ZEN_MODE_OFF, null, TAG)
+ return if (ModesUi.isEnabled) {
+ if (!isAvailable.value) {
KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+ } else {
+ val dnd = dndMode.value
+ if (dnd == null) {
+ Log.wtf(TAG, "Triggered DND but it's null!?")
+ return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+ }
+ if (dnd.isActive) {
+ interactor.deactivateMode(dnd)
+ return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+ } else {
+ if (interactor.shouldAskForZenDuration(dnd)) {
+ // NOTE: The dialog handles turning on the mode itself.
+ return KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog(
+ dialog.createDialog(),
+ expandable,
+ )
+ } else {
+ interactor.activateMode(dnd)
+ return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+ }
+ }
}
- settingsValue == ZEN_DURATION_PROMPT ->
- KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog(
- dialog.createDialog(),
- expandable
- )
- settingsValue == ZEN_DURATION_FOREVER -> {
- controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG)
- KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
- }
- else -> {
- controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, conditionUri, TAG)
- KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+ } else {
+ when {
+ !oldIsAvailable -> KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+ zenMode != ZEN_MODE_OFF -> {
+ controller.setZen(ZEN_MODE_OFF, null, TAG)
+ KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+ }
+
+ settingsValue == ZEN_DURATION_PROMPT ->
+ KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog(
+ dialog.createDialog(),
+ expandable,
+ )
+
+ settingsValue == ZEN_DURATION_FOREVER -> {
+ controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG)
+ KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+ }
+
+ else -> {
+ controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, conditionUri, TAG)
+ KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+ }
}
}
}
private fun updateState(): KeyguardQuickAffordanceConfig.LockScreenState {
- return if (!isAvailable) {
+ ModesUi.assertInLegacyMode()
+ return if (!oldIsAvailable) {
KeyguardQuickAffordanceConfig.LockScreenState.Hidden
- } else if (dndMode == ZEN_MODE_OFF) {
+ } else if (zenMode == ZEN_MODE_OFF) {
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
Icon.Resource(
R.drawable.qs_dnd_icon_off,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
index 7afc759..6932eb5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
@@ -25,13 +25,13 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardBlueprintRepository
-import com.android.systemui.keyguard.shared.ComposeLockscreen
import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -64,7 +64,7 @@
/** Current BlueprintId */
val blueprintId =
shadeInteractor.isShadeLayoutWide.map { isShadeLayoutWide ->
- val useSplitShade = isShadeLayoutWide && !ComposeLockscreen.isEnabled
+ val useSplitShade = isShadeLayoutWide && !SceneContainerFlag.isEnabled
when {
useSplitShade -> SplitShadeKeyguardBlueprint.ID
else -> DefaultKeyguardBlueprint.DEFAULT
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt
deleted file mode 100644
index 601fbfa..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt
+++ /dev/null
@@ -1,53 +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.systemui.keyguard.shared
-
-import com.android.systemui.Flags
-import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
-
-/** Helper for reading or using the compose lockscreen flag state. */
-@Suppress("NOTHING_TO_INLINE")
-object ComposeLockscreen {
- /** The aconfig flag name */
- const val FLAG_NAME = Flags.FLAG_COMPOSE_LOCKSCREEN
-
- /** A token used for dependency declaration */
- val token: FlagToken
- get() = FlagToken(FLAG_NAME, isEnabled)
-
- /** Is the refactor enabled */
- @JvmStatic
- inline val isEnabled
- get() = Flags.composeLockscreen()
-
- /**
- * Called to ensure code is only run when the flag is enabled. This protects users from the
- * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
- * build to ensure that the refactor author catches issues in testing.
- */
- @JvmStatic
- inline fun isUnexpectedlyInLegacyMode() =
- RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
- /**
- * Called to ensure code is only run when the flag is disabled. This will throw an exception if
- * the flag is enabled to ensure that the refactor author catches issues in testing.
- */
- @JvmStatic
- inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index ed82159..deb0b2d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -59,7 +59,6 @@
import com.android.systemui.keyguard.KeyguardViewMediator
import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
-import com.android.systemui.keyguard.shared.ComposeLockscreen
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
@@ -72,6 +71,7 @@
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.CrossFadeHelper
import com.android.systemui.statusbar.VibratorHelper
@@ -241,7 +241,7 @@
disposables +=
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.CREATED) {
- if (ComposeLockscreen.isEnabled) {
+ if (SceneContainerFlag.isEnabled) {
view.setViewTreeOnBackPressedDispatcherOwner(
object : OnBackPressedDispatcherOwner {
override val onBackPressedDispatcher =
@@ -261,10 +261,7 @@
->
if (biometricMessage?.message != null) {
chipbarCoordinator!!.displayView(
- createChipbarInfo(
- biometricMessage.message,
- R.drawable.ic_lock,
- )
+ createChipbarInfo(biometricMessage.message, R.drawable.ic_lock)
)
} else {
chipbarCoordinator!!.removeView(ID, "occludingAppMsgNull")
@@ -327,12 +324,16 @@
.getDimensionPixelSize(R.dimen.shelf_appear_translation)
.stateIn(this)
viewModel.isNotifIconContainerVisible.collect { isVisible ->
- childViews[aodNotificationIconContainerId]
- ?.setAodNotifIconContainerIsVisible(
- isVisible,
- iconsAppearTranslationPx.value,
- screenOffAnimationController,
- )
+ if (isVisible.value) {
+ blueprintViewModel.refreshBlueprint()
+ } else {
+ childViews[aodNotificationIconContainerId]
+ ?.setAodNotifIconContainerIsVisible(
+ isVisible,
+ iconsAppearTranslationPx.value,
+ screenOffAnimationController,
+ )
+ }
}
}
@@ -382,7 +383,7 @@
if (msdlFeedback()) {
msdlPlayer?.playToken(
MSDLToken.UNLOCK,
- authInteractionProperties
+ authInteractionProperties,
)
} else {
vibratorHelper.performHapticFeedback(
@@ -398,7 +399,7 @@
if (msdlFeedback()) {
msdlPlayer?.playToken(
MSDLToken.FAILURE,
- authInteractionProperties
+ authInteractionProperties,
)
} else {
vibratorHelper.performHapticFeedback(
@@ -425,7 +426,7 @@
blueprintViewModel,
clockViewModel,
childViews,
- burnInParams
+ burnInParams,
)
)
@@ -464,11 +465,7 @@
*/
private fun createChipbarInfo(message: String, @DrawableRes icon: Int): ChipbarInfo {
return ChipbarInfo(
- startIcon =
- TintedIcon(
- Icon.Resource(icon, null),
- ChipbarInfo.DEFAULT_ICON_TINT,
- ),
+ startIcon = TintedIcon(Icon.Resource(icon, null), ChipbarInfo.DEFAULT_ICON_TINT),
text = Text.Loaded(message),
endItem = null,
vibrationEffect = null,
@@ -499,7 +496,7 @@
oldLeft: Int,
oldTop: Int,
oldRight: Int,
- oldBottom: Int
+ oldBottom: Int,
) {
// After layout, ensure the notifications are positioned correctly
childViews[nsslPlaceholderId]?.let { notificationListPlaceholder ->
@@ -515,7 +512,7 @@
viewModel.onNotificationContainerBoundsChanged(
notificationListPlaceholder.top.toFloat(),
notificationListPlaceholder.bottom.toFloat(),
- animate = shouldAnimate
+ animate = shouldAnimate,
)
}
@@ -531,7 +528,7 @@
Int.MAX_VALUE
} else {
view.getTop()
- }
+ },
)
}
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
index c6efcfa..4cf3c4e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
@@ -25,20 +25,18 @@
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
-data class TransitionData(
- val config: Config,
- val start: Long = System.currentTimeMillis(),
-)
+data class TransitionData(val config: Config, val start: Long = System.currentTimeMillis())
class KeyguardBlueprintViewModel
@Inject
constructor(
@Main private val handler: Handler,
- keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
+ private val keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
) {
val blueprint = keyguardBlueprintInteractor.blueprint
val blueprintId = keyguardBlueprintInteractor.blueprintId
@@ -76,6 +74,9 @@
}
}
+ fun refreshBlueprint(type: Type = Type.NoTransition) =
+ keyguardBlueprintInteractor.refreshBlueprint(type)
+
fun updateTransitions(data: TransitionData?, mutate: MutableSet<Transition>.() -> Unit) {
runningTransitions.mutate()
@@ -95,7 +96,7 @@
Log.w(
TAG,
"runTransition: skipping ${transition::class.simpleName}: " +
- "currentPriority=$currentPriority; config=$config"
+ "currentPriority=$currentPriority; config=$config",
)
}
apply()
@@ -106,7 +107,7 @@
Log.i(
TAG,
"runTransition: running ${transition::class.simpleName}: " +
- "currentPriority=$currentPriority; config=$config"
+ "currentPriority=$currentPriority; config=$config",
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index 73028c5..36f684e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -25,10 +25,10 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
-import com.android.systemui.keyguard.shared.ComposeLockscreen
import com.android.systemui.keyguard.shared.model.ClockSize
import com.android.systemui.keyguard.shared.model.ClockSizeSetting
import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
import com.android.systemui.statusbar.ui.SystemBarUtilsProxy
@@ -56,10 +56,9 @@
var burnInLayer: Layer? = null
val clockSize: StateFlow<ClockSize> =
- combine(
- keyguardClockInteractor.selectedClockSize,
- keyguardClockInteractor.clockSize,
- ) { selectedSize, clockSize ->
+ combine(keyguardClockInteractor.selectedClockSize, keyguardClockInteractor.clockSize) {
+ selectedSize,
+ clockSize ->
if (selectedSize == ClockSizeSetting.SMALL) ClockSize.SMALL else clockSize
}
.stateIn(
@@ -80,10 +79,7 @@
val currentClock = keyguardClockInteractor.currentClock
val hasCustomWeatherDataDisplay =
- combine(
- isLargeClockVisible,
- currentClock,
- ) { isLargeClock, currentClock ->
+ combine(isLargeClockVisible, currentClock) { isLargeClock, currentClock ->
currentClock?.let { clock ->
val face = if (isLargeClock) clock.largeClock else clock.smallClock
face.config.hasCustomWeatherDataDisplay
@@ -93,14 +89,14 @@
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue =
- currentClock.value?.largeClock?.config?.hasCustomWeatherDataDisplay ?: false
+ currentClock.value?.largeClock?.config?.hasCustomWeatherDataDisplay ?: false,
)
val clockShouldBeCentered: StateFlow<Boolean> =
keyguardClockInteractor.clockShouldBeCentered.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
- initialValue = false
+ initialValue = false,
)
// To translate elements below smartspace in weather clock to avoid overlapping between date
@@ -111,7 +107,7 @@
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
- initialValue = false
+ initialValue = false,
)
val currentClockLayout: StateFlow<ClockLayout> =
@@ -145,7 +141,7 @@
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
- initialValue = ClockLayout.SMALL_CLOCK
+ initialValue = ClockLayout.SMALL_CLOCK,
)
val hasCustomPositionUpdatedAnimation: StateFlow<Boolean> =
@@ -156,7 +152,7 @@
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
- initialValue = false
+ initialValue = false,
)
/** Calculates the top margin for the small clock. */
@@ -164,10 +160,10 @@
val statusBarHeight = systemBarUtils.getStatusBarHeaderHeightKeyguard()
return if (shadeInteractor.isShadeLayoutWide.value) {
resources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin) -
- if (ComposeLockscreen.isEnabled) statusBarHeight else 0
+ if (SceneContainerFlag.isEnabled) statusBarHeight else 0
} else {
resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) +
- if (!ComposeLockscreen.isEnabled) statusBarHeight else 0
+ if (!SceneContainerFlag.isEnabled) statusBarHeight else 0
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index ff9495d..2961d05 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -57,7 +57,7 @@
private static final float DEVICE_CONNECTED_ALPHA = 1f;
protected List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>();
- public MediaOutputAdapter(MediaOutputController controller) {
+ public MediaOutputAdapter(MediaSwitchingController controller) {
super(controller);
setHasStableIds(true);
}
@@ -531,8 +531,10 @@
@RequiresApi(34)
private static class Api34Impl {
@DoNotInline
- static View.OnClickListener getClickListenerBasedOnSelectionBehavior(MediaDevice device,
- MediaOutputController controller, View.OnClickListener defaultTransferListener) {
+ static View.OnClickListener getClickListenerBasedOnSelectionBehavior(
+ MediaDevice device,
+ MediaSwitchingController controller,
+ View.OnClickListener defaultTransferListener) {
switch (device.getSelectionBehavior()) {
case SELECTION_BEHAVIOR_NONE:
return null;
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 5958b0a..63a7e01 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -63,7 +63,7 @@
static final int CUSTOMIZED_ITEM_GROUP = 2;
static final int CUSTOMIZED_ITEM_DYNAMIC_GROUP = 3;
- protected final MediaOutputController mController;
+ protected final MediaSwitchingController mController;
private static final int UNMUTE_DEFAULT_VOLUME = 2;
@@ -73,7 +73,7 @@
int mCurrentActivePosition;
private boolean mIsInitVolumeFirstTime;
- public MediaOutputBaseAdapter(MediaOutputController controller) {
+ public MediaOutputBaseAdapter(MediaSwitchingController controller) {
mController = controller;
mIsDragging = false;
mCurrentActivePosition = -1;
@@ -127,7 +127,7 @@
return mCurrentActivePosition;
}
- public MediaOutputController getController() {
+ public MediaSwitchingController getController() {
return mController;
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 6cc4dcb..6bc995f3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -65,11 +65,9 @@
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
-/**
- * Base dialog for media output UI
- */
-public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
- MediaOutputController.Callback, Window.Callback {
+/** Base dialog for media output UI */
+public abstract class MediaOutputBaseDialog extends SystemUIDialog
+ implements MediaSwitchingController.Callback, Window.Callback {
private static final String TAG = "MediaOutputDialog";
private static final String EMPTY_TITLE = " ";
@@ -82,7 +80,7 @@
private final RecyclerView.LayoutManager mLayoutManager;
final Context mContext;
- final MediaOutputController mMediaOutputController;
+ final MediaSwitchingController mMediaSwitchingController;
final BroadcastSender mBroadcastSender;
/**
@@ -212,22 +210,22 @@
@Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
- mMediaOutputController.setRefreshing(false);
- mMediaOutputController.refreshDataSetIfNeeded();
+ mMediaSwitchingController.setRefreshing(false);
+ mMediaSwitchingController.refreshDataSetIfNeeded();
}
}
public MediaOutputBaseDialog(
Context context,
BroadcastSender broadcastSender,
- MediaOutputController mediaOutputController,
+ MediaSwitchingController mediaSwitchingController,
boolean includePlaybackAndAppMetadata) {
super(context, R.style.Theme_SystemUI_Dialog_Media);
// Save the context that is wrapped with our theme.
mContext = getContext();
mBroadcastSender = broadcastSender;
- mMediaOutputController = mediaOutputController;
+ mMediaSwitchingController = mediaSwitchingController;
mLayoutManager = new LayoutManagerWrapper(mContext);
mListMaxHeight = context.getResources().getDimensionPixelSize(
R.dimen.media_output_dialog_list_max_height);
@@ -279,9 +277,9 @@
// Init bottom buttons
mDoneButton.setOnClickListener(v -> dismiss());
mStopButton.setOnClickListener(v -> onStopButtonClick());
- mAppButton.setOnClickListener(mMediaOutputController::tryToLaunchMediaApplication);
+ mAppButton.setOnClickListener(mMediaSwitchingController::tryToLaunchMediaApplication);
mMediaMetadataSectionLayout.setOnClickListener(
- mMediaOutputController::tryToLaunchMediaApplication);
+ mMediaSwitchingController::tryToLaunchMediaApplication);
mDismissing = false;
}
@@ -298,10 +296,10 @@
@Override
public void start() {
- mMediaOutputController.start(this);
+ mMediaSwitchingController.start(this);
if (isBroadcastSupported() && !mIsLeBroadcastCallbackRegistered) {
- mMediaOutputController.registerLeBroadcastServiceCallback(mExecutor,
- mBroadcastCallback);
+ mMediaSwitchingController.registerLeBroadcastServiceCallback(
+ mExecutor, mBroadcastCallback);
mIsLeBroadcastCallbackRegistered = true;
}
}
@@ -311,11 +309,11 @@
// unregister broadcast callback should only depend on profile and registered flag
// rather than remote device or broadcast state
// otherwise it might have risks of leaking registered callback handle
- if (mMediaOutputController.isBroadcastSupported() && mIsLeBroadcastCallbackRegistered) {
- mMediaOutputController.unregisterLeBroadcastServiceCallback(mBroadcastCallback);
+ if (mMediaSwitchingController.isBroadcastSupported() && mIsLeBroadcastCallbackRegistered) {
+ mMediaSwitchingController.unregisterLeBroadcastServiceCallback(mBroadcastCallback);
mIsLeBroadcastCallbackRegistered = false;
}
- mMediaOutputController.stop();
+ mMediaSwitchingController.stop();
}
@VisibleForTesting
@@ -326,18 +324,17 @@
void refresh(boolean deviceSetChanged) {
// TODO(287191450): remove binder calls in this method from the UI thread.
// If the dialog is going away or is already refreshing, do nothing.
- if (mDismissing || mMediaOutputController.isRefreshing()) {
+ if (mDismissing || mMediaSwitchingController.isRefreshing()) {
return;
}
- mMediaOutputController.setRefreshing(true);
+ mMediaSwitchingController.setRefreshing(true);
// Update header icon
final int iconRes = getHeaderIconRes();
final IconCompat headerIcon = getHeaderIcon();
final IconCompat appSourceIcon = getAppSourceIcon();
boolean colorSetUpdated = false;
mCastAppLayout.setVisibility(
- mMediaOutputController.shouldShowLaunchSection()
- ? View.VISIBLE : View.GONE);
+ mMediaSwitchingController.shouldShowLaunchSection() ? View.VISIBLE : View.GONE);
if (iconRes != 0) {
mHeaderIcon.setVisibility(View.VISIBLE);
mHeaderIcon.setImageResource(iconRes);
@@ -371,10 +368,10 @@
mAppResourceIcon.setVisibility(View.GONE);
} else if (appSourceIcon != null) {
Icon appIcon = appSourceIcon.toIcon(mContext);
- mAppResourceIcon.setColorFilter(mMediaOutputController.getColorItemContent());
+ mAppResourceIcon.setColorFilter(mMediaSwitchingController.getColorItemContent());
mAppResourceIcon.setImageIcon(appIcon);
} else {
- Drawable appIconDrawable = mMediaOutputController.getAppSourceIconFromPackage();
+ Drawable appIconDrawable = mMediaSwitchingController.getAppSourceIconFromPackage();
if (appIconDrawable != null) {
mAppResourceIcon.setImageDrawable(appIconDrawable);
} else {
@@ -387,7 +384,7 @@
R.dimen.media_output_dialog_header_icon_padding);
mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size + padding, size));
}
- mAppButton.setText(mMediaOutputController.getAppSourceName());
+ mAppButton.setText(mMediaSwitchingController.getAppSourceName());
if (!mIncludePlaybackAndAppMetadata) {
mHeaderTitle.setVisibility(View.GONE);
@@ -424,23 +421,26 @@
mAdapter.updateItems();
}
} else {
- mMediaOutputController.setRefreshing(false);
- mMediaOutputController.refreshDataSetIfNeeded();
+ mMediaSwitchingController.setRefreshing(false);
+ mMediaSwitchingController.refreshDataSetIfNeeded();
}
}
private void updateButtonBackgroundColorFilter() {
- ColorFilter buttonColorFilter = new PorterDuffColorFilter(
- mMediaOutputController.getColorButtonBackground(),
- PorterDuff.Mode.SRC_IN);
+ ColorFilter buttonColorFilter =
+ new PorterDuffColorFilter(
+ mMediaSwitchingController.getColorButtonBackground(),
+ PorterDuff.Mode.SRC_IN);
mDoneButton.getBackground().setColorFilter(buttonColorFilter);
mStopButton.getBackground().setColorFilter(buttonColorFilter);
- mDoneButton.setTextColor(mMediaOutputController.getColorPositiveButtonText());
+ mDoneButton.setTextColor(mMediaSwitchingController.getColorPositiveButtonText());
}
private void updateDialogBackgroundColor() {
- getDialogView().getBackground().setTint(mMediaOutputController.getColorDialogBackground());
- mDeviceListLayout.setBackgroundColor(mMediaOutputController.getColorDialogBackground());
+ getDialogView()
+ .getBackground()
+ .setTint(mMediaSwitchingController.getColorDialogBackground());
+ mDeviceListLayout.setBackgroundColor(mMediaSwitchingController.getColorDialogBackground());
}
private Drawable resizeDrawable(Drawable drawable, int size) {
@@ -499,7 +499,7 @@
protected void startLeBroadcast() {
mStopButton.setText(R.string.media_output_broadcast_starting);
mStopButton.setEnabled(false);
- if (!mMediaOutputController.startBluetoothLeBroadcast()) {
+ if (!mMediaSwitchingController.startBluetoothLeBroadcast()) {
// If the system can't execute "broadcast start", then UI shows the error.
handleLeBroadcastStartFailed();
}
@@ -512,9 +512,10 @@
&& sharedPref.getBoolean(PREF_IS_LE_BROADCAST_FIRST_LAUNCH, true)) {
Log.d(TAG, "PREF_IS_LE_BROADCAST_FIRST_LAUNCH: true");
- mMediaOutputController.launchLeBroadcastNotifyDialog(mDialogView,
+ mMediaSwitchingController.launchLeBroadcastNotifyDialog(
+ mDialogView,
mBroadcastSender,
- MediaOutputController.BroadcastNotifyDialog.ACTION_FIRST_LAUNCH,
+ MediaSwitchingController.BroadcastNotifyDialog.ACTION_FIRST_LAUNCH,
(d, w) -> {
startLeBroadcast();
});
@@ -527,14 +528,13 @@
}
protected void startLeBroadcastDialog() {
- mMediaOutputController.launchMediaOutputBroadcastDialog(mDialogView,
- mBroadcastSender);
+ mMediaSwitchingController.launchMediaOutputBroadcastDialog(mDialogView, mBroadcastSender);
refresh();
}
protected void stopLeBroadcast() {
mStopButton.setEnabled(false);
- if (!mMediaOutputController.stopBluetoothLeBroadcast()) {
+ if (!mMediaSwitchingController.stopBluetoothLeBroadcast()) {
// If the system can't execute "broadcast stop", then UI does refresh.
mMainThreadHandler.post(() -> refresh());
}
@@ -559,7 +559,7 @@
}
public void onStopButtonClick() {
- mMediaOutputController.releaseSession();
+ mMediaSwitchingController.releaseSession();
dismiss();
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
index 1e31755..9b5b872a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
@@ -235,14 +235,17 @@
}
};
- MediaOutputBroadcastDialog(Context context, boolean aboveStatusbar,
- BroadcastSender broadcastSender, MediaOutputController mediaOutputController) {
+ MediaOutputBroadcastDialog(
+ Context context,
+ boolean aboveStatusbar,
+ BroadcastSender broadcastSender,
+ MediaSwitchingController mediaSwitchingController) {
super(
context,
broadcastSender,
- mediaOutputController, /* includePlaybackAndAppMetadata */
+ mediaSwitchingController, /* includePlaybackAndAppMetadata */
true);
- mAdapter = new MediaOutputAdapter(mMediaOutputController);
+ mAdapter = new MediaOutputAdapter(mMediaSwitchingController);
// TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class
// that extends MediaOutputBaseDialog
if (!aboveStatusbar) {
@@ -262,8 +265,8 @@
super.start();
if (!mIsLeBroadcastAssistantCallbackRegistered) {
mIsLeBroadcastAssistantCallbackRegistered = true;
- mMediaOutputController.registerLeBroadcastAssistantServiceCallback(mExecutor,
- mBroadcastAssistantCallback);
+ mMediaSwitchingController.registerLeBroadcastAssistantServiceCallback(
+ mExecutor, mBroadcastAssistantCallback);
}
/* Add local source broadcast to connected capable devices that may be possible receivers
* of stream.
@@ -276,7 +279,7 @@
super.stop();
if (mIsLeBroadcastAssistantCallbackRegistered) {
mIsLeBroadcastAssistantCallbackRegistered = false;
- mMediaOutputController.unregisterLeBroadcastAssistantServiceCallback(
+ mMediaSwitchingController.unregisterLeBroadcastAssistantServiceCallback(
mBroadcastAssistantCallback);
}
}
@@ -288,7 +291,7 @@
@Override
IconCompat getHeaderIcon() {
- return mMediaOutputController.getHeaderIcon();
+ return mMediaSwitchingController.getHeaderIcon();
}
@Override
@@ -299,17 +302,17 @@
@Override
CharSequence getHeaderText() {
- return mMediaOutputController.getHeaderTitle();
+ return mMediaSwitchingController.getHeaderTitle();
}
@Override
CharSequence getHeaderSubtitle() {
- return mMediaOutputController.getHeaderSubTitle();
+ return mMediaSwitchingController.getHeaderSubTitle();
}
@Override
IconCompat getAppSourceIcon() {
- return mMediaOutputController.getNotificationSmallIcon();
+ return mMediaSwitchingController.getNotificationSmallIcon();
}
@Override
@@ -319,16 +322,16 @@
@Override
public void onStopButtonClick() {
- mMediaOutputController.stopBluetoothLeBroadcast();
+ mMediaSwitchingController.stopBluetoothLeBroadcast();
dismiss();
}
private String getBroadcastMetadataInfo(int metadata) {
switch (metadata) {
case METADATA_BROADCAST_NAME:
- return mMediaOutputController.getBroadcastName();
+ return mMediaSwitchingController.getBroadcastName();
case METADATA_BROADCAST_CODE:
- return mMediaOutputController.getBroadcastCode();
+ return mMediaSwitchingController.getBroadcastCode();
default:
return "";
}
@@ -342,13 +345,15 @@
mBroadcastQrCodeView = getDialogView().requireViewById(R.id.qrcode_view);
mBroadcastNotify = getDialogView().requireViewById(R.id.broadcast_info);
- mBroadcastNotify.setOnClickListener(v -> {
- mMediaOutputController.launchLeBroadcastNotifyDialog(
- /* view= */ null,
- /* broadcastSender= */ null,
- MediaOutputController.BroadcastNotifyDialog.ACTION_BROADCAST_INFO_ICON,
- /* onClickListener= */ null);
- });
+ mBroadcastNotify.setOnClickListener(
+ v -> {
+ mMediaSwitchingController.launchLeBroadcastNotifyDialog(
+ /* mediaOutputDialog= */ null,
+ /* broadcastSender= */ null,
+ MediaSwitchingController.BroadcastNotifyDialog
+ .ACTION_BROADCAST_INFO_ICON,
+ /* listener= */ null);
+ });
mBroadcastName = getDialogView().requireViewById(R.id.broadcast_name_summary);
mBroadcastNameEdit = getDialogView().requireViewById(R.id.broadcast_name_edit);
mBroadcastNameEdit.setOnClickListener(v -> {
@@ -409,16 +414,16 @@
return;
}
- for (BluetoothDevice sink : mMediaOutputController.getConnectedBroadcastSinkDevices()) {
+ for (BluetoothDevice sink : mMediaSwitchingController.getConnectedBroadcastSinkDevices()) {
Log.d(TAG, "The broadcastMetadata broadcastId: " + broadcastMetadata.getBroadcastId()
+ ", the device: " + sink.getAnonymizedAddress());
- if (mMediaOutputController.isThereAnyBroadcastSourceIntoSinkDevice(sink)) {
+ if (mMediaSwitchingController.isThereAnyBroadcastSourceIntoSinkDevice(sink)) {
Log.d(TAG, "The sink device has the broadcast source now.");
return;
}
- if (!mMediaOutputController.addSourceIntoSinkDeviceWithBluetoothLeAssistant(sink,
- broadcastMetadata, /*isGroupOp=*/ false)) {
+ if (!mMediaSwitchingController.addSourceIntoSinkDeviceWithBluetoothLeAssistant(
+ sink, broadcastMetadata, /* isGroupOp= */ false)) {
Log.e(TAG, "Error: Source add failed");
}
}
@@ -457,11 +462,11 @@
}
private String getLocalBroadcastMetadataQrCodeString() {
- return mMediaOutputController.getLocalBroadcastMetadataQrCodeString();
+ return mMediaSwitchingController.getLocalBroadcastMetadataQrCodeString();
}
private BluetoothLeBroadcastMetadata getBroadcastMetadata() {
- return mMediaOutputController.getBroadcastMetadata();
+ return mMediaSwitchingController.getBroadcastMetadata();
}
@VisibleForTesting
@@ -476,8 +481,8 @@
* stopped then used the new Broadcast code to start the Broadcast.
*/
mIsStopbyUpdateBroadcastCode = true;
- mMediaOutputController.setBroadcastCode(updatedString);
- if (!mMediaOutputController.stopBluetoothLeBroadcast()) {
+ mMediaSwitchingController.setBroadcastCode(updatedString);
+ if (!mMediaSwitchingController.stopBluetoothLeBroadcast()) {
handleLeBroadcastStopFailed();
return;
}
@@ -485,8 +490,8 @@
/* If the user wants to update the Broadcast Name, we don't need to stop the Broadcast
* session. Only use the new Broadcast name to update the broadcast session.
*/
- mMediaOutputController.setBroadcastName(updatedString);
- if (!mMediaOutputController.updateBluetoothLeBroadcast()) {
+ mMediaSwitchingController.setBroadcastName(updatedString);
+ if (!mMediaSwitchingController.updateBluetoothLeBroadcast()) {
handleLeBroadcastUpdateFailed();
}
}
@@ -496,12 +501,13 @@
public boolean isBroadcastSupported() {
if (!legacyLeAudioSharing()) return false;
boolean isBluetoothLeDevice = false;
- if (mMediaOutputController.getCurrentConnectedMediaDevice() != null) {
- isBluetoothLeDevice = mMediaOutputController.isBluetoothLeDevice(
- mMediaOutputController.getCurrentConnectedMediaDevice());
+ if (mMediaSwitchingController.getCurrentConnectedMediaDevice() != null) {
+ isBluetoothLeDevice =
+ mMediaSwitchingController.isBluetoothLeDevice(
+ mMediaSwitchingController.getCurrentConnectedMediaDevice());
}
- return mMediaOutputController.isBroadcastSupported() && isBluetoothLeDevice;
+ return mMediaSwitchingController.isBroadcastSupported() && isBluetoothLeDevice;
}
@Override
@@ -515,7 +521,7 @@
@Override
public void handleLeBroadcastStartFailed() {
- mMediaOutputController.setBroadcastCode(mCurrentBroadcastCode);
+ mMediaSwitchingController.setBroadcastCode(mCurrentBroadcastCode);
mRetryCount++;
handleUpdateFailedUi();
@@ -538,8 +544,8 @@
@Override
public void handleLeBroadcastUpdateFailed() {
- //Change the value in shared preferences back to it original value
- mMediaOutputController.setBroadcastName(mCurrentBroadcastName);
+ // Change the value in shared preferences back to it original value
+ mMediaSwitchingController.setBroadcastName(mCurrentBroadcastName);
mRetryCount++;
handleUpdateFailedUi();
@@ -550,7 +556,7 @@
if (mIsStopbyUpdateBroadcastCode) {
mIsStopbyUpdateBroadcastCode = false;
mRetryCount = 0;
- if (!mMediaOutputController.startBluetoothLeBroadcast()) {
+ if (!mMediaSwitchingController.startBluetoothLeBroadcast()) {
handleLeBroadcastStartFailed();
return;
}
@@ -561,8 +567,8 @@
@Override
public void handleLeBroadcastStopFailed() {
- //Change the value in shared preferences back to it original value
- mMediaOutputController.setBroadcastCode(mCurrentBroadcastCode);
+ // Change the value in shared preferences back to it original value
+ mMediaSwitchingController.setBroadcastCode(mCurrentBroadcastCode);
mRetryCount++;
handleUpdateFailedUi();
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
index 6ef9ea3..2e7e66f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
@@ -29,7 +29,7 @@
private val context: Context,
private val broadcastSender: BroadcastSender,
private val dialogTransitionAnimator: DialogTransitionAnimator,
- private val mediaOutputControllerFactory: MediaOutputController.Factory
+ private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory
) {
var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null
@@ -41,7 +41,7 @@
// TODO: b/321969740 - Populate the userHandle parameter. The user handle is necessary to
// disambiguate the same package running on different users.
val controller =
- mediaOutputControllerFactory.create(
+ mediaSwitchingControllerFactory.create(
packageName,
/* userHandle= */ null,
/* token */ null,
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index eb6a320..c9af7b3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -46,14 +46,14 @@
Context context,
boolean aboveStatusbar,
BroadcastSender broadcastSender,
- MediaOutputController mediaOutputController,
+ MediaSwitchingController mediaSwitchingController,
DialogTransitionAnimator dialogTransitionAnimator,
UiEventLogger uiEventLogger,
boolean includePlaybackAndAppMetadata) {
- super(context, broadcastSender, mediaOutputController, includePlaybackAndAppMetadata);
+ super(context, broadcastSender, mediaSwitchingController, includePlaybackAndAppMetadata);
mDialogTransitionAnimator = dialogTransitionAnimator;
mUiEventLogger = uiEventLogger;
- mAdapter = new MediaOutputAdapter(mMediaOutputController);
+ mAdapter = new MediaOutputAdapter(mMediaSwitchingController);
if (!aboveStatusbar) {
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
}
@@ -72,7 +72,7 @@
@Override
IconCompat getHeaderIcon() {
- return mMediaOutputController.getHeaderIcon();
+ return mMediaSwitchingController.getHeaderIcon();
}
@Override
@@ -83,27 +83,29 @@
@Override
CharSequence getHeaderText() {
- return mMediaOutputController.getHeaderTitle();
+ return mMediaSwitchingController.getHeaderTitle();
}
@Override
CharSequence getHeaderSubtitle() {
- return mMediaOutputController.getHeaderSubTitle();
+ return mMediaSwitchingController.getHeaderSubTitle();
}
@Override
IconCompat getAppSourceIcon() {
- return mMediaOutputController.getNotificationSmallIcon();
+ return mMediaSwitchingController.getNotificationSmallIcon();
}
@Override
int getStopButtonVisibility() {
boolean isActiveRemoteDevice = false;
- if (mMediaOutputController.getCurrentConnectedMediaDevice() != null) {
- isActiveRemoteDevice = mMediaOutputController.isActiveRemoteDevice(
- mMediaOutputController.getCurrentConnectedMediaDevice());
+ if (mMediaSwitchingController.getCurrentConnectedMediaDevice() != null) {
+ isActiveRemoteDevice =
+ mMediaSwitchingController.isActiveRemoteDevice(
+ mMediaSwitchingController.getCurrentConnectedMediaDevice());
}
- boolean showBroadcastButton = isBroadcastSupported() && mMediaOutputController.isPlaying();
+ boolean showBroadcastButton =
+ isBroadcastSupported() && mMediaSwitchingController.isPlaying();
return (isActiveRemoteDevice || showBroadcastButton) ? View.VISIBLE : View.GONE;
}
@@ -115,13 +117,14 @@
boolean isBroadcastEnabled = false;
if (FeatureFlagUtils.isEnabled(mContext,
FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST)) {
- if (mMediaOutputController.getCurrentConnectedMediaDevice() != null) {
- isBluetoothLeDevice = mMediaOutputController.isBluetoothLeDevice(
- mMediaOutputController.getCurrentConnectedMediaDevice());
+ if (mMediaSwitchingController.getCurrentConnectedMediaDevice() != null) {
+ isBluetoothLeDevice =
+ mMediaSwitchingController.isBluetoothLeDevice(
+ mMediaSwitchingController.getCurrentConnectedMediaDevice());
// if broadcast is active, broadcast should be considered as supported
// there could be a valid case that broadcast is ongoing
// without active LEA device connected
- isBroadcastEnabled = mMediaOutputController.isBluetoothLeBroadcastEnabled();
+ isBroadcastEnabled = mMediaSwitchingController.isBluetoothLeBroadcastEnabled();
}
} else {
// To decouple LE Audio Broadcast and Unicast, it always displays the button when there
@@ -129,15 +132,16 @@
isBluetoothLeDevice = true;
}
- return mMediaOutputController.isBroadcastSupported()
+ return mMediaSwitchingController.isBroadcastSupported()
&& (isBluetoothLeDevice || isBroadcastEnabled);
}
@Override
public CharSequence getStopButtonText() {
int resId = R.string.media_output_dialog_button_stop_casting;
- if (isBroadcastSupported() && mMediaOutputController.isPlaying()
- && !mMediaOutputController.isBluetoothLeBroadcastEnabled()) {
+ if (isBroadcastSupported()
+ && mMediaSwitchingController.isPlaying()
+ && !mMediaSwitchingController.isBluetoothLeBroadcastEnabled()) {
resId = R.string.media_output_broadcast;
}
return mContext.getText(resId);
@@ -145,8 +149,8 @@
@Override
public void onStopButtonClick() {
- if (isBroadcastSupported() && mMediaOutputController.isPlaying()) {
- if (!mMediaOutputController.isBluetoothLeBroadcastEnabled()) {
+ if (isBroadcastSupported() && mMediaSwitchingController.isPlaying()) {
+ if (!mMediaSwitchingController.isBluetoothLeBroadcastEnabled()) {
if (startLeBroadcastDialogForFirstTime()) {
return;
}
@@ -155,7 +159,7 @@
stopLeBroadcast();
}
} else {
- mMediaOutputController.releaseSession();
+ mMediaSwitchingController.releaseSession();
mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations();
dismiss();
}
@@ -163,8 +167,9 @@
@Override
public int getBroadcastIconVisibility() {
- return (isBroadcastSupported() && mMediaOutputController.isBluetoothLeBroadcastEnabled())
- ? View.VISIBLE : View.GONE;
+ return (isBroadcastSupported() && mMediaSwitchingController.isBluetoothLeBroadcastEnabled())
+ ? View.VISIBLE
+ : View.GONE;
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
index 47e0691..4e9451a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
@@ -35,7 +35,7 @@
private val broadcastSender: BroadcastSender,
private val uiEventLogger: UiEventLogger,
private val dialogTransitionAnimator: DialogTransitionAnimator,
- private val mediaOutputControllerFactory: MediaOutputController.Factory,
+ private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory,
) {
companion object {
const val INTERACTION_JANK_TAG = "media_output"
@@ -118,7 +118,7 @@
// Dismiss the previous dialog, if any.
mediaOutputDialog?.dismiss()
- val controller = mediaOutputControllerFactory.create(packageName, userHandle, token)
+ val controller = mediaSwitchingControllerFactory.create(packageName, userHandle, token)
val mediaOutputDialog =
MediaOutputDialog(
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
similarity index 92%
rename from packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
rename to packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 875e505..2cbc7575 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -77,6 +77,7 @@
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastMetadata;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.media.InfoMediaManager;
+import com.android.settingslib.media.InputRouteManager;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.media.flags.Flags;
@@ -116,12 +117,13 @@
import java.util.stream.Collectors;
/**
- * Controller for media output dialog
+ * Controller for a dialog that allows users to switch media output and input devices, control
+ * volume, connect to new devices, etc.
*/
-public class MediaOutputController implements LocalMediaManager.DeviceCallback,
- INearbyMediaDevicesUpdateCallback {
+public class MediaSwitchingController
+ implements LocalMediaManager.DeviceCallback, INearbyMediaDevicesUpdateCallback {
- private static final String TAG = "MediaOutputController";
+ private static final String TAG = "MediaSwitchingController";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final String PAGE_CONNECTED_DEVICES_KEY =
"top_level_connected_devices";
@@ -137,10 +139,12 @@
private final DialogTransitionAnimator mDialogTransitionAnimator;
private final CommonNotifCollection mNotifCollection;
protected final Object mMediaDevicesLock = new Object();
+ protected final Object mInputMediaDevicesLock = new Object();
@VisibleForTesting
final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>();
- private final List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>();
+ private final List<MediaItem> mOutputMediaItemList = new CopyOnWriteArrayList<>();
+ private final List<MediaItem> mInputMediaItemList = new CopyOnWriteArrayList<>();
private final AudioManager mAudioManager;
private final PowerExemptionManager mPowerExemptionManager;
private final KeyguardManager mKeyGuardManager;
@@ -153,6 +157,7 @@
@VisibleForTesting
boolean mNeedRefresh = false;
private MediaController mMediaController;
+ private InputRouteManager mInputRouteManager;
@VisibleForTesting
Callback mCallback;
@VisibleForTesting
@@ -181,8 +186,20 @@
ACTION_BROADCAST_INFO_ICON
}
+ @VisibleForTesting
+ final InputRouteManager.InputDeviceCallback mInputDeviceCallback =
+ new InputRouteManager.InputDeviceCallback() {
+ @Override
+ public void onInputDeviceListUpdated(@NonNull List<MediaDevice> devices) {
+ synchronized (mInputMediaDevicesLock) {
+ buildInputMediaItems(devices);
+ mCallback.onDeviceListChanged();
+ }
+ }
+ };
+
@AssistedInject
- public MediaOutputController(
+ public MediaSwitchingController(
Context context,
@Assisted String packageName,
@Assisted @Nullable UserHandle userHandle,
@@ -241,19 +258,23 @@
R.dimen.media_output_dialog_default_margin_end);
mItemMarginEndSelectable = (int) mContext.getResources().getDimension(
R.dimen.media_output_dialog_selectable_margin_end);
+
+ if (enableInputRouting()) {
+ mInputRouteManager = new InputRouteManager(mContext, audioManager);
+ }
}
@AssistedFactory
public interface Factory {
- /** Construct a MediaOutputController */
- MediaOutputController create(
+ /** Construct a MediaSwitchingController */
+ MediaSwitchingController create(
String packageName, UserHandle userHandle, MediaSession.Token token);
}
protected void start(@NonNull Callback cb) {
synchronized (mMediaDevicesLock) {
mCachedMediaDevices.clear();
- mMediaItemList.clear();
+ mOutputMediaItemList.clear();
}
mNearbyDeviceInfoMap.clear();
if (mNearbyMediaDevicesManager != null) {
@@ -277,6 +298,10 @@
mCallback = cb;
mLocalMediaManager.registerCallback(this);
mLocalMediaManager.startScan();
+
+ if (enableInputRouting()) {
+ mInputRouteManager.registerCallback(mInputDeviceCallback);
+ }
}
boolean shouldShowLaunchSection() {
@@ -300,12 +325,19 @@
mLocalMediaManager.stopScan();
synchronized (mMediaDevicesLock) {
mCachedMediaDevices.clear();
- mMediaItemList.clear();
+ mOutputMediaItemList.clear();
}
if (mNearbyMediaDevicesManager != null) {
mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this);
}
mNearbyDeviceInfoMap.clear();
+
+ if (enableInputRouting()) {
+ mInputRouteManager.unregisterCallback(mInputDeviceCallback);
+ synchronized (mInputMediaDevicesLock) {
+ mInputMediaItemList.clear();
+ }
+ }
}
private MediaController getMediaController() {
@@ -335,7 +367,7 @@
@Override
public void onDeviceListUpdate(List<MediaDevice> devices) {
- boolean isListEmpty = mMediaItemList.isEmpty();
+ boolean isListEmpty = mOutputMediaItemList.isEmpty();
if (isListEmpty || !mIsRefreshing) {
buildMediaItems(devices);
mCallback.onDeviceListChanged();
@@ -352,7 +384,8 @@
public void onSelectedDeviceStateChanged(MediaDevice device,
@LocalMediaManager.MediaDeviceState int state) {
mCallback.onRouteChanged();
- mMetricLogger.logOutputItemSuccess(device.toString(), new ArrayList<>(mMediaItemList));
+ mMetricLogger.logOutputItemSuccess(
+ device.toString(), new ArrayList<>(mOutputMediaItemList));
}
@Override
@@ -363,7 +396,7 @@
@Override
public void onRequestFailed(int reason) {
mCallback.onRouteChanged();
- mMetricLogger.logOutputItemFailure(new ArrayList<>(mMediaItemList), reason);
+ mMetricLogger.logOutputItemFailure(new ArrayList<>(mOutputMediaItemList), reason);
}
/**
@@ -382,7 +415,7 @@
}
try {
synchronized (mMediaDevicesLock) {
- mMediaItemList.removeIf((MediaItem::isMutingExpectedDevice));
+ mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice));
}
mAudioManager.cancelMuteAwaitConnection(mAudioManager.getMutingExpectedDevice());
} catch (Exception e) {
@@ -638,9 +671,9 @@
private void buildMediaItems(List<MediaDevice> devices) {
synchronized (mMediaDevicesLock) {
- List<MediaItem> updatedMediaItems = buildMediaItems(mMediaItemList, devices);
- mMediaItemList.clear();
- mMediaItemList.addAll(updatedMediaItems);
+ List<MediaItem> updatedMediaItems = buildMediaItems(mOutputMediaItemList, devices);
+ mOutputMediaItemList.clear();
+ mOutputMediaItemList.addAll(updatedMediaItems);
}
}
@@ -714,6 +747,19 @@
}
}
+ private boolean enableInputRouting() {
+ return com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl();
+ }
+
+ private void buildInputMediaItems(List<MediaDevice> devices) {
+ synchronized (mInputMediaDevicesLock) {
+ List<MediaItem> updatedInputMediaItems =
+ devices.stream().map(MediaItem::createDeviceMediaItem).toList();
+ mInputMediaItemList.clear();
+ mInputMediaItemList.addAll(updatedInputMediaItems);
+ }
+ }
+
/**
* Initial categorization of current devices, will not be called for updates to the devices
* list.
@@ -778,7 +824,6 @@
mediaDevice.setRangeZone(mNearbyDeviceInfoMap.get(mediaDevice.getId()));
}
}
-
}
boolean isCurrentConnectedDeviceRemote() {
@@ -837,8 +882,31 @@
});
}
+ private void addInputDevices(List<MediaItem> mediaItems) {
+ mediaItems.add(
+ MediaItem.createGroupDividerMediaItem(
+ mContext.getString(R.string.media_input_group_title)));
+ mediaItems.addAll(mInputMediaItemList);
+ }
+
+ private void addOutputDevices(List<MediaItem> mediaItems) {
+ mediaItems.add(
+ MediaItem.createGroupDividerMediaItem(
+ mContext.getString(R.string.media_output_group_title)));
+ mediaItems.addAll(mOutputMediaItemList);
+ }
+
public List<MediaItem> getMediaItemList() {
- return mMediaItemList;
+ // If input routing is not enabled, only return output media items.
+ if (!enableInputRouting()) {
+ return mOutputMediaItemList;
+ }
+
+ // If input routing is enabled, return both output and input media items.
+ List<MediaItem> mediaItems = new ArrayList<>();
+ addOutputDevices(mediaItems);
+ addInputDevices(mediaItems);
+ return mediaItems;
}
public MediaDevice getCurrentConnectedMediaDevice() {
@@ -921,7 +989,7 @@
public boolean isAnyDeviceTransferring() {
synchronized (mMediaDevicesLock) {
- for (MediaItem mediaItem : mMediaItemList) {
+ for (MediaItem mediaItem : mOutputMediaItemList) {
if (mediaItem.getMediaDevice().isPresent()
&& mediaItem.getMediaDevice().get().getState()
== LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
@@ -986,8 +1054,8 @@
}
void launchMediaOutputBroadcastDialog(View mediaOutputDialog, BroadcastSender broadcastSender) {
- MediaOutputController controller =
- new MediaOutputController(
+ MediaSwitchingController controller =
+ new MediaSwitchingController(
mContext,
mPackageName,
mUserHandle,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
index 072d322..1fe54e4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
@@ -23,17 +23,14 @@
import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepositoryImpl
import com.android.systemui.qs.panels.data.repository.GridLayoutTypeRepository
import com.android.systemui.qs.panels.data.repository.GridLayoutTypeRepositoryImpl
-import com.android.systemui.qs.panels.domain.interactor.GridTypeConsistencyInteractor
-import com.android.systemui.qs.panels.domain.interactor.InfiniteGridConsistencyInteractor
-import com.android.systemui.qs.panels.domain.interactor.NoopGridConsistencyInteractor
import com.android.systemui.qs.panels.shared.model.GridLayoutType
import com.android.systemui.qs.panels.shared.model.InfiniteGridLayoutType
import com.android.systemui.qs.panels.shared.model.PaginatedGridLayoutType
import com.android.systemui.qs.panels.shared.model.PanelsLog
import com.android.systemui.qs.panels.ui.compose.GridLayout
-import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout
import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout
import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModelImpl
import com.android.systemui.qs.panels.ui.viewmodel.IconLabelVisibilityViewModel
@@ -56,11 +53,6 @@
@Binds
fun bindGridLayoutTypeRepository(impl: GridLayoutTypeRepositoryImpl): GridLayoutTypeRepository
- @Binds
- fun bindDefaultGridConsistencyInteractor(
- impl: NoopGridConsistencyInteractor
- ): GridTypeConsistencyInteractor
-
@Binds fun bindIconTilesViewModel(impl: IconTilesViewModelImpl): IconTilesViewModel
@Binds fun bindGridSizeViewModel(impl: FixedColumnsSizeViewModelImpl): FixedColumnsSizeViewModel
@@ -74,12 +66,6 @@
@PaginatedBaseLayoutType
fun bindPaginatedBaseGridLayout(impl: InfiniteGridLayout): PaginatableGridLayout
- @Binds
- @PaginatedBaseLayoutType
- fun bindPaginatedBaseConsistencyInteractor(
- impl: NoopGridConsistencyInteractor
- ): GridTypeConsistencyInteractor
-
@Binds @Named("Default") fun bindDefaultGridLayout(impl: PaginatedGridLayout): GridLayout
companion object {
@@ -117,28 +103,5 @@
): Set<GridLayoutType> {
return entries.map { it.first }.toSet()
}
-
- @Provides
- @IntoSet
- fun provideGridConsistencyInteractor(
- consistencyInteractor: InfiniteGridConsistencyInteractor
- ): Pair<GridLayoutType, GridTypeConsistencyInteractor> {
- return Pair(InfiniteGridLayoutType, consistencyInteractor)
- }
-
- @Provides
- @IntoSet
- fun providePaginatedGridConsistencyInteractor(
- @PaginatedBaseLayoutType consistencyInteractor: GridTypeConsistencyInteractor,
- ): Pair<GridLayoutType, GridTypeConsistencyInteractor> {
- return Pair(PaginatedGridLayoutType, consistencyInteractor)
- }
-
- @Provides
- fun provideGridConsistencyInteractorMap(
- entries: Set<@JvmSuppressWildcards Pair<GridLayoutType, GridTypeConsistencyInteractor>>
- ): Map<GridLayoutType, GridTypeConsistencyInteractor> {
- return entries.toMap()
- }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractor.kt
deleted file mode 100644
index a2e7ea6..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractor.kt
+++ /dev/null
@@ -1,75 +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.systemui.qs.panels.domain.interactor
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.core.LogLevel
-import com.android.systemui.qs.panels.shared.model.GridLayoutType
-import com.android.systemui.qs.panels.shared.model.PanelsLog
-import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
-
-@SysUISingleton
-class GridConsistencyInteractor
-@Inject
-constructor(
- private val gridLayoutTypeInteractor: GridLayoutTypeInteractor,
- private val currentTilesInteractor: CurrentTilesInteractor,
- private val consistencyInteractors:
- Map<GridLayoutType, @JvmSuppressWildcards GridTypeConsistencyInteractor>,
- private val defaultConsistencyInteractor: GridTypeConsistencyInteractor,
- @PanelsLog private val logBuffer: LogBuffer,
- @Application private val applicationScope: CoroutineScope,
-) {
- fun start() {
- applicationScope.launch {
- gridLayoutTypeInteractor.layout.collectLatest { type ->
- val consistencyInteractor =
- consistencyInteractors[type] ?: defaultConsistencyInteractor
- currentTilesInteractor.currentTiles
- .map { tiles -> tiles.map { it.spec } }
- .collectLatest { tiles ->
- val newTiles = consistencyInteractor.reconcileTiles(tiles)
- if (newTiles != tiles) {
- currentTilesInteractor.setTiles(newTiles)
- logChange(newTiles)
- }
- }
- }
- }
- }
-
- private fun logChange(tiles: List<TileSpec>) {
- logBuffer.log(
- LOG_BUFFER_CURRENT_TILES_CHANGE_TAG,
- LogLevel.DEBUG,
- { str1 = tiles.toString() },
- { "Tiles reordered: $str1" }
- )
- }
-
- private companion object {
- const val LOG_BUFFER_CURRENT_TILES_CHANGE_TAG = "GridConsistencyTilesChange"
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridTypeConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridTypeConsistencyInteractor.kt
deleted file mode 100644
index 4cdabae..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridTypeConsistencyInteractor.kt
+++ /dev/null
@@ -1,27 +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.systemui.qs.panels.domain.interactor
-
-import com.android.systemui.qs.pipeline.shared.TileSpec
-
-interface GridTypeConsistencyInteractor {
- /**
- * Given a list of tiles, return the best list of the same tiles (preserving as much order as
- * possible, such that it's consistent with the current layout.
- */
- fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec>
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
deleted file mode 100644
index 874b3b0..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
+++ /dev/null
@@ -1,103 +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.systemui.qs.panels.domain.interactor
-
-import android.util.Log
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.panels.shared.model.SizedTile
-import com.android.systemui.qs.panels.shared.model.SizedTileImpl
-import com.android.systemui.qs.panels.shared.model.TileRow
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import javax.inject.Inject
-
-@SysUISingleton
-class InfiniteGridConsistencyInteractor
-@Inject
-constructor(
- private val iconTilesInteractor: IconTilesInteractor,
- private val gridSizeInteractor: FixedColumnsSizeInteractor
-) : GridTypeConsistencyInteractor {
-
- /**
- * Tries to fill in every columns of all rows (except the last row), potentially reordering
- * tiles.
- */
- override fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec> {
- val newTiles: MutableList<TileSpec> = mutableListOf()
- val row = TileRow<TileSpec>(columns = gridSizeInteractor.columns.value)
- val tilesQueue: ArrayDeque<SizedTile<TileSpec>> =
- ArrayDeque(
- tiles.map {
- SizedTileImpl(
- it,
- if (iconTilesInteractor.isIconTile(it)) 1 else 2,
- )
- }
- )
-
- while (tilesQueue.isNotEmpty()) {
- if (row.isFull()) {
- newTiles.addAll(row.tiles.map { it.tile })
- row.clear()
- }
-
- val tile = tilesQueue.removeFirst()
-
- // If the tile fits in the row, add it.
- if (!row.maybeAddTile(tile)) {
- // If the tile does not fit the row, find an icon tile to move.
- // We'll try to either add an icon tile from the queue to complete the row, or
- // remove an icon tile from the current row to free up space.
-
- val iconTile: SizedTile<TileSpec>? = tilesQueue.firstOrNull { it.width == 1 }
- if (iconTile != null) {
- tilesQueue.remove(iconTile)
- tilesQueue.addFirst(tile)
- row.maybeAddTile(iconTile)
- } else {
- val tileToRemove: SizedTile<TileSpec>? = row.findLastIconTile()
- if (tileToRemove != null) {
- row.removeTile(tileToRemove)
- row.maybeAddTile(tile)
-
- // Moving the icon tile to the end because there's no other
- // icon tiles in the queue.
- tilesQueue.addLast(tileToRemove)
- } else {
- // If the row does not have an icon tile, add the incomplete row.
- // Note: this shouldn't happen because an icon tile is guaranteed to be in a
- // row that doesn't have enough space for a large tile.
- val tileSpecs = row.tiles.map { it.tile }
- Log.wtf(TAG, "Uneven row does not have an icon tile to remove: $tileSpecs")
- newTiles.addAll(tileSpecs)
- row.clear()
- tilesQueue.addFirst(tile)
- }
- }
- }
- }
-
- // Add last row that might be incomplete
- newTiles.addAll(row.tiles.map { it.tile })
-
- return newTiles.toList()
- }
-
- private companion object {
- const val TAG = "InfiniteGridConsistencyInteractor"
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractor.kt
deleted file mode 100644
index 0386a6a..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractor.kt
+++ /dev/null
@@ -1,27 +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.systemui.qs.panels.domain.interactor
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import javax.inject.Inject
-
-/** [GridTypeConsistencyInteractor] implementation that doesn't do any changes to tiles. */
-@SysUISingleton
-class NoopGridConsistencyInteractor @Inject constructor() : GridTypeConsistencyInteractor {
- override fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec> = tiles
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
index 9a2315b..1f8a24a1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
@@ -161,9 +161,9 @@
@Composable
fun Modifier.dragAndDropTileSource(
sizedTile: SizedTile<EditTileViewModel>,
+ dragAndDropState: DragAndDropState,
onTap: (TileSpec) -> Unit,
- onDoubleTap: (TileSpec) -> Unit,
- dragAndDropState: DragAndDropState
+ onDoubleTap: (TileSpec) -> Unit = {},
): Modifier {
val state by rememberUpdatedState(dragAndDropState)
return dragAndDropSource {
@@ -181,11 +181,11 @@
ClipData(
QsDragAndDrop.CLIPDATA_LABEL,
arrayOf(QsDragAndDrop.TILESPEC_MIME_TYPE),
- ClipData.Item(sizedTile.tile.tileSpec.spec)
+ ClipData.Item(sizedTile.tile.tileSpec.spec),
)
)
)
- }
+ },
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
index fde40da..f4acbec 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
@@ -25,6 +25,8 @@
import androidx.compose.ui.util.fastMap
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.Tile
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.TileLazyGrid
import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel
@Composable
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
deleted file mode 100644
index afd47a7..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
+++ /dev/null
@@ -1,926 +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.
- */
-
-@file:OptIn(ExperimentalFoundationApi::class)
-
-package com.android.systemui.qs.panels.ui.compose
-
-import android.content.res.Resources
-import android.graphics.drawable.Animatable
-import android.service.quicksettings.Tile.STATE_ACTIVE
-import android.service.quicksettings.Tile.STATE_INACTIVE
-import android.text.TextUtils
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.animateDpAsState
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
-import androidx.compose.animation.graphics.res.animatedVectorResource
-import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
-import androidx.compose.animation.graphics.vector.AnimatedImageVector
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.LocalOverscrollConfiguration
-import androidx.compose.foundation.background
-import androidx.compose.foundation.basicMarquee
-import androidx.compose.foundation.border
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Arrangement.spacedBy
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.LazyGridItemScope
-import androidx.compose.foundation.lazy.grid.LazyGridScope
-import androidx.compose.foundation.lazy.grid.LazyGridState
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.rememberLazyGridState
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Clear
-import androidx.compose.material3.Icon
-import androidx.compose.material3.LocalContentColor
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.ReadOnlyComposable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.layout.positionInRoot
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.dimensionResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.clearAndSetSemantics
-import androidx.compose.ui.semantics.contentDescription
-import androidx.compose.ui.semantics.onClick
-import androidx.compose.ui.semantics.role
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.stateDescription
-import androidx.compose.ui.semantics.toggleableState
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.compose.ui.util.fastMap
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.compose.animation.Expandable
-import com.android.compose.modifiers.background
-import com.android.compose.modifiers.thenIf
-import com.android.systemui.animation.Expandable
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.common.ui.compose.Icon
-import com.android.systemui.common.ui.compose.load
-import com.android.systemui.compose.modifiers.sysuiResTag
-import com.android.systemui.plugins.qs.QSTile
-import com.android.systemui.qs.panels.shared.model.SizedTile
-import com.android.systemui.qs.panels.shared.model.SizedTileImpl
-import com.android.systemui.qs.panels.ui.compose.TileDefaults.longPressLabel
-import com.android.systemui.qs.panels.ui.model.GridCell
-import com.android.systemui.qs.panels.ui.model.SpacerGridCell
-import com.android.systemui.qs.panels.ui.model.TileGridCell
-import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState
-import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
-import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.toUiState
-import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import com.android.systemui.qs.shared.model.groupAndSort
-import com.android.systemui.qs.tileimpl.QSTileImpl
-import com.android.systemui.res.R
-import java.util.function.Supplier
-import kotlinx.coroutines.delay
-
-object TileType
-
-private const val TEST_TAG_SMALL = "qs_tile_small"
-private const val TEST_TAG_LARGE = "qs_tile_large"
-private const val TEST_TAG_TOGGLE = "qs_tile_toggle_target"
-
-@Composable
-fun Tile(tile: TileViewModel, iconOnly: Boolean, showLabels: Boolean = false, modifier: Modifier) {
- val state by tile.state.collectAsStateWithLifecycle(tile.currentState)
- val resources = resources()
- val uiState = remember(state, resources) { state.toUiState(resources) }
- val colors = TileDefaults.getColorForState(uiState)
-
- // TODO(b/361789146): Draw the shapes instead of clipping
- val tileShape = TileDefaults.animateTileShape(uiState.state)
-
- TileContainer(
- colors = colors,
- showLabels = showLabels,
- label = uiState.label,
- iconOnly = iconOnly,
- shape = tileShape,
- clickEnabled = true,
- onClick = tile::onClick,
- onLongClick = tile::onLongClick,
- modifier = modifier.height(tileHeight()),
- uiState = uiState,
- ) {
- val icon = getTileIcon(icon = uiState.icon)
- if (iconOnly) {
- TileIcon(icon = icon, color = colors.icon, modifier = Modifier.align(Alignment.Center))
- } else {
- val iconShape = TileDefaults.animateIconShape(uiState.state)
- LargeTileContent(
- label = uiState.label,
- secondaryLabel = uiState.secondaryLabel,
- icon = icon,
- colors = colors,
- iconShape = iconShape,
- toggleClickSupported = state.handlesSecondaryClick,
- onClick = {
- if (state.handlesSecondaryClick) {
- tile.onSecondaryClick()
- }
- },
- onLongClick = { tile.onLongClick(it) },
- accessibilityUiState = uiState.accessibilityUiState,
- )
- }
- }
-}
-
-@Composable
-private fun TileContainer(
- colors: TileColors,
- showLabels: Boolean,
- label: String,
- iconOnly: Boolean,
- shape: Shape,
- clickEnabled: Boolean = false,
- onClick: (Expandable) -> Unit = {},
- onLongClick: (Expandable) -> Unit = {},
- modifier: Modifier = Modifier,
- uiState: TileUiState? = null,
- content: @Composable BoxScope.(Expandable) -> Unit,
-) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement =
- spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin), Alignment.Top),
- modifier = modifier,
- ) {
- val backgroundColor =
- if (iconOnly || uiState?.handlesSecondaryClick != true) {
- colors.iconBackground
- } else {
- colors.background
- }
- Expandable(
- color = backgroundColor,
- shape = shape,
- modifier = Modifier.height(tileHeight()).clip(shape),
- ) {
- val longPressLabel = longPressLabel()
- Box(
- modifier =
- Modifier.fillMaxSize()
- .thenIf(clickEnabled) {
- Modifier.combinedClickable(
- onClick = { onClick(it) },
- onLongClick = { onLongClick(it) },
- onClickLabel = uiState?.accessibilityUiState?.clickLabel,
- onLongClickLabel = longPressLabel,
- )
- }
- .thenIf(uiState != null) {
- uiState as TileUiState
- Modifier.semantics {
- role = uiState.accessibilityUiState.accessibilityRole
- if (
- uiState.accessibilityUiState.accessibilityRole ==
- Role.Switch
- ) {
- uiState.accessibilityUiState.toggleableState?.let {
- toggleableState = it
- }
- }
- stateDescription = uiState.accessibilityUiState.stateDescription
- }
- .sysuiResTag(if (iconOnly) TEST_TAG_SMALL else TEST_TAG_LARGE)
- .thenIf(iconOnly) {
- Modifier.semantics {
- contentDescription =
- uiState.accessibilityUiState.contentDescription
- }
- }
- }
- .tilePadding()
- ) {
- content(it)
- }
- }
-
- if (showLabels && iconOnly) {
- Text(
- label,
- maxLines = 2,
- color = colors.label,
- overflow = TextOverflow.Ellipsis,
- textAlign = TextAlign.Center,
- )
- }
- }
-}
-
-@Composable
-private fun LargeTileContent(
- label: String,
- secondaryLabel: String?,
- icon: Icon,
- colors: TileColors,
- iconShape: Shape,
- accessibilityUiState: AccessibilityUiState? = null,
- toggleClickSupported: Boolean = false,
- onClick: () -> Unit = {},
- onLongClick: () -> Unit = {},
-) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = tileHorizontalArrangement(),
- ) {
- // Icon
- val longPressLabel = longPressLabel()
- Box(
- modifier =
- Modifier.size(TileDefaults.ToggleTargetSize).thenIf(toggleClickSupported) {
- Modifier.clip(iconShape)
- .background(colors.iconBackground, { 1f })
- .combinedClickable(
- onClick = onClick,
- onLongClick = onLongClick,
- onLongClickLabel = longPressLabel,
- )
- .thenIf(accessibilityUiState != null) {
- accessibilityUiState as AccessibilityUiState
- Modifier.semantics {
- contentDescription = accessibilityUiState.contentDescription
- stateDescription = accessibilityUiState.stateDescription
- accessibilityUiState.toggleableState?.let {
- toggleableState = it
- }
- role = Role.Switch
- }
- .sysuiResTag(TEST_TAG_TOGGLE)
- }
- }
- ) {
- TileIcon(icon = icon, color = colors.icon, modifier = Modifier.align(Alignment.Center))
- }
-
- // Labels
- Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
- Text(label, color = colors.label, modifier = Modifier.tileMarquee())
- if (!TextUtils.isEmpty(secondaryLabel)) {
- Text(
- secondaryLabel ?: "",
- color = colors.secondaryLabel,
- modifier =
- Modifier.tileMarquee().thenIf(
- accessibilityUiState
- ?.stateDescription
- ?.contains(secondaryLabel ?: "") == true
- ) {
- Modifier.clearAndSetSemantics {}
- },
- )
- }
- }
- }
-}
-
-private fun Modifier.tileMarquee(): Modifier {
- return basicMarquee(iterations = 1, initialDelayMillis = 200)
-}
-
-@Composable
-fun TileLazyGrid(
- modifier: Modifier = Modifier,
- state: LazyGridState = rememberLazyGridState(),
- columns: GridCells,
- content: LazyGridScope.() -> Unit,
-) {
- LazyVerticalGrid(
- state = state,
- columns = columns,
- verticalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)),
- horizontalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_horizontal)),
- modifier = modifier,
- content = content,
- )
-}
-
-@Composable
-fun DefaultEditTileGrid(
- currentListState: EditTileListState,
- otherTiles: List<SizedTile<EditTileViewModel>>,
- columns: Int,
- modifier: Modifier,
- onAddTile: (TileSpec, Int) -> Unit,
- onRemoveTile: (TileSpec) -> Unit,
- onSetTiles: (List<TileSpec>) -> Unit,
- onResize: (TileSpec) -> Unit,
-) {
- val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
- onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
- }
- val tilePadding = dimensionResource(R.dimen.qs_tile_margin_vertical)
-
- CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
- Column(
- verticalArrangement =
- spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
- modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()),
- ) {
- AnimatedContent(
- targetState = currentListState.dragInProgress,
- modifier = Modifier.wrapContentSize(),
- ) { dragIsInProgress ->
- EditGridHeader(Modifier.dragAndDropRemoveZone(currentListState, onRemoveTile)) {
- if (dragIsInProgress) {
- RemoveTileTarget()
- } else {
- Text(text = "Hold and drag to rearrange tiles.")
- }
- }
- }
-
- CurrentTilesGrid(
- currentListState,
- columns,
- tilePadding,
- onRemoveTile,
- onResize,
- onSetTiles,
- )
-
- // Hide available tiles when dragging
- AnimatedVisibility(
- visible = !currentListState.dragInProgress,
- enter = fadeIn(),
- exit = fadeOut(),
- ) {
- Column(
- verticalArrangement =
- spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
- modifier = modifier.fillMaxSize(),
- ) {
- EditGridHeader { Text(text = "Hold and drag to add tiles.") }
-
- AvailableTileGrid(
- otherTiles,
- columns,
- tilePadding,
- addTileToEnd,
- currentListState,
- )
- }
- }
-
- // Drop zone to remove tiles dragged out of the tile grid
- Spacer(
- modifier =
- Modifier.fillMaxWidth()
- .weight(1f)
- .dragAndDropRemoveZone(currentListState, onRemoveTile)
- )
- }
- }
-}
-
-@Composable
-private fun EditGridHeader(
- modifier: Modifier = Modifier,
- content: @Composable BoxScope.() -> Unit,
-) {
- CompositionLocalProvider(
- LocalContentColor provides MaterialTheme.colorScheme.onBackground.copy(alpha = .5f)
- ) {
- Box(
- contentAlignment = Alignment.Center,
- modifier = modifier.fillMaxWidth().height(EditModeTileDefaults.EditGridHeaderHeight),
- ) {
- content()
- }
- }
-}
-
-@Composable
-private fun RemoveTileTarget() {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = tileHorizontalArrangement(),
- modifier =
- Modifier.fillMaxHeight()
- .border(1.dp, LocalContentColor.current, shape = CircleShape)
- .padding(10.dp),
- ) {
- Icon(imageVector = Icons.Default.Clear, contentDescription = null)
- Text(text = "Remove")
- }
-}
-
-@Composable
-private fun CurrentTilesContainer(content: @Composable () -> Unit) {
- Box(
- Modifier.fillMaxWidth()
- .border(
- width = 1.dp,
- color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f),
- shape = RoundedCornerShape(48.dp),
- )
- .padding(dimensionResource(R.dimen.qs_tile_margin_vertical))
- ) {
- content()
- }
-}
-
-@Composable
-private fun CurrentTilesGrid(
- listState: EditTileListState,
- columns: Int,
- tilePadding: Dp,
- onClick: (TileSpec) -> Unit,
- onResize: (TileSpec) -> Unit,
- onSetTiles: (List<TileSpec>) -> Unit,
-) {
- val currentListState by rememberUpdatedState(listState)
-
- CurrentTilesContainer {
- val tileHeight = tileHeight()
- val totalRows = listState.tiles.lastOrNull()?.row ?: 0
- val totalHeight = gridHeight(totalRows + 1, tileHeight, tilePadding)
- val gridState = rememberLazyGridState()
- var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) }
-
- TileLazyGrid(
- state = gridState,
- modifier =
- Modifier.height(totalHeight)
- .dragAndDropTileList(gridState, gridContentOffset, listState) {
- onSetTiles(currentListState.tileSpecs())
- }
- .onGloballyPositioned { coordinates ->
- gridContentOffset = coordinates.positionInRoot()
- }
- .testTag(CURRENT_TILES_GRID_TEST_TAG),
- columns = GridCells.Fixed(columns),
- ) {
- editTiles(
- listState.tiles,
- ClickAction.REMOVE,
- onClick,
- listState,
- onResize = onResize,
- indicatePosition = true,
- )
- }
- }
-}
-
-@Composable
-private fun AvailableTileGrid(
- tiles: List<SizedTile<EditTileViewModel>>,
- columns: Int,
- tilePadding: Dp,
- onClick: (TileSpec) -> Unit,
- dragAndDropState: DragAndDropState,
-) {
- val availableTileHeight = tileHeight(true)
- val availableGridHeight = gridHeight(tiles.size, availableTileHeight, columns, tilePadding)
-
- // Available tiles aren't visible during drag and drop, so the row isn't needed
- val groupedTiles =
- remember(tiles.fastMap { it.tile.category }, tiles.fastMap { it.tile.label }) {
- groupAndSort(tiles.fastMap { TileGridCell(it, 0) })
- }
- val labelColors = TileDefaults.inactiveTileColors()
- // Available tiles
- TileLazyGrid(
- modifier = Modifier.height(availableGridHeight).testTag(AVAILABLE_TILES_GRID_TEST_TAG),
- columns = GridCells.Fixed(columns),
- ) {
- groupedTiles.forEach { category, tiles ->
- stickyHeader {
- Text(
- text = category.label.load() ?: "",
- fontSize = 20.sp,
- color = labelColors.label,
- modifier =
- Modifier.background(Color.Black)
- .padding(start = 16.dp, bottom = 8.dp, top = 8.dp),
- )
- }
- editTiles(
- tiles,
- ClickAction.ADD,
- onClick,
- dragAndDropState = dragAndDropState,
- showLabels = true,
- )
- }
- }
-}
-
-fun gridHeight(nTiles: Int, tileHeight: Dp, columns: Int, padding: Dp): Dp {
- val rows = (nTiles + columns - 1) / columns
- return gridHeight(rows, tileHeight, padding)
-}
-
-fun gridHeight(rows: Int, tileHeight: Dp, padding: Dp): Dp {
- return ((tileHeight + padding) * rows) - padding
-}
-
-private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any {
- return if (this is TileGridCell && !dragAndDropState.isMoving(tile.tileSpec)) {
- key
- } else {
- index
- }
-}
-
-fun LazyGridScope.editTiles(
- cells: List<GridCell>,
- clickAction: ClickAction,
- onClick: (TileSpec) -> Unit,
- dragAndDropState: DragAndDropState,
- onResize: (TileSpec) -> Unit = {},
- showLabels: Boolean = false,
- indicatePosition: Boolean = false,
-) {
- items(
- count = cells.size,
- key = { cells[it].key(it, dragAndDropState) },
- span = { cells[it].span },
- contentType = { TileType },
- ) { index ->
- when (val cell = cells[index]) {
- is TileGridCell ->
- if (dragAndDropState.isMoving(cell.tile.tileSpec)) {
- // If the tile is being moved, replace it with a visible spacer
- SpacerGridCell(
- Modifier.background(
- color = MaterialTheme.colorScheme.secondary,
- alpha = { EditModeTileDefaults.PLACEHOLDER_ALPHA },
- shape = RoundedCornerShape(TileDefaults.InactiveCornerRadius),
- )
- .animateItem()
- )
- } else {
- TileGridCell(
- cell = cell,
- index = index,
- dragAndDropState = dragAndDropState,
- clickAction = clickAction,
- onClick = onClick,
- onResize = onResize,
- showLabels = showLabels,
- indicatePosition = indicatePosition,
- )
- }
- is SpacerGridCell -> SpacerGridCell()
- }
- }
-}
-
-@Composable
-private fun LazyGridItemScope.TileGridCell(
- cell: TileGridCell,
- index: Int,
- dragAndDropState: DragAndDropState,
- clickAction: ClickAction,
- onClick: (TileSpec) -> Unit,
- onResize: (TileSpec) -> Unit = {},
- showLabels: Boolean = false,
- indicatePosition: Boolean = false,
-) {
- val tileHeight = tileHeight(cell.isIcon && showLabels)
- val onClickActionName =
- when (clickAction) {
- ClickAction.ADD -> stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
- ClickAction.REMOVE ->
- stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
- }
- val stateDescription =
- if (indicatePosition) {
- stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
- } else {
- ""
- }
- EditTile(
- tileViewModel = cell.tile,
- iconOnly = cell.isIcon,
- showLabels = showLabels,
- modifier =
- Modifier.height(tileHeight)
- .animateItem()
- .semantics(mergeDescendants = true) {
- onClick(onClickActionName) { false }
- this.stateDescription = stateDescription
- }
- .dragAndDropTileSource(
- SizedTileImpl(cell.tile, cell.width),
- onClick,
- onResize,
- dragAndDropState,
- ),
- )
-}
-
-@Composable
-private fun SpacerGridCell(modifier: Modifier = Modifier) {
- // By default, spacers are invisible and exist purely to catch drag movements
- Box(modifier.height(tileHeight()).fillMaxWidth().tilePadding())
-}
-
-@Composable
-fun EditTile(
- tileViewModel: EditTileViewModel,
- iconOnly: Boolean,
- showLabels: Boolean,
- modifier: Modifier = Modifier,
-) {
- val label = tileViewModel.label.text
- val colors = TileDefaults.inactiveTileColors()
-
- TileContainer(
- colors = colors,
- showLabels = showLabels,
- label = label,
- iconOnly = iconOnly,
- shape = RoundedCornerShape(TileDefaults.InactiveCornerRadius),
- modifier = modifier,
- ) {
- if (iconOnly) {
- TileIcon(
- icon = tileViewModel.icon,
- color = colors.icon,
- modifier = Modifier.align(Alignment.Center),
- )
- } else {
- LargeTileContent(
- label = label,
- secondaryLabel = tileViewModel.appName?.text,
- icon = tileViewModel.icon,
- colors = colors,
- iconShape = RoundedCornerShape(TileDefaults.InactiveCornerRadius),
- )
- }
- }
-}
-
-enum class ClickAction {
- ADD,
- REMOVE,
-}
-
-@Composable
-private fun getTileIcon(icon: Supplier<QSTile.Icon?>): Icon {
- val context = LocalContext.current
- return icon.get()?.let {
- if (it is QSTileImpl.ResourceIcon) {
- Icon.Resource(it.resId, null)
- } else {
- Icon.Loaded(it.getDrawable(context), null)
- }
- } ?: Icon.Resource(R.drawable.ic_error_outline, null)
-}
-
-@OptIn(ExperimentalAnimationGraphicsApi::class)
-@Composable
-private fun TileIcon(
- icon: Icon,
- color: Color,
- animateToEnd: Boolean = false,
- modifier: Modifier = Modifier,
-) {
- val iconModifier = modifier.size(TileDefaults.IconSize)
- val context = LocalContext.current
- val loadedDrawable =
- remember(icon, context) {
- when (icon) {
- is Icon.Loaded -> icon.drawable
- is Icon.Resource -> context.getDrawable(icon.res)
- }
- }
- if (loadedDrawable !is Animatable) {
- Icon(icon = icon, tint = color, modifier = iconModifier)
- } else if (icon is Icon.Resource) {
- val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
- val painter =
- if (animateToEnd) {
- rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true)
- } else {
- var atEnd by remember(icon.res) { mutableStateOf(false) }
- LaunchedEffect(key1 = icon.res) {
- delay(350)
- atEnd = true
- }
- rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
- }
- Image(
- painter = painter,
- contentDescription = icon.contentDescription?.load(),
- colorFilter = ColorFilter.tint(color = color),
- modifier = iconModifier,
- )
- }
-}
-
-private fun Modifier.tilePadding(): Modifier {
- return padding(TileDefaults.TilePadding)
-}
-
-private fun tileHorizontalArrangement(): Arrangement.Horizontal {
- return spacedBy(space = TileDefaults.TileArrangementPadding, alignment = Alignment.Start)
-}
-
-@Composable
-fun tileHeight(iconWithLabel: Boolean = false): Dp {
- return if (iconWithLabel) {
- TileDefaults.IconTileWithLabelHeight
- } else {
- TileDefaults.TileHeight
- }
-}
-
-private data class TileColors(
- val background: Color,
- val iconBackground: Color,
- val label: Color,
- val secondaryLabel: Color,
- val icon: Color,
-)
-
-private object EditModeTileDefaults {
- const val PLACEHOLDER_ALPHA = .3f
- val EditGridHeaderHeight = 60.dp
-}
-
-private object TileDefaults {
- val InactiveCornerRadius = 50.dp
- val ActiveIconCornerRadius = 16.dp
- val ActiveTileCornerRadius = 24.dp
-
- val ToggleTargetSize = 56.dp
- val IconSize = 24.dp
-
- val TilePadding = 8.dp
- val TileArrangementPadding = 6.dp
-
- val TileHeight = 72.dp
- val IconTileWithLabelHeight = 140.dp
-
- @Composable fun longPressLabel() = stringResource(id = R.string.accessibility_long_click_tile)
-
- /** An active tile without dual target uses the active color as background */
- @Composable
- fun activeTileColors(): TileColors =
- TileColors(
- background = MaterialTheme.colorScheme.primary,
- iconBackground = MaterialTheme.colorScheme.primary,
- label = MaterialTheme.colorScheme.onPrimary,
- secondaryLabel = MaterialTheme.colorScheme.onPrimary,
- icon = MaterialTheme.colorScheme.onPrimary,
- )
-
- /** An active tile with dual target only show the active color on the icon */
- @Composable
- fun activeDualTargetTileColors(): TileColors =
- TileColors(
- background = MaterialTheme.colorScheme.surfaceVariant,
- iconBackground = MaterialTheme.colorScheme.primary,
- label = MaterialTheme.colorScheme.onSurfaceVariant,
- secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant,
- icon = MaterialTheme.colorScheme.onPrimary,
- )
-
- @Composable
- fun inactiveTileColors(): TileColors =
- TileColors(
- background = MaterialTheme.colorScheme.surfaceVariant,
- iconBackground = MaterialTheme.colorScheme.surfaceVariant,
- label = MaterialTheme.colorScheme.onSurfaceVariant,
- secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant,
- icon = MaterialTheme.colorScheme.onSurfaceVariant,
- )
-
- @Composable
- fun unavailableTileColors(): TileColors =
- TileColors(
- background = MaterialTheme.colorScheme.surface,
- iconBackground = MaterialTheme.colorScheme.surface,
- label = MaterialTheme.colorScheme.onSurface,
- secondaryLabel = MaterialTheme.colorScheme.onSurface,
- icon = MaterialTheme.colorScheme.onSurface,
- )
-
- @Composable
- fun getColorForState(uiState: TileUiState): TileColors {
- return when (uiState.state) {
- STATE_ACTIVE -> {
- if (uiState.handlesSecondaryClick) {
- activeDualTargetTileColors()
- } else {
- activeTileColors()
- }
- }
- STATE_INACTIVE -> inactiveTileColors()
- else -> unavailableTileColors()
- }
- }
-
- @Composable
- fun animateIconShape(state: Int): Shape {
- return animateShape(
- state = state,
- activeCornerRadius = ActiveIconCornerRadius,
- label = "QSTileCornerRadius",
- )
- }
-
- @Composable
- fun animateTileShape(state: Int): Shape {
- return animateShape(
- state = state,
- activeCornerRadius = ActiveTileCornerRadius,
- label = "QSTileIconCornerRadius",
- )
- }
-
- @Composable
- fun animateShape(state: Int, activeCornerRadius: Dp, label: String): Shape {
- val animatedCornerRadius by
- animateDpAsState(
- targetValue =
- if (state == STATE_ACTIVE) {
- activeCornerRadius
- } else {
- InactiveCornerRadius
- },
- label = label,
- )
- return RoundedCornerShape(animatedCornerRadius)
- }
-}
-
-private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid"
-private const val AVAILABLE_TILES_GRID_TEST_TAG = "AvailableTilesGrid"
-
-/**
- * A composable function that returns the [Resources]. It will be recomposed when [Configuration]
- * gets updated.
- */
-@Composable
-@ReadOnlyComposable
-private fun resources(): Resources {
- LocalConfiguration.current
- return LocalContext.current.resources
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
new file mode 100644
index 0000000..aeb6031
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.compose.infinitegrid
+
+import android.graphics.drawable.Animatable
+import android.text.TextUtils
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.toggleableState
+import androidx.compose.ui.unit.dp
+import com.android.compose.modifiers.background
+import com.android.compose.modifiers.thenIf
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.common.ui.compose.load
+import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel
+import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState
+import com.android.systemui.res.R
+import kotlinx.coroutines.delay
+
+private const val TEST_TAG_TOGGLE = "qs_tile_toggle_target"
+
+@Composable
+fun LargeTileContent(
+ label: String,
+ secondaryLabel: String?,
+ icon: Icon,
+ colors: TileColors,
+ accessibilityUiState: AccessibilityUiState? = null,
+ toggleClickSupported: Boolean = false,
+ iconShape: Shape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius),
+ onClick: () -> Unit = {},
+ onLongClick: () -> Unit = {},
+) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = tileHorizontalArrangement(),
+ ) {
+ // Icon
+ val longPressLabel = longPressLabel()
+ Box(
+ modifier =
+ Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClickSupported) {
+ Modifier.clip(iconShape)
+ .background(colors.iconBackground, { 1f })
+ .combinedClickable(
+ onClick = onClick,
+ onLongClick = onLongClick,
+ onLongClickLabel = longPressLabel,
+ )
+ .thenIf(accessibilityUiState != null) {
+ Modifier.semantics {
+ accessibilityUiState as AccessibilityUiState
+ contentDescription = accessibilityUiState.contentDescription
+ stateDescription = accessibilityUiState.stateDescription
+ accessibilityUiState.toggleableState?.let {
+ toggleableState = it
+ }
+ role = Role.Switch
+ }
+ .sysuiResTag(TEST_TAG_TOGGLE)
+ }
+ }
+ ) {
+ SmallTileContent(
+ icon = icon,
+ color = colors.icon,
+ modifier = Modifier.align(Alignment.Center),
+ )
+ }
+
+ // Labels
+ LargeTileLabels(
+ label = label,
+ secondaryLabel = secondaryLabel,
+ colors = colors,
+ accessibilityUiState = accessibilityUiState,
+ )
+ }
+}
+
+@Composable
+private fun LargeTileLabels(
+ label: String,
+ secondaryLabel: String?,
+ colors: TileColors,
+ accessibilityUiState: AccessibilityUiState? = null,
+) {
+ Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
+ Text(label, color = colors.label, modifier = Modifier.tileMarquee())
+ if (!TextUtils.isEmpty(secondaryLabel)) {
+ Text(
+ secondaryLabel ?: "",
+ color = colors.secondaryLabel,
+ modifier =
+ Modifier.tileMarquee().thenIf(
+ accessibilityUiState?.stateDescription?.contains(secondaryLabel ?: "") ==
+ true
+ ) {
+ Modifier.clearAndSetSemantics {}
+ },
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalAnimationGraphicsApi::class)
+@Composable
+fun SmallTileContent(
+ modifier: Modifier = Modifier,
+ icon: Icon,
+ color: Color,
+ animateToEnd: Boolean = false,
+) {
+ val iconModifier = modifier.size(CommonTileDefaults.IconSize)
+ val context = LocalContext.current
+ val loadedDrawable =
+ remember(icon, context) {
+ when (icon) {
+ is Icon.Loaded -> icon.drawable
+ is Icon.Resource -> context.getDrawable(icon.res)
+ }
+ }
+ if (loadedDrawable !is Animatable) {
+ Icon(icon = icon, tint = color, modifier = iconModifier)
+ } else if (icon is Icon.Resource) {
+ val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
+ val painter =
+ if (animateToEnd) {
+ rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true)
+ } else {
+ var atEnd by remember(icon.res) { mutableStateOf(false) }
+ LaunchedEffect(key1 = icon.res) {
+ delay(350)
+ atEnd = true
+ }
+ rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
+ }
+ Image(
+ painter = painter,
+ contentDescription = icon.contentDescription?.load(),
+ colorFilter = ColorFilter.tint(color = color),
+ modifier = iconModifier,
+ )
+ }
+}
+
+object CommonTileDefaults {
+ val IconSize = 24.dp
+ val ToggleTargetSize = 56.dp
+ val TileHeight = 72.dp
+ val TilePadding = 8.dp
+ val TileArrangementPadding = 6.dp
+ val InactiveCornerRadius = 50.dp
+
+ @Composable fun longPressLabel() = stringResource(id = R.string.accessibility_long_click_tile)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
new file mode 100644
index 0000000..a43b880
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
@@ -0,0 +1,503 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalFoundationApi::class)
+
+package com.android.systemui.qs.panels.ui.compose.infinitegrid
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.LocalOverscrollConfiguration
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyGridItemScope
+import androidx.compose.foundation.lazy.grid.LazyGridScope
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.util.fastMap
+import com.android.compose.modifiers.background
+import com.android.systemui.common.ui.compose.load
+import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.shared.model.SizedTileImpl
+import com.android.systemui.qs.panels.ui.compose.DragAndDropState
+import com.android.systemui.qs.panels.ui.compose.EditTileListState
+import com.android.systemui.qs.panels.ui.compose.dragAndDropRemoveZone
+import com.android.systemui.qs.panels.ui.compose.dragAndDropTileList
+import com.android.systemui.qs.panels.ui.compose.dragAndDropTileSource
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius
+import com.android.systemui.qs.panels.ui.model.GridCell
+import com.android.systemui.qs.panels.ui.model.SpacerGridCell
+import com.android.systemui.qs.panels.ui.model.TileGridCell
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.groupAndSort
+import com.android.systemui.res.R
+
+object TileType
+
+@Composable
+fun DefaultEditTileGrid(
+ currentListState: EditTileListState,
+ otherTiles: List<SizedTile<EditTileViewModel>>,
+ columns: Int,
+ modifier: Modifier,
+ onAddTile: (TileSpec, Int) -> Unit,
+ onRemoveTile: (TileSpec) -> Unit,
+ onSetTiles: (List<TileSpec>) -> Unit,
+ onResize: (TileSpec) -> Unit,
+) {
+ val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
+ onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
+ }
+
+ CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
+ Column(
+ verticalArrangement =
+ spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
+ modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()),
+ ) {
+ AnimatedContent(
+ targetState = currentListState.dragInProgress,
+ modifier = Modifier.wrapContentSize(),
+ label = "",
+ ) { dragIsInProgress ->
+ EditGridHeader(Modifier.dragAndDropRemoveZone(currentListState, onRemoveTile)) {
+ if (dragIsInProgress) {
+ RemoveTileTarget()
+ } else {
+ Text(text = "Hold and drag to rearrange tiles.")
+ }
+ }
+ }
+
+ CurrentTilesGrid(currentListState, columns, onRemoveTile, onResize, onSetTiles)
+
+ // Hide available tiles when dragging
+ AnimatedVisibility(
+ visible = !currentListState.dragInProgress,
+ enter = fadeIn(),
+ exit = fadeOut(),
+ ) {
+ Column(
+ verticalArrangement =
+ spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
+ modifier = modifier.fillMaxSize(),
+ ) {
+ EditGridHeader { Text(text = "Hold and drag to add tiles.") }
+
+ AvailableTileGrid(otherTiles, columns, addTileToEnd, currentListState)
+ }
+ }
+
+ // Drop zone to remove tiles dragged out of the tile grid
+ Spacer(
+ modifier =
+ Modifier.fillMaxWidth()
+ .weight(1f)
+ .dragAndDropRemoveZone(currentListState, onRemoveTile)
+ )
+ }
+ }
+}
+
+@Composable
+private fun EditGridHeader(
+ modifier: Modifier = Modifier,
+ content: @Composable BoxScope.() -> Unit,
+) {
+ CompositionLocalProvider(
+ LocalContentColor provides MaterialTheme.colorScheme.onBackground.copy(alpha = .5f)
+ ) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = modifier.fillMaxWidth().height(EditModeTileDefaults.EditGridHeaderHeight),
+ ) {
+ content()
+ }
+ }
+}
+
+@Composable
+private fun RemoveTileTarget() {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = tileHorizontalArrangement(),
+ modifier =
+ Modifier.fillMaxHeight()
+ .border(1.dp, LocalContentColor.current, shape = CircleShape)
+ .padding(10.dp),
+ ) {
+ Icon(imageVector = Icons.Default.Clear, contentDescription = null)
+ Text(text = "Remove")
+ }
+}
+
+@Composable
+private fun CurrentTilesContainer(content: @Composable () -> Unit) {
+ Box(
+ Modifier.fillMaxWidth()
+ .border(
+ width = 1.dp,
+ color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f),
+ shape = RoundedCornerShape(48.dp),
+ )
+ .padding(dimensionResource(R.dimen.qs_tile_margin_vertical))
+ ) {
+ content()
+ }
+}
+
+@Composable
+private fun CurrentTilesGrid(
+ listState: EditTileListState,
+ columns: Int,
+ onClick: (TileSpec) -> Unit,
+ onResize: (TileSpec) -> Unit,
+ onSetTiles: (List<TileSpec>) -> Unit,
+) {
+ val currentListState by rememberUpdatedState(listState)
+ val tilePadding = CommonTileDefaults.TileArrangementPadding
+
+ CurrentTilesContainer {
+ val tileHeight = CommonTileDefaults.TileHeight
+ val totalRows = listState.tiles.lastOrNull()?.row ?: 0
+ val totalHeight = gridHeight(totalRows + 1, tileHeight, tilePadding)
+ val gridState = rememberLazyGridState()
+ var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) }
+
+ TileLazyGrid(
+ state = gridState,
+ modifier =
+ Modifier.height(totalHeight)
+ .dragAndDropTileList(gridState, gridContentOffset, listState) {
+ onSetTiles(currentListState.tileSpecs())
+ }
+ .onGloballyPositioned { coordinates ->
+ gridContentOffset = coordinates.positionInRoot()
+ }
+ .testTag(CURRENT_TILES_GRID_TEST_TAG),
+ columns = GridCells.Fixed(columns),
+ ) {
+ EditTiles(listState.tiles, onClick, listState, onResize = onResize)
+ }
+ }
+}
+
+@Composable
+private fun AvailableTileGrid(
+ tiles: List<SizedTile<EditTileViewModel>>,
+ columns: Int,
+ onClick: (TileSpec) -> Unit,
+ dragAndDropState: DragAndDropState,
+) {
+ // Available tiles aren't visible during drag and drop, so the row isn't needed
+ val groupedTiles =
+ remember(tiles.fastMap { it.tile.category }, tiles.fastMap { it.tile.label }) {
+ groupAndSort(tiles.fastMap { TileGridCell(it, 0) })
+ }
+ val labelColors = EditModeTileDefaults.editTileColors()
+
+ // Available tiles
+ Column(
+ verticalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding),
+ horizontalAlignment = Alignment.Start,
+ modifier =
+ Modifier.fillMaxWidth().wrapContentHeight().testTag(AVAILABLE_TILES_GRID_TEST_TAG),
+ ) {
+ groupedTiles.forEach { (category, tiles) ->
+ Text(
+ text = category.label.load() ?: "",
+ fontSize = 20.sp,
+ color = labelColors.label,
+ modifier =
+ Modifier.fillMaxWidth()
+ .background(Color.Black)
+ .padding(start = 16.dp, bottom = 8.dp, top = 8.dp),
+ )
+ tiles.chunked(columns).forEach { row ->
+ Row(
+ horizontalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding),
+ modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Max),
+ ) {
+ row.forEachIndexed { index, tileGridCell ->
+ AvailableTileGridCell(
+ cell = tileGridCell,
+ index = index,
+ dragAndDropState = dragAndDropState,
+ onClick = onClick,
+ modifier = Modifier.weight(1f).fillMaxHeight(),
+ )
+ }
+
+ // Spacers for incomplete rows
+ repeat(columns - row.size) { Spacer(modifier = Modifier.weight(1f)) }
+ }
+ }
+ }
+ }
+}
+
+fun gridHeight(rows: Int, tileHeight: Dp, padding: Dp): Dp {
+ return ((tileHeight + padding) * rows) - padding
+}
+
+private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any {
+ return when (this) {
+ is TileGridCell -> {
+ if (dragAndDropState.isMoving(tile.tileSpec)) index else key
+ }
+ is SpacerGridCell -> index
+ }
+}
+
+fun LazyGridScope.EditTiles(
+ cells: List<GridCell>,
+ onClick: (TileSpec) -> Unit,
+ dragAndDropState: DragAndDropState,
+ onResize: (TileSpec) -> Unit = {},
+) {
+ items(
+ count = cells.size,
+ key = { cells[it].key(it, dragAndDropState) },
+ span = { cells[it].span },
+ contentType = { TileType },
+ ) { index ->
+ when (val cell = cells[index]) {
+ is TileGridCell ->
+ if (dragAndDropState.isMoving(cell.tile.tileSpec)) {
+ // If the tile is being moved, replace it with a visible spacer
+ SpacerGridCell(
+ Modifier.background(
+ color = MaterialTheme.colorScheme.secondary,
+ alpha = { EditModeTileDefaults.PLACEHOLDER_ALPHA },
+ shape = RoundedCornerShape(InactiveCornerRadius),
+ )
+ .animateItem()
+ )
+ } else {
+ TileGridCell(
+ cell = cell,
+ index = index,
+ dragAndDropState = dragAndDropState,
+ onClick = onClick,
+ onResize = onResize,
+ )
+ }
+ is SpacerGridCell -> SpacerGridCell()
+ }
+ }
+}
+
+@Composable
+private fun LazyGridItemScope.TileGridCell(
+ cell: TileGridCell,
+ index: Int,
+ dragAndDropState: DragAndDropState,
+ onClick: (TileSpec) -> Unit,
+ onResize: (TileSpec) -> Unit = {},
+) {
+ val onClickActionName = stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
+ val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
+
+ EditTile(
+ tileViewModel = cell.tile,
+ iconOnly = cell.isIcon,
+ modifier =
+ Modifier.animateItem()
+ .semantics(mergeDescendants = true) {
+ onClick(onClickActionName) { false }
+ this.stateDescription = stateDescription
+ }
+ .dragAndDropTileSource(
+ SizedTileImpl(cell.tile, cell.width),
+ dragAndDropState,
+ onClick,
+ onResize,
+ ),
+ )
+}
+
+@Composable
+private fun AvailableTileGridCell(
+ cell: TileGridCell,
+ index: Int,
+ dragAndDropState: DragAndDropState,
+ modifier: Modifier = Modifier,
+ onClick: (TileSpec) -> Unit,
+) {
+ val onClickActionName = stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
+ val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
+ val colors = EditModeTileDefaults.editTileColors()
+
+ // Displays the tile as an icon tile with the label underneath
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = spacedBy(CommonTileDefaults.TilePadding, Alignment.Top),
+ modifier = modifier,
+ ) {
+ EditTile(
+ tileViewModel = cell.tile,
+ iconOnly = true,
+ colors = colors,
+ modifier =
+ Modifier.semantics(mergeDescendants = true) {
+ onClick(onClickActionName) { false }
+ this.stateDescription = stateDescription
+ }
+ .dragAndDropTileSource(
+ SizedTileImpl(cell.tile, cell.width),
+ dragAndDropState,
+ onTap = onClick,
+ ),
+ )
+ Box(Modifier.fillMaxSize()) {
+ Text(
+ cell.tile.label.text,
+ maxLines = 2,
+ color = colors.label,
+ overflow = TextOverflow.Ellipsis,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.align(Alignment.Center),
+ )
+ }
+ }
+}
+
+@Composable
+private fun SpacerGridCell(modifier: Modifier = Modifier) {
+ // By default, spacers are invisible and exist purely to catch drag movements
+ Box(modifier.height(CommonTileDefaults.TileHeight).fillMaxWidth().tilePadding())
+}
+
+@Composable
+fun EditTile(
+ tileViewModel: EditTileViewModel,
+ iconOnly: Boolean,
+ modifier: Modifier = Modifier,
+ colors: TileColors = EditModeTileDefaults.editTileColors(),
+) {
+ EditTileContainer(colors = colors, modifier = modifier) {
+ if (iconOnly) {
+ SmallTileContent(
+ icon = tileViewModel.icon,
+ color = colors.icon,
+ modifier = Modifier.align(Alignment.Center),
+ )
+ } else {
+ LargeTileContent(
+ label = tileViewModel.label.text,
+ secondaryLabel = tileViewModel.appName?.text,
+ icon = tileViewModel.icon,
+ colors = colors,
+ )
+ }
+ }
+}
+
+@Composable
+private fun EditTileContainer(
+ colors: TileColors,
+ modifier: Modifier = Modifier,
+ content: @Composable BoxScope.() -> Unit,
+) {
+ Box(
+ modifier =
+ modifier
+ .height(CommonTileDefaults.TileHeight)
+ .fillMaxWidth()
+ .drawBehind {
+ drawRoundRect(
+ SolidColor(colors.background),
+ cornerRadius = CornerRadius(InactiveCornerRadius.toPx()),
+ )
+ }
+ .tilePadding(),
+ content = content,
+ )
+}
+
+private object EditModeTileDefaults {
+ const val PLACEHOLDER_ALPHA = .3f
+ val EditGridHeaderHeight = 60.dp
+
+ @Composable
+ fun editTileColors(): TileColors =
+ TileColors(
+ background = MaterialTheme.colorScheme.surfaceVariant,
+ iconBackground = MaterialTheme.colorScheme.surfaceVariant,
+ label = MaterialTheme.colorScheme.onSurfaceVariant,
+ secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant,
+ icon = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+}
+
+private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid"
+private const val AVAILABLE_TILES_GRID_TEST_TAG = "AvailableTilesGrid"
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index c75b601..f96c27d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.qs.panels.ui.compose
+package com.android.systemui.qs.panels.ui.compose.infinitegrid
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
@@ -26,6 +26,8 @@
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.panels.shared.model.SizedTileImpl
+import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout
+import com.android.systemui.qs.panels.ui.compose.rememberEditListState
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
@@ -61,7 +63,7 @@
Tile(
tile = sizedTiles[index].tile,
iconOnly = iconTilesViewModel.isIconTile(sizedTiles[index].tile.spec),
- modifier = Modifier
+ modifier = Modifier,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
new file mode 100644
index 0000000..aa6c08e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
@@ -0,0 +1,329 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalFoundationApi::class)
+
+package com.android.systemui.qs.panels.ui.compose.infinitegrid
+
+import android.content.res.Resources
+import android.service.quicksettings.Tile.STATE_ACTIVE
+import android.service.quicksettings.Tile.STATE_INACTIVE
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyGridScope
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.toggleableState
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.Expandable
+import com.android.compose.modifiers.thenIf
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel
+import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
+import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.toUiState
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.res.R
+import java.util.function.Supplier
+
+private const val TEST_TAG_SMALL = "qs_tile_small"
+private const val TEST_TAG_LARGE = "qs_tile_large"
+
+@Composable
+fun TileLazyGrid(
+ columns: GridCells,
+ modifier: Modifier = Modifier,
+ state: LazyGridState = rememberLazyGridState(),
+ content: LazyGridScope.() -> Unit,
+) {
+ LazyVerticalGrid(
+ state = state,
+ columns = columns,
+ verticalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding),
+ horizontalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding),
+ modifier = modifier,
+ content = content,
+ )
+}
+
+@Composable
+fun Tile(tile: TileViewModel, iconOnly: Boolean, modifier: Modifier) {
+ val state by tile.state.collectAsStateWithLifecycle(tile.currentState)
+ val resources = resources()
+ val uiState = remember(state, resources) { state.toUiState(resources) }
+ val colors = TileDefaults.getColorForState(uiState)
+
+ // TODO(b/361789146): Draw the shapes instead of clipping
+ val tileShape = TileDefaults.animateTileShape(uiState.state)
+
+ TileContainer(
+ color =
+ if (iconOnly || !uiState.handlesSecondaryClick) {
+ colors.iconBackground
+ } else {
+ colors.background
+ },
+ shape = tileShape,
+ iconOnly = iconOnly,
+ onClick = tile::onClick,
+ onLongClick = tile::onLongClick,
+ uiState = uiState,
+ modifier = modifier,
+ ) { expandable ->
+ val icon = getTileIcon(icon = uiState.icon)
+ if (iconOnly) {
+ SmallTileContent(
+ icon = icon,
+ color = colors.icon,
+ modifier = Modifier.align(Alignment.Center),
+ )
+ } else {
+ val iconShape = TileDefaults.animateIconShape(uiState.state)
+ LargeTileContent(
+ label = uiState.label,
+ secondaryLabel = uiState.secondaryLabel,
+ icon = icon,
+ colors = colors,
+ iconShape = iconShape,
+ toggleClickSupported = state.handlesSecondaryClick,
+ onClick = {
+ if (state.handlesSecondaryClick) {
+ tile.onSecondaryClick()
+ }
+ },
+ onLongClick = { tile.onLongClick(expandable) },
+ )
+ }
+ }
+}
+
+@Composable
+private fun TileContainer(
+ color: Color,
+ shape: Shape,
+ iconOnly: Boolean,
+ uiState: TileUiState,
+ modifier: Modifier = Modifier,
+ onClick: (Expandable) -> Unit = {},
+ onLongClick: (Expandable) -> Unit = {},
+ content: @Composable BoxScope.(Expandable) -> Unit,
+) {
+ Expandable(color = color, shape = shape, modifier = modifier.clip(shape)) {
+ val longPressLabel = longPressLabel()
+ Box(
+ modifier =
+ Modifier.height(CommonTileDefaults.TileHeight)
+ .fillMaxWidth()
+ .combinedClickable(
+ onClick = { onClick(it) },
+ onLongClick = { onLongClick(it) },
+ onClickLabel = uiState.accessibilityUiState.clickLabel,
+ onLongClickLabel = longPressLabel,
+ )
+ .semantics {
+ role = uiState.accessibilityUiState.accessibilityRole
+ if (uiState.accessibilityUiState.accessibilityRole == Role.Switch) {
+ uiState.accessibilityUiState.toggleableState?.let {
+ toggleableState = it
+ }
+ }
+ stateDescription = uiState.accessibilityUiState.stateDescription
+ }
+ .sysuiResTag(if (iconOnly) TEST_TAG_SMALL else TEST_TAG_LARGE)
+ .thenIf(iconOnly) {
+ Modifier.semantics {
+ contentDescription = uiState.accessibilityUiState.contentDescription
+ }
+ }
+ .tilePadding()
+ ) {
+ content(it)
+ }
+ }
+}
+
+@Composable
+private fun getTileIcon(icon: Supplier<QSTile.Icon?>): Icon {
+ val context = LocalContext.current
+ return icon.get()?.let {
+ if (it is QSTileImpl.ResourceIcon) {
+ Icon.Resource(it.resId, null)
+ } else {
+ Icon.Loaded(it.getDrawable(context), null)
+ }
+ } ?: Icon.Resource(R.drawable.ic_error_outline, null)
+}
+
+fun tileHorizontalArrangement(): Arrangement.Horizontal {
+ return spacedBy(space = CommonTileDefaults.TileArrangementPadding, alignment = Alignment.Start)
+}
+
+fun Modifier.tileMarquee(): Modifier {
+ return basicMarquee(iterations = 1, initialDelayMillis = 200)
+}
+
+fun Modifier.tilePadding(): Modifier {
+ return padding(CommonTileDefaults.TilePadding)
+}
+
+data class TileColors(
+ val background: Color,
+ val iconBackground: Color,
+ val label: Color,
+ val secondaryLabel: Color,
+ val icon: Color,
+)
+
+private object TileDefaults {
+ val ActiveIconCornerRadius = 16.dp
+ val ActiveTileCornerRadius = 24.dp
+
+ /** An active tile without dual target uses the active color as background */
+ @Composable
+ fun activeTileColors(): TileColors =
+ TileColors(
+ background = MaterialTheme.colorScheme.primary,
+ iconBackground = MaterialTheme.colorScheme.primary,
+ label = MaterialTheme.colorScheme.onPrimary,
+ secondaryLabel = MaterialTheme.colorScheme.onPrimary,
+ icon = MaterialTheme.colorScheme.onPrimary,
+ )
+
+ /** An active tile with dual target only show the active color on the icon */
+ @Composable
+ fun activeDualTargetTileColors(): TileColors =
+ TileColors(
+ background = MaterialTheme.colorScheme.surfaceVariant,
+ iconBackground = MaterialTheme.colorScheme.primary,
+ label = MaterialTheme.colorScheme.onSurfaceVariant,
+ secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant,
+ icon = MaterialTheme.colorScheme.onPrimary,
+ )
+
+ @Composable
+ fun inactiveTileColors(): TileColors =
+ TileColors(
+ background = MaterialTheme.colorScheme.surfaceVariant,
+ iconBackground = MaterialTheme.colorScheme.surfaceVariant,
+ label = MaterialTheme.colorScheme.onSurfaceVariant,
+ secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant,
+ icon = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+
+ @Composable
+ fun unavailableTileColors(): TileColors =
+ TileColors(
+ background = MaterialTheme.colorScheme.surface,
+ iconBackground = MaterialTheme.colorScheme.surface,
+ label = MaterialTheme.colorScheme.onSurface,
+ secondaryLabel = MaterialTheme.colorScheme.onSurface,
+ icon = MaterialTheme.colorScheme.onSurface,
+ )
+
+ @Composable
+ fun getColorForState(uiState: TileUiState): TileColors {
+ return when (uiState.state) {
+ STATE_ACTIVE -> {
+ if (uiState.handlesSecondaryClick) {
+ activeDualTargetTileColors()
+ } else {
+ activeTileColors()
+ }
+ }
+ STATE_INACTIVE -> inactiveTileColors()
+ else -> unavailableTileColors()
+ }
+ }
+
+ @Composable
+ fun animateIconShape(state: Int): Shape {
+ return animateShape(
+ state = state,
+ activeCornerRadius = ActiveIconCornerRadius,
+ label = "QSTileCornerRadius",
+ )
+ }
+
+ @Composable
+ fun animateTileShape(state: Int): Shape {
+ return animateShape(
+ state = state,
+ activeCornerRadius = ActiveTileCornerRadius,
+ label = "QSTileIconCornerRadius",
+ )
+ }
+
+ @Composable
+ fun animateShape(state: Int, activeCornerRadius: Dp, label: String): Shape {
+ val animatedCornerRadius by
+ animateDpAsState(
+ targetValue =
+ if (state == STATE_ACTIVE) {
+ activeCornerRadius
+ } else {
+ InactiveCornerRadius
+ },
+ label = label,
+ )
+ return RoundedCornerShape(animatedCornerRadius)
+ }
+}
+
+/**
+ * A composable function that returns the [Resources]. It will be recomposed when [Configuration]
+ * gets updated.
+ */
+@Composable
+@ReadOnlyComposable
+private fun resources(): Resources {
+ LocalConfiguration.current
+ return LocalContext.current.resources
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt
index 08ee856..b16a707 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt
@@ -24,7 +24,7 @@
import com.android.systemui.qs.shared.model.CategoryAndName
/** Represents an item from a grid associated with a row and a span */
-interface GridCell {
+sealed interface GridCell {
val row: Int
val span: GridItemSpan
}
@@ -38,30 +38,26 @@
override val tile: EditTileViewModel,
override val row: Int,
override val width: Int,
- override val span: GridItemSpan = GridItemSpan(width)
+ override val span: GridItemSpan = GridItemSpan(width),
) : GridCell, SizedTile<EditTileViewModel>, CategoryAndName by tile {
val key: String = "${tile.tileSpec.spec}-$row"
constructor(
sizedTile: SizedTile<EditTileViewModel>,
- row: Int
- ) : this(
- tile = sizedTile.tile,
- row = row,
- width = sizedTile.width,
- )
+ row: Int,
+ ) : this(tile = sizedTile.tile, row = row, width = sizedTile.width)
}
/** Represents an empty space used to fill incomplete rows. Will always display as a 1x1 tile */
@Immutable
data class SpacerGridCell(
override val row: Int,
- override val span: GridItemSpan = GridItemSpan(1)
+ override val span: GridItemSpan = GridItemSpan(1),
) : GridCell
fun List<SizedTile<EditTileViewModel>>.toGridCells(
columns: Int,
- includeSpacers: Boolean = false
+ includeSpacers: Boolean = false,
): List<GridCell> {
return splitInRowsSequence(this, columns)
.flatMapIndexed { rowIndex, sizedTiles ->
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
index 0bcb6b7..9677d47 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
@@ -18,8 +18,6 @@
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.flags.NewQsUI
-import com.android.systemui.qs.panels.domain.interactor.GridConsistencyInteractor
import com.android.systemui.qs.pipeline.domain.interactor.AccessibilityTilesInteractor
import com.android.systemui.qs.pipeline.domain.interactor.AutoAddInteractor
import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
@@ -36,16 +34,11 @@
private val autoAddInteractor: AutoAddInteractor,
private val featureFlags: QSPipelineFlagsRepository,
private val restoreReconciliationInteractor: RestoreReconciliationInteractor,
- private val gridConsistencyInteractor: GridConsistencyInteractor,
) : CoreStartable {
override fun start() {
accessibilityTilesInteractor.init(currentTilesInteractor)
autoAddInteractor.init(currentTilesInteractor)
restoreReconciliationInteractor.start()
-
- if (NewQsUI.isEnabled) {
- gridConsistencyInteractor.start()
- }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index e11ffcc..b7e2cf2 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -61,6 +61,7 @@
import com.android.systemui.scene.session.shared.SessionStorage
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.logger.SceneLogger
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.NotificationShadeWindowController
@@ -228,8 +229,10 @@
is ObservableTransitionState.Idle -> {
if (state.currentScene != Scenes.Gone) {
true to "scene is not Gone"
+ } else if (state.currentOverlays.isNotEmpty()) {
+ true to "overlay is shown"
} else {
- false to "scene is Gone"
+ false to "scene is Gone and no overlays are shown"
}
}
is ObservableTransitionState.Transition -> {
@@ -712,19 +715,21 @@
if (isDeviceLocked) {
sceneInteractor.transitionState
.mapNotNull { it as? ObservableTransitionState.Idle }
- .map { it.currentScene }
+ .map { it.currentScene to it.currentOverlays }
.distinctUntilChanged()
- .map { sceneKey ->
- when (sceneKey) {
+ .map { (sceneKey, currentOverlays) ->
+ when {
// When locked, showing the lockscreen scene should be reported
// as "interacting" while showing other scenes should report as
// "not interacting".
//
// This is done here in order to match the legacy
// implementation. The real reason why is lost to lore and myth.
- Scenes.Lockscreen -> true
- Scenes.Bouncer -> false
- Scenes.Shade -> false
+ Overlays.NotificationsShade in currentOverlays -> false
+ Overlays.QuickSettingsShade in currentOverlays -> null
+ sceneKey == Scenes.Lockscreen -> true
+ sceneKey == Scenes.Bouncer -> false
+ sceneKey == Scenes.Shade -> false
else -> null
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
index 751448f..7b6b0f6 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
@@ -26,7 +26,6 @@
import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
import com.android.systemui.keyguard.KeyguardWmStateRefactor
import com.android.systemui.keyguard.MigrateClocksToBlueprint
-import com.android.systemui.keyguard.shared.ComposeLockscreen
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
import com.android.systemui.statusbar.phone.PredictiveBackSysUiFlag
@@ -39,7 +38,6 @@
inline val isEnabled
get() =
sceneContainer() && // mainAconfigFlag
- ComposeLockscreen.isEnabled &&
KeyguardBottomAreaRefactor.isEnabled &&
KeyguardWmStateRefactor.isEnabled &&
MigrateClocksToBlueprint.isEnabled &&
@@ -55,7 +53,6 @@
/** The set of secondary flags which must be enabled for scene container to work properly */
inline fun getSecondaryFlags(): Sequence<FlagToken> =
sequenceOf(
- ComposeLockscreen.token,
KeyguardBottomAreaRefactor.token,
KeyguardWmStateRefactor.token,
MigrateClocksToBlueprint.token,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 42499f0..f76c5fd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -137,7 +137,6 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver;
-import com.android.systemui.keyguard.shared.ComposeLockscreen;
import com.android.systemui.keyguard.shared.model.Edge;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
@@ -186,6 +185,7 @@
import com.android.systemui.statusbar.notification.AnimatableProperty;
import com.android.systemui.statusbar.notification.ConversationNotificationManager;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
+import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
import com.android.systemui.statusbar.notification.PropertyAnimator;
import com.android.systemui.statusbar.notification.ViewGroupFadeHelper;
@@ -207,7 +207,6 @@
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
-import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
import com.android.systemui.statusbar.phone.KeyguardBottomAreaViewController;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -2511,11 +2510,6 @@
return 0;
}
- if (ComposeLockscreen.isEnabled()) {
- return (int) mKeyguardInteractor.getNotificationContainerBounds()
- .getValue().getTop();
- }
-
if (!mKeyguardBypassController.getBypassEnabled()) {
if (MigrateClocksToBlueprint.isEnabled() && !mSplitShadeEnabled) {
return (int) mKeyguardInteractor.getNotificationContainerBounds()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt
index c0302bc..9af4b8c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt
@@ -25,6 +25,7 @@
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
/**
* Repository used for tracking the state of notification remote input (e.g. when the user presses
@@ -33,14 +34,21 @@
interface RemoteInputRepository {
/** Whether remote input is currently active for any notification. */
val isRemoteInputActive: Flow<Boolean>
+
+ /**
+ * The bottom bound of the currently focused remote input notification row, or null if there
+ * isn't one.
+ */
+ val remoteInputRowBottomBound: Flow<Float?>
+
+ fun setRemoteInputRowBottomBound(bottom: Float?)
}
@SysUISingleton
class RemoteInputRepositoryImpl
@Inject
-constructor(
- private val notificationRemoteInputManager: NotificationRemoteInputManager,
-) : RemoteInputRepository {
+constructor(private val notificationRemoteInputManager: NotificationRemoteInputManager) :
+ RemoteInputRepository {
override val isRemoteInputActive: Flow<Boolean> = conflatedCallbackFlow {
trySend(false) // initial value is false
val callback =
@@ -52,6 +60,12 @@
notificationRemoteInputManager.addControllerCallback(callback)
awaitClose { notificationRemoteInputManager.removeControllerCallback(callback) }
}
+
+ override val remoteInputRowBottomBound = MutableStateFlow<Float?>(null)
+
+ override fun setRemoteInputRowBottomBound(bottom: Float?) {
+ remoteInputRowBottomBound.value = bottom
+ }
}
@Module
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt
index 68f727b..b83b0cc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt
@@ -20,13 +20,24 @@
import com.android.systemui.statusbar.data.repository.RemoteInputRepository
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.mapNotNull
/**
* Interactor used for business logic pertaining to the notification remote input (e.g. when the
* user presses "reply" on a notification and the keyboard opens).
*/
@SysUISingleton
-class RemoteInputInteractor @Inject constructor(remoteInputRepository: RemoteInputRepository) {
+class RemoteInputInteractor
+@Inject
+constructor(private val remoteInputRepository: RemoteInputRepository) {
/** Is remote input currently active for a notification? */
val isRemoteInputActive: Flow<Boolean> = remoteInputRepository.isRemoteInputActive
+
+ /** The bottom bound of the currently focused remote input notification row. */
+ val remoteInputRowBottomBound: Flow<Float> =
+ remoteInputRepository.remoteInputRowBottomBound.mapNotNull { it }
+
+ fun setRemoteInputRowBottomBound(bottom: Float?) {
+ remoteInputRepository.setRemoteInputRowBottomBound(bottom)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index cb3e26b..5003a6a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -21,6 +21,7 @@
import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED;
import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP;
+import static com.android.systemui.statusbar.policy.RemoteInputView.FOCUS_ANIMATION_MIN_SCALE;
import static com.android.systemui.util.ColorUtilKt.hexColorString;
import android.animation.Animator;
@@ -83,6 +84,7 @@
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.StatusBarIconView;
@@ -118,6 +120,7 @@
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
+import com.android.systemui.statusbar.policy.RemoteInputView;
import com.android.systemui.statusbar.policy.SmartReplyConstants;
import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent;
import com.android.systemui.util.Compile;
@@ -830,6 +833,20 @@
mPrivateLayout.setRemoteInputController(r);
}
+ /**
+ * Return the cumulative y-value that the actions container expands via its scale animator when
+ * remote input is activated.
+ */
+ public float getRemoteInputActionsContainerExpandedOffset() {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+ RemoteInputView expandedRemoteInput = mPrivateLayout.getExpandedRemoteInput();
+ if (expandedRemoteInput == null) return 0f;
+ View actionsContainerLayout = expandedRemoteInput.getActionsContainerLayout();
+ if (actionsContainerLayout == null) return 0f;
+
+ return actionsContainerLayout.getHeight() * (1 - FOCUS_ANIMATION_MIN_SCALE) * 0.5f;
+ }
+
public void addChildNotification(ExpandableNotificationRow row) {
addChildNotification(row, -1);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 7543f3b..e7c67f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -99,6 +99,7 @@
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.ColorUpdateLogger;
import com.android.systemui.statusbar.notification.FakeShadowView;
+import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
import com.android.systemui.statusbar.notification.LaunchAnimationParameters;
import com.android.systemui.statusbar.notification.NotificationTransitionAnimatorController;
import com.android.systemui.statusbar.notification.NotificationUtils;
@@ -120,7 +121,6 @@
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape;
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView;
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
-import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
import com.android.systemui.statusbar.policy.HeadsUpUtil;
import com.android.systemui.statusbar.policy.ScrollAdapter;
@@ -740,6 +740,15 @@
updateFooter();
}
+ void sendRemoteInputRowBottomBound(Float bottom) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ if (bottom != null) {
+ bottom += getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_content_margin);
+ }
+ mScrollViewFields.sendRemoteInputRowBottomBound(bottom);
+ }
+
/** Setter for filtered notifs, to be removed with the FooterViewRefactor flag. */
public void setHasFilteredOutSeenNotifications(boolean hasFilteredOutSeenNotifications) {
FooterViewRefactor.assertInLegacyMode();
@@ -1274,6 +1283,11 @@
}
@Override
+ public void setRemoteInputRowBottomBoundConsumer(@Nullable Consumer<Float> consumer) {
+ mScrollViewFields.setRemoteInputRowBottomBoundConsumer(consumer);
+ }
+
+ @Override
public void setHeadsUpHeightConsumer(@Nullable Consumer<Float> consumer) {
mScrollViewFields.setHeadsUpHeightConsumer(consumer);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index e5f63c1..dad6894 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -98,6 +98,9 @@
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.ColorUpdateLogger;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
+import com.android.systemui.statusbar.notification.HeadsUpNotificationViewControllerEmptyImpl;
+import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
+import com.android.systemui.statusbar.notification.HeadsUpTouchHelper.HeadsUpNotificationViewController;
import com.android.systemui.statusbar.notification.LaunchAnimationParameters;
import com.android.systemui.statusbar.notification.NotificationActivityStarter;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
@@ -129,9 +132,6 @@
import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix;
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
-import com.android.systemui.statusbar.notification.HeadsUpNotificationViewControllerEmptyImpl;
-import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
-import com.android.systemui.statusbar.notification.HeadsUpTouchHelper.HeadsUpNotificationViewController;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -1605,6 +1605,9 @@
return new RemoteInputController.Delegate() {
public void setRemoteInputActive(NotificationEntry entry,
boolean remoteInputActive) {
+ if (SceneContainerFlag.isEnabled()) {
+ sendRemoteInputRowBottomBound(entry, remoteInputActive);
+ }
mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive);
entry.notifyHeightChanged(true /* needsAnimation */);
if (!FooterViewRefactor.isEnabled()) {
@@ -1620,6 +1623,15 @@
mView.requestDisallowLongPress();
mView.requestDisallowDismiss();
}
+
+ private void sendRemoteInputRowBottomBound(NotificationEntry entry,
+ boolean remoteInputActive) {
+ ExpandableNotificationRow row = entry.getRow();
+ float top = row.getTranslationY();
+ int height = row.getActualHeight();
+ float bottom = top + height + row.getRemoteInputActionsContainerExpandedOffset();
+ mView.sendRemoteInputRowBottomBound(remoteInputActive ? bottom : null);
+ }
};
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
index aa39539..c08ed61 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
@@ -57,6 +57,13 @@
* guts off of this gesture, we can notify the placeholder through here.
*/
var currentGestureInGutsConsumer: Consumer<Boolean>? = null
+
+ /**
+ * When a notification begins remote input, its bottom Y bound is sent to the placeholder
+ * through here in order to adjust to accommodate the IME.
+ */
+ var remoteInputRowBottomBoundConsumer: Consumer<Float?>? = null
+
/**
* Any time the heads up height is recalculated, it should be updated here to be used by the
* placeholder
@@ -75,6 +82,10 @@
fun sendCurrentGestureInGuts(isCurrentGestureInGuts: Boolean) =
currentGestureInGutsConsumer?.accept(isCurrentGestureInGuts)
+ /** send [bottomY] to the [remoteInputRowBottomBoundConsumer], if present. */
+ fun sendRemoteInputRowBottomBound(bottomY: Float?) =
+ remoteInputRowBottomBoundConsumer?.accept(bottomY)
+
/** send the [headsUpHeight] to the [headsUpHeightConsumer], if present. */
fun sendHeadsUpHeight(headsUpHeight: Float) = headsUpHeightConsumer?.accept(headsUpHeight)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index 235b4da..41c0293 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -74,6 +74,9 @@
/** Set a consumer for current gesture in guts events */
fun setCurrentGestureInGutsConsumer(consumer: Consumer<Boolean>?)
+ /** Set a consumer for current remote input notification row bottom bound events */
+ fun setRemoteInputRowBottomBoundConsumer(consumer: Consumer<Float?>?)
+
/** Set a consumer for heads up height changed events */
fun setHeadsUpHeightConsumer(consumer: Consumer<Float>?)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
index 6d5553f..2e37dea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -108,10 +108,14 @@
view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer)
view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer)
view.setCurrentGestureInGutsConsumer(viewModel.currentGestureInGutsConsumer)
+ view.setRemoteInputRowBottomBoundConsumer(
+ viewModel.remoteInputRowBottomBoundConsumer
+ )
DisposableHandle {
view.setSyntheticScrollConsumer(null)
view.setCurrentGestureOverscrollConsumer(null)
view.setCurrentGestureInGutsConsumer(null)
+ view.setRemoteInputRowBottomBoundConsumer(null)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 8d7007b..5b2e02d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -31,6 +31,7 @@
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimClipping
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
@@ -56,6 +57,7 @@
dumpManager: DumpManager,
stackAppearanceInteractor: NotificationStackAppearanceInteractor,
shadeInteractor: ShadeInteractor,
+ private val remoteInputInteractor: RemoteInputInteractor,
private val sceneInteractor: SceneInteractor,
// TODO(b/336364825) Remove Lazy when SceneContainerFlag is released -
// while the flag is off, creating this object too early results in a crash
@@ -240,6 +242,10 @@
val currentGestureInGutsConsumer: (Boolean) -> Unit =
stackAppearanceInteractor::setCurrentGestureInGuts
+ /** Receives the bottom bound of the currently focused remote input notification row. */
+ val remoteInputRowBottomBoundConsumer: (Float?) -> Unit =
+ remoteInputInteractor::setRemoteInputRowBottomBound
+
/** Whether the notification stack is scrollable or not. */
val isScrollable: Flow<Boolean> =
combine(sceneInteractor.currentScene, sceneInteractor.currentOverlays) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index 69c1bf3..c8e8358 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -24,6 +24,7 @@
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
@@ -49,6 +50,7 @@
private val sceneInteractor: SceneInteractor,
private val shadeInteractor: ShadeInteractor,
private val headsUpNotificationInteractor: HeadsUpNotificationInteractor,
+ remoteInputInteractor: RemoteInputInteractor,
featureFlags: FeatureFlagsClassic,
dumpManager: DumpManager,
) :
@@ -132,6 +134,12 @@
val isCurrentGestureOverscroll: Flow<Boolean> =
interactor.isCurrentGestureOverscroll.dumpWhileCollecting("isCurrentGestureOverScroll")
+ /** Whether remote input is currently active for any notification. */
+ val isRemoteInputActive = remoteInputInteractor.isRemoteInputActive
+
+ /** The bottom bound of the currently focused remote input notification row. */
+ val remoteInputRowBottomBound = remoteInputInteractor.remoteInputRowBottomBound
+
/** Sets whether the notification stack is scrolled to the top. */
fun setScrolledToTop(scrolledToTop: Boolean) {
interactor.setScrolledToTop(scrolledToTop)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 31776cf..16d5f8d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -106,7 +106,7 @@
private static final long FOCUS_ANIMATION_CROSSFADE_DURATION = 50;
private static final long FOCUS_ANIMATION_FADE_IN_DELAY = 33;
private static final long FOCUS_ANIMATION_FADE_IN_DURATION = 83;
- private static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f;
+ public static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f;
private static final long DEFOCUS_ANIMATION_FADE_OUT_DELAY = 120;
private static final long DEFOCUS_ANIMATION_CROSSFADE_DELAY = 180;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index dbeaa59..ba45942 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -27,7 +27,10 @@
import com.android.settingslib.notification.modes.ZenIconLoader
import com.android.settingslib.notification.modes.ZenMode
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.modes.shared.ModesUi
import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
+import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository
+import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes
import com.android.systemui.statusbar.policy.domain.model.ZenModeInfo
import java.time.Duration
@@ -51,7 +54,17 @@
private val notificationSettingsRepository: NotificationSettingsRepository,
@Background private val bgDispatcher: CoroutineDispatcher,
private val iconLoader: ZenIconLoader,
+ private val deviceProvisioningRepository: DeviceProvisioningRepository,
+ private val userSetupRepository: UserSetupRepository,
) {
+ val isZenAvailable: Flow<Boolean> =
+ combine(
+ deviceProvisioningRepository.isDeviceProvisioned,
+ userSetupRepository.isUserSetUp,
+ ) { isDeviceProvisioned, isUserSetUp ->
+ isDeviceProvisioned && isUserSetUp
+ }
+
val isZenModeEnabled: Flow<Boolean> =
zenModeRepository.globalZenMode
.map {
@@ -80,6 +93,18 @@
val modes: Flow<List<ZenMode>> = zenModeRepository.modes
+ /**
+ * Returns the special "manual DND" mode.
+ *
+ * This is only meant as a temporary solution for "legacy" UI pieces that handle DND
+ * specifically; any new or migrated features should use modes more generally, through [modes]
+ * or [activeModes].
+ */
+ val dndMode: Flow<ZenMode?> by lazy {
+ ModesUi.assertInNewMode()
+ zenModeRepository.modes.map { modes -> modes.singleOrNull { it.isManualDnd } }
+ }
+
/** Flow returning the currently active mode(s), if any. */
val activeModes: Flow<ActiveZenModes> =
modes
@@ -113,10 +138,11 @@
Log.e(
TAG,
"Interactor cannot handle showing the zen duration prompt. " +
- "Please use EnableZenModeDialog when this setting is active."
+ "Please use EnableZenModeDialog when this setting is active.",
)
null
}
+
ZEN_DURATION_FOREVER -> null
else -> Duration.ofMinutes(zenDuration.toLong())
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
index af93880..27bc6d3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
@@ -59,32 +59,26 @@
)
CompositionLocalProvider(LocalContentColor provides contentColor) {
- Surface(
- color = tileColor,
- shape = RoundedCornerShape(16.dp),
- ) {
+ Surface(color = tileColor, shape = RoundedCornerShape(16.dp)) {
Row(
modifier =
Modifier.combinedClickable(
onClick = viewModel.onClick,
onLongClick = viewModel.onLongClick,
- onLongClickLabel = viewModel.onLongClickLabel
+ onLongClickLabel = viewModel.onLongClickLabel,
)
- .padding(20.dp)
+ .padding(16.dp)
.semantics { stateDescription = viewModel.stateDescription },
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement =
- Arrangement.spacedBy(
- space = 10.dp,
- alignment = Alignment.Start,
- ),
+ Arrangement.spacedBy(space = 8.dp, alignment = Alignment.Start),
) {
Icon(icon = viewModel.icon, modifier = Modifier.size(24.dp))
Column {
Text(
viewModel.text,
fontWeight = FontWeight.W500,
- modifier = Modifier.tileMarquee().testTag("name")
+ modifier = Modifier.tileMarquee().testTag("name"),
)
Text(
viewModel.subtext,
@@ -94,7 +88,7 @@
.testTag(if (viewModel.enabled) "stateOn" else "stateOff")
.clearAndSetSemantics {
contentDescription = viewModel.subtextDescription
- }
+ },
)
}
}
@@ -103,8 +97,5 @@
}
private fun Modifier.tileMarquee(): Modifier {
- return this.basicMarquee(
- iterations = 1,
- initialDelayMillis = 200,
- )
+ return this.basicMarquee(iterations = 1)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt
index 73d361f6..5953ea5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt
@@ -19,7 +19,6 @@
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.runtime.Composable
@@ -27,23 +26,20 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.systemui.Flags
import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel
@Composable
fun ModeTileGrid(viewModel: ModesDialogViewModel) {
val tiles by viewModel.tiles.collectAsStateWithLifecycle(initialValue = emptyList())
- // TODO(b/346519570): Handle what happens when we have more than a few modes.
LazyVerticalGrid(
- columns = GridCells.Fixed(2),
- modifier = Modifier.padding(8.dp).fillMaxWidth().heightIn(max = 300.dp),
+ columns = GridCells.Fixed(if (Flags.modesDialogSingleRows()) 1 else 2),
+ modifier = Modifier.fillMaxWidth().heightIn(max = 300.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
- items(
- tiles.size,
- key = { index -> tiles[index].id },
- ) { index ->
+ items(tiles.size, key = { index -> tiles[index].id }) { index ->
ModeTile(viewModel = tiles[index])
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index db4f9ef..7166428 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -35,7 +35,6 @@
import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL;
import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder;
import static com.android.settingslib.flags.Flags.volumeDialogAudioSharingFix;
-import static com.android.systemui.Flags.hapticVolumeSlider;
import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED;
import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED;
@@ -928,10 +927,8 @@
}
private void addSliderHapticsToRow(VolumeRow row) {
- if (hapticVolumeSlider()) {
- row.createPlugin(mVibratorHelper, mSystemClock);
- HapticSliderViewBinder.bind(row.slider, row.mHapticPlugin);
- }
+ row.createPlugin(mVibratorHelper, mSystemClock);
+ HapticSliderViewBinder.bind(row.slider, row.mHapticPlugin);
}
@VisibleForTesting void addSliderHapticsToRows() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
index c65a117..d72b72c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
@@ -32,6 +32,7 @@
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.os.PersistableBundle;
+import android.os.UserHandle;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.provider.Settings;
@@ -101,8 +102,18 @@
when(mClipboardManager.getPrimaryClip()).thenReturn(mSampleClipData);
when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource);
- mClipboardListener = new ClipboardListener(getContext(), mOverlayControllerProvider,
- mClipboardToast, mClipboardManager, mKeyguardManager, mUiEventLogger);
+ mClipboardListener = new ClipboardListener(
+ getContext(),
+ mOverlayControllerProvider,
+ mClipboardToast,
+ user -> {
+ if (UserHandle.CURRENT.equals(user)) {
+ return mClipboardManager;
+ }
+ return null;
+ },
+ mKeyguardManager,
+ mUiEventLogger);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 411ff91..8731853 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -77,7 +77,8 @@
private static final int TEST_CURRENT_VOLUME = 10;
// Mock
- private MediaOutputController mMediaOutputController = mock(MediaOutputController.class);
+ private MediaSwitchingController mMediaSwitchingController =
+ mock(MediaSwitchingController.class);
private MediaOutputDialog mMediaOutputDialog = mock(MediaOutputDialog.class);
private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
@@ -95,13 +96,13 @@
@Before
public void setUp() {
- when(mMediaOutputController.getMediaItemList()).thenReturn(mMediaItems);
- when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false);
- when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false);
- when(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat);
- when(mMediaOutputController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat);
- when(mMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice1);
- when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(true);
+ when(mMediaSwitchingController.getMediaItemList()).thenReturn(mMediaItems);
+ when(mMediaSwitchingController.hasAdjustVolumeUserRestriction()).thenReturn(false);
+ when(mMediaSwitchingController.isAnyDeviceTransferring()).thenReturn(false);
+ when(mMediaSwitchingController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat);
+ when(mMediaSwitchingController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat);
+ when(mMediaSwitchingController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice1);
+ when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(true);
when(mIconCompat.toIcon(mContext)).thenReturn(mIcon);
when(mMediaDevice1.getName()).thenReturn(TEST_DEVICE_NAME_1);
when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_ID_1);
@@ -116,7 +117,7 @@
mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice1));
mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice2));
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+ mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -142,7 +143,7 @@
@Test
public void onBindViewHolder_bindPairNew_verifyView() {
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+ mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -161,11 +162,13 @@
@Test
public void onBindViewHolder_bindGroup_withSessionName_verifyView() {
- when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(
- mMediaItems.stream().map((item) -> item.getMediaDevice().get()).collect(
- Collectors.toList()));
- when(mMediaOutputController.getSessionName()).thenReturn(TEST_SESSION_NAME);
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+ when(mMediaSwitchingController.getSelectedMediaDevice())
+ .thenReturn(
+ mMediaItems.stream()
+ .map((item) -> item.getMediaDevice().get())
+ .collect(Collectors.toList()));
+ when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME);
+ mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -181,11 +184,13 @@
@Test
public void onBindViewHolder_bindGroup_noSessionName_verifyView() {
- when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(
- mMediaItems.stream().map((item) -> item.getMediaDevice().get()).collect(
- Collectors.toList()));
- when(mMediaOutputController.getSessionName()).thenReturn(null);
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+ when(mMediaSwitchingController.getSelectedMediaDevice())
+ .thenReturn(
+ mMediaItems.stream()
+ .map((item) -> item.getMediaDevice().get())
+ .collect(Collectors.toList()));
+ when(mMediaSwitchingController.getSessionName()).thenReturn(null);
+ mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -214,7 +219,7 @@
@Test
public void onBindViewHolder_bindNonRemoteConnectedDevice_verifyView() {
- when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false);
+ when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false);
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -230,9 +235,9 @@
@Test
public void onBindViewHolder_bindConnectedRemoteDevice_verifyView() {
- when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(
- ImmutableList.of(mMediaDevice2));
- when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true);
+ when(mMediaSwitchingController.getSelectableMediaDevice())
+ .thenReturn(ImmutableList.of(mMediaDevice2));
+ when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -249,9 +254,9 @@
@Test
public void onBindViewHolder_bindConnectedRemoteDevice_verifyContentDescriptionNotNull() {
- when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(
- ImmutableList.of(mMediaDevice2));
- when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true);
+ when(mMediaSwitchingController.getSelectableMediaDevice())
+ .thenReturn(ImmutableList.of(mMediaDevice2));
+ when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -263,9 +268,8 @@
@Test
public void onBindViewHolder_bindSingleConnectedRemoteDevice_verifyView() {
- when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(
- ImmutableList.of());
- when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true);
+ when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of());
+ when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -283,9 +287,8 @@
@Test
public void onBindViewHolder_bindConnectedRemoteDeviceWithOnGoingSession_verifyView() {
when(mMediaDevice1.hasOngoingSession()).thenReturn(true);
- when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(
- ImmutableList.of());
- when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true);
+ when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of());
+ when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -305,9 +308,8 @@
public void onBindViewHolder_bindConnectedRemoteDeviceWithHostOnGoingSession_verifyView() {
when(mMediaDevice1.hasOngoingSession()).thenReturn(true);
when(mMediaDevice1.isHostForOngoingSession()).thenReturn(true);
- when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(
- ImmutableList.of());
- when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true);
+ when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of());
+ when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -326,8 +328,8 @@
@Test
public void onBindViewHolder_bindConnectedDeviceWithMutingExpectedDeviceExist_verifyView() {
- when(mMediaOutputController.hasMutingExpectedDevice()).thenReturn(true);
- when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false);
+ when(mMediaSwitchingController.hasMutingExpectedDevice()).thenReturn(true);
+ when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(false);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
@@ -340,8 +342,8 @@
@Test
public void onBindViewHolder_isMutingExpectedDevice_verifyView() {
when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(true);
- when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false);
- when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false);
+ when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(false);
+ when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false);
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -378,14 +380,14 @@
mOnSeekBarChangeListenerCaptor.getValue().onStopTrackingTouch(mViewHolder.mSeekBar);
assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
- verify(mMediaOutputController).logInteractionAdjustVolume(mMediaDevice1);
+ verify(mMediaSwitchingController).logInteractionAdjustVolume(mMediaDevice1);
}
@Test
public void onBindViewHolder_bindSelectableDevice_verifyView() {
List<MediaDevice> selectableDevices = new ArrayList<>();
selectableDevices.add(mMediaDevice2);
- when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices);
+ when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectableDevices);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
@@ -440,7 +442,7 @@
@Test
public void subStatusSupported_onBindViewHolder_bindHostDeviceWithOngoingSession_verifyView() {
- when(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true);
+ when(mMediaSwitchingController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true);
when(mMediaDevice1.isHostForOngoingSession()).thenReturn(true);
when(mMediaDevice1.hasSubtext()).thenReturn(true);
when(mMediaDevice1.getSubtext()).thenReturn(SUBTEXT_CUSTOM);
@@ -540,7 +542,7 @@
@Test
public void onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() {
- when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true);
+ when(mMediaSwitchingController.isAnyDeviceTransferring()).thenReturn(true);
when(mMediaDevice1.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -556,7 +558,7 @@
@Test
public void onBindViewHolder_bindGroupingDevice_verifyView() {
- when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false);
+ when(mMediaSwitchingController.isAnyDeviceTransferring()).thenReturn(false);
when(mMediaDevice1.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_GROUPING);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -572,7 +574,7 @@
@Test
public void onBindViewHolder_inTransferring_bindNonTransferringDevice_verifyView() {
- when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true);
+ when(mMediaSwitchingController.isAnyDeviceTransferring()).thenReturn(true);
when(mMediaDevice2.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -586,7 +588,7 @@
@Test
public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() {
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+ mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -595,16 +597,16 @@
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2);
mViewHolder.mContainerLayout.performClick();
- verify(mMediaOutputController).launchBluetoothPairing(mViewHolder.mContainerLayout);
+ verify(mMediaSwitchingController).launchBluetoothPairing(mViewHolder.mContainerLayout);
}
@Test
public void onItemClick_clickDevice_verifyConnectDevice() {
- when(mMediaOutputController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false);
+ when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false);
assertThat(mMediaDevice2.getState()).isEqualTo(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER);
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+ mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -613,16 +615,16 @@
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
mViewHolder.mContainerLayout.performClick();
- verify(mMediaOutputController).connectDevice(mMediaDevice2);
+ verify(mMediaSwitchingController).connectDevice(mMediaDevice2);
}
@Test
public void onItemClick_clickDeviceWithSessionOngoing_verifyShowsDialog() {
- when(mMediaOutputController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(true);
+ when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(true);
assertThat(mMediaDevice2.getState()).isEqualTo(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER);
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+ mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -633,66 +635,68 @@
mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 1);
spyMediaDeviceViewHolder.mContainerLayout.performClick();
- verify(mMediaOutputController, never()).connectDevice(mMediaDevice2);
+ verify(mMediaSwitchingController, never()).connectDevice(mMediaDevice2);
verify(spyMediaDeviceViewHolder).showCustomEndSessionDialog(mMediaDevice2);
}
@Test
public void onItemClick_clicksWithMutingExpectedDeviceExist_cancelsMuteAwaitConnection() {
- when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false);
- when(mMediaOutputController.hasMutingExpectedDevice()).thenReturn(true);
- when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false);
+ when(mMediaSwitchingController.isAnyDeviceTransferring()).thenReturn(false);
+ when(mMediaSwitchingController.hasMutingExpectedDevice()).thenReturn(true);
+ when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(false);
when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(false);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
mViewHolder.mContainerLayout.performClick();
- verify(mMediaOutputController).cancelMuteAwaitConnection();
+ verify(mMediaSwitchingController).cancelMuteAwaitConnection();
}
@Test
public void onGroupActionTriggered_clicksEndAreaOfSelectableDevice_triggerGrouping() {
List<MediaDevice> selectableDevices = new ArrayList<>();
selectableDevices.add(mMediaDevice2);
- when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices);
+ when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectableDevices);
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
mViewHolder.mEndTouchArea.performClick();
- verify(mMediaOutputController).addDeviceToPlayMedia(mMediaDevice2);
+ verify(mMediaSwitchingController).addDeviceToPlayMedia(mMediaDevice2);
}
@Test
public void onGroupActionTriggered_clickSelectedRemoteDevice_triggerUngrouping() {
- when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(
- ImmutableList.of(mMediaDevice2));
- when(mMediaOutputController.getDeselectableMediaDevice()).thenReturn(
- ImmutableList.of(mMediaDevice1));
- when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true);
+ when(mMediaSwitchingController.getSelectableMediaDevice())
+ .thenReturn(ImmutableList.of(mMediaDevice2));
+ when(mMediaSwitchingController.getDeselectableMediaDevice())
+ .thenReturn(ImmutableList.of(mMediaDevice1));
+ when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
mViewHolder.mEndTouchArea.performClick();
- verify(mMediaOutputController).removeDeviceFromPlayMedia(mMediaDevice1);
+ verify(mMediaSwitchingController).removeDeviceFromPlayMedia(mMediaDevice1);
}
@Test
public void onItemClick_onGroupActionTriggered_verifySeekbarDisabled() {
- when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(
- mMediaItems.stream().map((item) -> item.getMediaDevice().get()).collect(
- Collectors.toList()));
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+ when(mMediaSwitchingController.getSelectedMediaDevice())
+ .thenReturn(
+ mMediaItems.stream()
+ .map((item) -> item.getMediaDevice().get())
+ .collect(Collectors.toList()));
+ mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
List<MediaDevice> selectableDevices = new ArrayList<>();
selectableDevices.add(mMediaDevice1);
- when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices);
- when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(true);
+ when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectableDevices);
+ when(mMediaSwitchingController.hasAdjustVolumeUserRestriction()).thenReturn(true);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
mViewHolder.mContainerLayout.performClick();
@@ -702,11 +706,11 @@
@Test
public void onBindViewHolder_volumeControlChangeToEnabled_enableSeekbarAgain() {
- when(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(false);
+ when(mMediaSwitchingController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(false);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
assertThat(mViewHolder.mSeekBar.isEnabled()).isFalse();
- when(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true);
+ when(mMediaSwitchingController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
assertThat(mViewHolder.mSeekBar.isEnabled()).isTrue();
@@ -719,7 +723,7 @@
mMediaOutputAdapter.updateColorScheme(wallpaperColors, true);
- verify(mMediaOutputController).setCurrentColorScheme(wallpaperColors, true);
+ verify(mMediaSwitchingController).setCurrentColorScheme(wallpaperColors, true);
}
@Test
@@ -727,7 +731,7 @@
mMediaOutputAdapter.updateItems();
List<MediaItem> updatedList = new ArrayList<>();
updatedList.add(MediaItem.createPairNewDeviceMediaItem());
- when(mMediaOutputController.getMediaItemList()).thenReturn(updatedList);
+ when(mMediaSwitchingController.getMediaItemList()).thenReturn(updatedList);
assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaItems.size());
mMediaOutputAdapter.updateItems();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index c8cc6b5..47371df 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -104,7 +104,7 @@
private List<MediaController> mMediaControllers = new ArrayList<>();
private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
- private MediaOutputController mMediaOutputController;
+ private MediaSwitchingController mMediaSwitchingController;
private int mHeaderIconRes;
private IconCompat mIconCompat;
private CharSequence mHeaderTitle;
@@ -132,8 +132,8 @@
VolumePanelGlobalStateInteractorKosmosKt.getVolumePanelGlobalStateInteractor(
mKosmos);
- mMediaOutputController =
- new MediaOutputController(
+ mMediaSwitchingController =
+ new MediaSwitchingController(
mContext,
TEST_PACKAGE,
mContext.getUser(),
@@ -153,12 +153,13 @@
// Using a fake package will cause routing operations to fail, so we intercept
// scanning-related operations.
- mMediaOutputController.mLocalMediaManager = mock(LocalMediaManager.class);
- doNothing().when(mMediaOutputController.mLocalMediaManager).startScan();
- doNothing().when(mMediaOutputController.mLocalMediaManager).stopScan();
+ mMediaSwitchingController.mLocalMediaManager = mock(LocalMediaManager.class);
+ doNothing().when(mMediaSwitchingController.mLocalMediaManager).startScan();
+ doNothing().when(mMediaSwitchingController.mLocalMediaManager).stopScan();
- mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext, mBroadcastSender,
- mMediaOutputController);
+ mMediaOutputBaseDialogImpl =
+ new MediaOutputBaseDialogImpl(
+ mContext, mBroadcastSender, mMediaSwitchingController);
mMediaOutputBaseDialogImpl.onCreate(new Bundle());
}
@@ -176,7 +177,7 @@
public void refresh_withIconCompat_iconIsVisible() {
mIconCompat = IconCompat.createWithBitmap(
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888));
- when(mMediaOutputBaseAdapter.getController()).thenReturn(mMediaOutputController);
+ when(mMediaOutputBaseAdapter.getController()).thenReturn(mMediaSwitchingController);
mMediaOutputBaseDialogImpl.refresh();
final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
@@ -263,7 +264,7 @@
when(mMediaOutputBaseAdapter.isDragging()).thenReturn(true);
mMediaOutputBaseDialogImpl.refresh();
- assertThat(mMediaOutputController.isRefreshing()).isFalse();
+ assertThat(mMediaSwitchingController.isRefreshing()).isFalse();
}
@Test
@@ -335,12 +336,14 @@
class MediaOutputBaseDialogImpl extends MediaOutputBaseDialog {
- MediaOutputBaseDialogImpl(Context context, BroadcastSender broadcastSender,
- MediaOutputController mediaOutputController) {
+ MediaOutputBaseDialogImpl(
+ Context context,
+ BroadcastSender broadcastSender,
+ MediaSwitchingController mediaSwitchingController) {
super(
context,
broadcastSender,
- mediaOutputController, /* includePlaybackAndAppMetadata */
+ mediaSwitchingController, /* includePlaybackAndAppMetadata */
true);
mAdapter = mMediaOutputBaseAdapter;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
index 189a561..f0902e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
@@ -119,7 +119,7 @@
private UserTracker mUserTracker = mock(UserTracker.class);
private MediaOutputBroadcastDialog mMediaOutputBroadcastDialog;
- private MediaOutputController mMediaOutputController;
+ private MediaSwitchingController mMediaSwitchingController;
@Before
public void setUp() {
@@ -133,8 +133,8 @@
VolumePanelGlobalStateInteractorKosmosKt.getVolumePanelGlobalStateInteractor(
mKosmos);
- mMediaOutputController =
- new MediaOutputController(
+ mMediaSwitchingController =
+ new MediaSwitchingController(
mContext,
TEST_PACKAGE,
mContext.getUser(),
@@ -151,9 +151,10 @@
mFlags,
volumePanelGlobalStateInteractor,
mUserTracker);
- mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
- mMediaOutputBroadcastDialog = new MediaOutputBroadcastDialog(mContext, false,
- mBroadcastSender, mMediaOutputController);
+ mMediaSwitchingController.mLocalMediaManager = mLocalMediaManager;
+ mMediaOutputBroadcastDialog =
+ new MediaOutputBroadcastDialog(
+ mContext, false, mBroadcastSender, mMediaSwitchingController);
mMediaOutputBroadcastDialog.show();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index 90c2930..d3ecb3d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -119,7 +119,7 @@
private List<MediaController> mMediaControllers = new ArrayList<>();
private MediaOutputDialog mMediaOutputDialog;
- private MediaOutputController mMediaOutputController;
+ private MediaSwitchingController mMediaSwitchingController;
private final List<String> mFeatures = new ArrayList<>();
@Override
@@ -146,8 +146,8 @@
VolumePanelGlobalStateInteractorKosmosKt.getVolumePanelGlobalStateInteractor(
mKosmos);
- mMediaOutputController =
- new MediaOutputController(
+ mMediaSwitchingController =
+ new MediaSwitchingController(
mContext,
TEST_PACKAGE,
mContext.getUser(),
@@ -164,8 +164,8 @@
mFlags,
volumePanelGlobalStateInteractor,
mUserTracker);
- mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
- mMediaOutputDialog = makeTestDialog(mMediaOutputController);
+ mMediaSwitchingController.mLocalMediaManager = mLocalMediaManager;
+ mMediaOutputDialog = makeTestDialog(mMediaSwitchingController);
mMediaOutputDialog.show();
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice);
@@ -388,12 +388,15 @@
public void getStopButtonText_notSupportsBroadcast_returnsDefaultText() {
String stopText = mContext.getText(
R.string.media_output_dialog_button_stop_casting).toString();
- MediaOutputController mockMediaOutputController = mock(MediaOutputController.class);
- when(mockMediaOutputController.isBroadcastSupported()).thenReturn(false);
+ MediaSwitchingController mockMediaSwitchingController =
+ mock(MediaSwitchingController.class);
+ when(mockMediaSwitchingController.isBroadcastSupported()).thenReturn(false);
- withTestDialog(mockMediaOutputController, testDialog -> {
- assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText);
- });
+ withTestDialog(
+ mockMediaSwitchingController,
+ testDialog -> {
+ assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText);
+ });
}
@Test
@@ -401,28 +404,35 @@
public void getStopButtonText_supportsBroadcast_returnsBroadcastText() {
String stopText = mContext.getText(R.string.media_output_broadcast).toString();
MediaDevice mMediaDevice = mock(MediaDevice.class);
- MediaOutputController mockMediaOutputController = mock(MediaOutputController.class);
- when(mockMediaOutputController.isBroadcastSupported()).thenReturn(true);
- when(mockMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice);
- when(mockMediaOutputController.isBluetoothLeDevice(any())).thenReturn(true);
- when(mockMediaOutputController.isPlaying()).thenReturn(true);
- when(mockMediaOutputController.isBluetoothLeBroadcastEnabled()).thenReturn(false);
- withTestDialog(mockMediaOutputController, testDialog -> {
- assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText);
- });
+ MediaSwitchingController mockMediaSwitchingController =
+ mock(MediaSwitchingController.class);
+ when(mockMediaSwitchingController.isBroadcastSupported()).thenReturn(true);
+ when(mockMediaSwitchingController.getCurrentConnectedMediaDevice())
+ .thenReturn(mMediaDevice);
+ when(mockMediaSwitchingController.isBluetoothLeDevice(any())).thenReturn(true);
+ when(mockMediaSwitchingController.isPlaying()).thenReturn(true);
+ when(mockMediaSwitchingController.isBluetoothLeBroadcastEnabled()).thenReturn(false);
+ withTestDialog(
+ mockMediaSwitchingController,
+ testDialog -> {
+ assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText);
+ });
}
@Test
public void onStopButtonClick_notPlaying_releaseSession() {
- MediaOutputController mockMediaOutputController = mock(MediaOutputController.class);
- when(mockMediaOutputController.isBroadcastSupported()).thenReturn(false);
- when(mockMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(null);
- when(mockMediaOutputController.isPlaying()).thenReturn(false);
- withTestDialog(mockMediaOutputController, testDialog -> {
- testDialog.onStopButtonClick();
- });
+ MediaSwitchingController mockMediaSwitchingController =
+ mock(MediaSwitchingController.class);
+ when(mockMediaSwitchingController.isBroadcastSupported()).thenReturn(false);
+ when(mockMediaSwitchingController.getCurrentConnectedMediaDevice()).thenReturn(null);
+ when(mockMediaSwitchingController.isPlaying()).thenReturn(false);
+ withTestDialog(
+ mockMediaSwitchingController,
+ testDialog -> {
+ testDialog.onStopButtonClick();
+ });
- verify(mockMediaOutputController).releaseSession();
+ verify(mockMediaSwitchingController).releaseSession();
verify(mDialogTransitionAnimator).disableAllCurrentDialogsExitAnimations();
}
@@ -430,14 +440,14 @@
// Check the visibility metric logging by creating a new MediaOutput dialog,
// and verify if the calling times increases.
public void onCreate_ShouldLogVisibility() {
- withTestDialog(mMediaOutputController, testDialog -> {});
+ withTestDialog(mMediaSwitchingController, testDialog -> {});
verify(mUiEventLogger, times(2))
.log(MediaOutputDialog.MediaOutputEvent.MEDIA_OUTPUT_DIALOG_SHOW);
}
@NonNull
- private MediaOutputDialog makeTestDialog(MediaOutputController controller) {
+ private MediaOutputDialog makeTestDialog(MediaSwitchingController controller) {
return new MediaOutputDialog(
mContext,
false,
@@ -448,7 +458,8 @@
true);
}
- private void withTestDialog(MediaOutputController controller, Consumer<MediaOutputDialog> c) {
+ private void withTestDialog(
+ MediaSwitchingController controller, Consumer<MediaOutputDialog> c) {
MediaOutputDialog testDialog = makeTestDialog(controller);
testDialog.show();
c.accept(testDialog);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
similarity index 75%
rename from packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
index 714fad9..d3e20c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
@@ -43,6 +43,7 @@
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.MediaDescription;
import android.media.MediaMetadata;
@@ -58,6 +59,7 @@
import android.os.PowerExemptionManager;
import android.os.RemoteException;
import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
import android.service.notification.StatusBarNotification;
import android.testing.TestableLooper;
import android.text.TextUtils;
@@ -67,8 +69,10 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.media.flags.Flags;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.media.InputMediaDevice;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.SysuiTestCase;
@@ -101,7 +105,7 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class MediaOutputControllerTest extends SysuiTestCase {
+public class MediaSwitchingControllerTest extends SysuiTestCase {
private static final String TEST_DEVICE_1_ID = "test_device_1_id";
private static final String TEST_DEVICE_2_ID = "test_device_2_id";
private static final String TEST_DEVICE_3_ID = "test_device_3_id";
@@ -126,8 +130,7 @@
private CachedBluetoothDeviceManager mCachedBluetoothDeviceManager;
@Mock
private LocalBluetoothManager mLocalBluetoothManager;
- @Mock
- private MediaOutputController.Callback mCb;
+ @Mock private MediaSwitchingController.Callback mCb;
@Mock
private MediaDevice mMediaDevice1;
@Mock
@@ -166,7 +169,8 @@
private FeatureFlags mFlags = mock(FeatureFlags.class);
private View mDialogLaunchView = mock(View.class);
- private MediaOutputController.Callback mCallback = mock(MediaOutputController.Callback.class);
+ private MediaSwitchingController.Callback mCallback =
+ mock(MediaSwitchingController.Callback.class);
final Notification mNotification = mock(Notification.class);
private final VolumePanelGlobalStateInteractor mVolumePanelGlobalStateInteractor =
@@ -175,7 +179,7 @@
private Context mSpyContext;
private String mPackageName = null;
- private MediaOutputController mMediaOutputController;
+ private MediaSwitchingController mMediaSwitchingController;
private LocalMediaManager mLocalMediaManager;
private List<MediaController> mMediaControllers = new ArrayList<>();
private List<MediaDevice> mMediaDevices = new ArrayList<>();
@@ -203,9 +207,8 @@
when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(
mCachedBluetoothDeviceManager);
-
- mMediaOutputController =
- new MediaOutputController(
+ mMediaSwitchingController =
+ new MediaSwitchingController(
mSpyContext,
mPackageName,
mContext.getUser(),
@@ -222,9 +225,9 @@
mFlags,
mVolumePanelGlobalStateInteractor,
mUserTracker);
- mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
+ mLocalMediaManager = spy(mMediaSwitchingController.mLocalMediaManager);
when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(false);
- mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
+ mMediaSwitchingController.mLocalMediaManager = mLocalMediaManager;
MediaDescription.Builder builder = new MediaDescription.Builder();
builder.setTitle(TEST_SONG);
builder.setSubtitle(TEST_ARTIST);
@@ -264,26 +267,26 @@
@Test
public void start_verifyLocalMediaManagerInit() {
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
- verify(mLocalMediaManager).registerCallback(mMediaOutputController);
+ verify(mLocalMediaManager).registerCallback(mMediaSwitchingController);
verify(mLocalMediaManager).startScan();
}
@Test
public void stop_verifyLocalMediaManagerDeinit() {
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mLocalMediaManager);
- mMediaOutputController.stop();
+ mMediaSwitchingController.stop();
- verify(mLocalMediaManager).unregisterCallback(mMediaOutputController);
+ verify(mLocalMediaManager).unregisterCallback(mMediaSwitchingController);
verify(mLocalMediaManager).stopScan();
}
@Test
public void start_notificationNotFound_mediaControllerInitFromSession() {
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
verify(mSessionMediaController).registerCallback(any());
}
@@ -291,7 +294,7 @@
@Test
public void start_MediaNotificationFound_mediaControllerNotInitFromSession() {
when(mNotification.isMediaNotification()).thenReturn(true);
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
verify(mSessionMediaController, never()).registerCallback(any());
verifyZeroInteractions(mMediaSessionManager);
@@ -299,8 +302,8 @@
@Test
public void start_withoutPackageName_verifyMediaControllerInit() {
- mMediaOutputController =
- new MediaOutputController(
+ mMediaSwitchingController =
+ new MediaSwitchingController(
mSpyContext,
null,
mContext.getUser(),
@@ -318,32 +321,32 @@
mVolumePanelGlobalStateInteractor,
mUserTracker);
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
verify(mSessionMediaController, never()).registerCallback(any());
}
@Test
public void start_nearbyMediaDevicesManagerNotNull_registersNearbyDevicesCallback() {
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
verify(mNearbyMediaDevicesManager).registerNearbyDevicesCallback(any());
}
@Test
public void stop_withPackageName_verifyMediaControllerDeinit() {
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mSessionMediaController);
- mMediaOutputController.stop();
+ mMediaSwitchingController.stop();
verify(mSessionMediaController).unregisterCallback(any());
}
@Test
public void stop_withoutPackageName_verifyMediaControllerDeinit() {
- mMediaOutputController =
- new MediaOutputController(
+ mMediaSwitchingController =
+ new MediaSwitchingController(
mSpyContext,
null,
mSpyContext.getUser(),
@@ -361,26 +364,26 @@
mVolumePanelGlobalStateInteractor,
mUserTracker);
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
- mMediaOutputController.stop();
+ mMediaSwitchingController.stop();
verify(mSessionMediaController, never()).unregisterCallback(any());
}
@Test
public void stop_nearbyMediaDevicesManagerNotNull_unregistersNearbyDevicesCallback() {
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mSessionMediaController);
- mMediaOutputController.stop();
+ mMediaSwitchingController.stop();
verify(mNearbyMediaDevicesManager).unregisterNearbyDevicesCallback(any());
}
@Test
public void tryToLaunchMediaApplication_nullIntent_skip() {
- mMediaOutputController.tryToLaunchMediaApplication(mDialogLaunchView);
+ mMediaSwitchingController.tryToLaunchMediaApplication(mDialogLaunchView);
verify(mCb, never()).dismissDialog();
}
@@ -391,9 +394,9 @@
.thenReturn(mController);
Intent intent = new Intent(mPackageName);
doReturn(intent).when(mPackageManager).getLaunchIntentForPackage(mPackageName);
- mMediaOutputController.start(mCallback);
+ mMediaSwitchingController.start(mCallback);
- mMediaOutputController.tryToLaunchMediaApplication(mDialogLaunchView);
+ mMediaSwitchingController.tryToLaunchMediaApplication(mDialogLaunchView);
verify(mStarter).startActivity(any(Intent.class), anyBoolean(),
Mockito.eq(mController));
@@ -403,11 +406,12 @@
public void tryToLaunchInAppRoutingIntent_componentNameNotNull_startActivity() {
when(mDialogTransitionAnimator.createActivityTransitionController(any(View.class)))
.thenReturn(mController);
- mMediaOutputController.start(mCallback);
+ mMediaSwitchingController.start(mCallback);
when(mLocalMediaManager.getLinkedItemComponentName()).thenReturn(
new ComponentName(mPackageName, ""));
- mMediaOutputController.tryToLaunchInAppRoutingIntent(TEST_DEVICE_1_ID, mDialogLaunchView);
+ mMediaSwitchingController.tryToLaunchInAppRoutingIntent(
+ TEST_DEVICE_1_ID, mDialogLaunchView);
verify(mStarter).startActivity(any(Intent.class), anyBoolean(),
Mockito.eq(mController));
@@ -415,9 +419,9 @@
@Test
public void onDevicesUpdated_unregistersNearbyDevicesCallback() throws RemoteException {
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
- mMediaOutputController.onDevicesUpdated(ImmutableList.of());
+ mMediaSwitchingController.onDevicesUpdated(ImmutableList.of());
verify(mNearbyMediaDevicesManager).unregisterNearbyDevicesCallback(any());
}
@@ -425,11 +429,11 @@
@Test
public void onDeviceListUpdate_withNearbyDevices_updatesRangeInformation()
throws RemoteException {
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.onDevicesUpdated(mNearbyDevices);
- mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+ mMediaSwitchingController.onDevicesUpdated(mNearbyDevices);
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
verify(mMediaDevice1).setRangeZone(NearbyDevice.RANGE_FAR);
verify(mMediaDevice2).setRangeZone(NearbyDevice.RANGE_CLOSE);
@@ -438,11 +442,11 @@
@Test
public void onDeviceListUpdate_withNearbyDevices_rankByRangeInformation()
throws RemoteException {
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.onDevicesUpdated(mNearbyDevices);
- mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+ mMediaSwitchingController.onDevicesUpdated(mNearbyDevices);
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
assertThat(mMediaDevices.get(0).getId()).isEqualTo(TEST_DEVICE_1_ID);
}
@@ -451,11 +455,11 @@
public void routeProcessSupport_onDeviceListUpdate_preferenceExist_NotUpdatesRangeInformation()
throws RemoteException {
when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true);
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.onDevicesUpdated(mNearbyDevices);
- mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+ mMediaSwitchingController.onDevicesUpdated(mNearbyDevices);
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
verify(mMediaDevice1, never()).setRangeZone(anyInt());
verify(mMediaDevice2, never()).setRangeZone(anyInt());
@@ -463,7 +467,8 @@
@Test
public void onDeviceListUpdate_verifyDeviceListCallback() {
- // This test relies on mMediaOutputController.start being called while the selected device
+ // This test relies on mMediaSwitchingController.start being called while the selected
+ // device
// list has exactly one item, and that item's id is:
// - Different from both ids in mMediaDevices.
// - Different from the id of the route published by the device under test (usually the
@@ -475,12 +480,12 @@
.when(mLocalMediaManager)
.getSelectedMediaDevice();
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
final List<MediaDevice> devices = new ArrayList<>();
- for (MediaItem item : mMediaOutputController.getMediaItemList()) {
+ for (MediaItem item : mMediaSwitchingController.getMediaItemList()) {
if (item.getMediaDevice().isPresent()) {
devices.add(item.getMediaDevice().get());
}
@@ -488,14 +493,15 @@
assertThat(devices.containsAll(mMediaDevices)).isTrue();
assertThat(devices.size()).isEqualTo(mMediaDevices.size());
- assertThat(mMediaOutputController.getMediaItemList().size()).isEqualTo(
- mMediaDevices.size() + 2);
+ assertThat(mMediaSwitchingController.getMediaItemList().size())
+ .isEqualTo(mMediaDevices.size() + 2);
verify(mCb).onDeviceListChanged();
}
@Test
public void advanced_onDeviceListUpdateWithConnectedDeviceRemote_verifyItemSize() {
- // This test relies on mMediaOutputController.start being called while the selected device
+ // This test relies on mMediaSwitchingController.start being called while the selected
+ // device
// list has exactly one item, and that item's id is:
// - Different from both ids in mMediaDevices.
// - Different from the id of the route published by the device under test (usually the
@@ -510,12 +516,12 @@
when(mMediaDevice1.getFeatures()).thenReturn(
ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK));
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1);
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
final List<MediaDevice> devices = new ArrayList<>();
- for (MediaItem item : mMediaOutputController.getMediaItemList()) {
+ for (MediaItem item : mMediaSwitchingController.getMediaItemList()) {
if (item.getMediaDevice().isPresent()) {
devices.add(item.getMediaDevice().get());
}
@@ -523,23 +529,72 @@
assertThat(devices.containsAll(mMediaDevices)).isTrue();
assertThat(devices.size()).isEqualTo(mMediaDevices.size());
- assertThat(mMediaOutputController.getMediaItemList().size()).isEqualTo(
- mMediaDevices.size() + 1);
+ assertThat(mMediaSwitchingController.getMediaItemList().size())
+ .isEqualTo(mMediaDevices.size() + 1);
verify(mCb).onDeviceListChanged();
}
+ @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+ @Test
+ public void onInputDeviceListUpdate_verifyDeviceListCallback() {
+ AudioDeviceInfo[] audioDeviceInfos = {};
+ when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS))
+ .thenReturn(audioDeviceInfos);
+ mMediaSwitchingController.start(mCb);
+
+ // Output devices have changed.
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
+
+ final int MAX_VOLUME = 1;
+ final int CURRENT_VOLUME = 0;
+ final boolean IS_VOLUME_FIXED = true;
+ final MediaDevice mediaDevice3 =
+ InputMediaDevice.create(
+ mContext,
+ TEST_DEVICE_3_ID,
+ AudioDeviceInfo.TYPE_BUILTIN_MIC,
+ MAX_VOLUME,
+ CURRENT_VOLUME,
+ IS_VOLUME_FIXED);
+ final MediaDevice mediaDevice4 =
+ InputMediaDevice.create(
+ mContext,
+ TEST_DEVICE_4_ID,
+ AudioDeviceInfo.TYPE_WIRED_HEADSET,
+ MAX_VOLUME,
+ CURRENT_VOLUME,
+ IS_VOLUME_FIXED);
+ final List<MediaDevice> inputDevices = new ArrayList<>();
+ inputDevices.add(mediaDevice3);
+ inputDevices.add(mediaDevice4);
+
+ // Input devices have changed.
+ mMediaSwitchingController.mInputDeviceCallback.onInputDeviceListUpdated(inputDevices);
+
+ final List<MediaDevice> devices = new ArrayList<>();
+ for (MediaItem item : mMediaSwitchingController.getMediaItemList()) {
+ if (item.getMediaDevice().isPresent()) {
+ devices.add(item.getMediaDevice().get());
+ }
+ }
+
+ assertThat(devices).containsAtLeastElementsIn(mMediaDevices);
+ assertThat(devices).hasSize(mMediaDevices.size() + inputDevices.size());
+ verify(mCb, atLeastOnce()).onDeviceListChanged();
+ }
+
@Test
public void advanced_categorizeMediaItems_withSuggestedDevice_verifyDeviceListSize() {
when(mMediaDevice1.isSuggestedDevice()).thenReturn(true);
when(mMediaDevice2.isSuggestedDevice()).thenReturn(false);
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.getMediaItemList().clear();
- mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+ mMediaSwitchingController.getMediaItemList().clear();
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
final List<MediaDevice> devices = new ArrayList<>();
int dividerSize = 0;
- for (MediaItem item : mMediaOutputController.getMediaItemList()) {
+ for (MediaItem item : mMediaSwitchingController.getMediaItemList()) {
if (item.getMediaDevice().isPresent()) {
devices.add(item.getMediaDevice().get());
}
@@ -556,33 +611,33 @@
@Test
public void onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() {
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.mIsRefreshing = true;
+ mMediaSwitchingController.mIsRefreshing = true;
- mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
- assertThat(mMediaOutputController.mNeedRefresh).isTrue();
+ assertThat(mMediaSwitchingController.mNeedRefresh).isTrue();
}
@Test
public void advanced_onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() {
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.mIsRefreshing = true;
+ mMediaSwitchingController.mIsRefreshing = true;
- mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
- assertThat(mMediaOutputController.mNeedRefresh).isTrue();
+ assertThat(mMediaSwitchingController.mNeedRefresh).isTrue();
}
@Test
public void cancelMuteAwaitConnection_cancelsWithMediaManager() {
when(mAudioManager.getMutingExpectedDevice()).thenReturn(mock(AudioDeviceAttributes.class));
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.cancelMuteAwaitConnection();
+ mMediaSwitchingController.cancelMuteAwaitConnection();
verify(mAudioManager).cancelMuteAwaitConnection(any());
}
@@ -590,17 +645,17 @@
@Test
public void cancelMuteAwaitConnection_audioManagerIsNull_noAction() {
when(mAudioManager.getMutingExpectedDevice()).thenReturn(null);
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.cancelMuteAwaitConnection();
+ mMediaSwitchingController.cancelMuteAwaitConnection();
verify(mAudioManager, never()).cancelMuteAwaitConnection(any());
}
@Test
public void getAppSourceName_packageNameIsNull_returnsNull() {
- MediaOutputController testMediaOutputController =
- new MediaOutputController(
+ MediaSwitchingController testMediaSwitchingController =
+ new MediaSwitchingController(
mSpyContext,
"",
mSpyContext.getUser(),
@@ -617,25 +672,25 @@
mFlags,
mVolumePanelGlobalStateInteractor,
mUserTracker);
- testMediaOutputController.start(mCb);
+ testMediaSwitchingController.start(mCb);
reset(mCb);
- testMediaOutputController.getAppSourceName();
+ testMediaSwitchingController.getAppSourceName();
- assertThat(testMediaOutputController.getAppSourceName()).isNull();
+ assertThat(testMediaSwitchingController.getAppSourceName()).isNull();
}
@Test
public void isActiveItem_deviceNotConnected_returnsFalse() {
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2);
- assertThat(mMediaOutputController.isActiveItem(mMediaDevice1)).isFalse();
+ assertThat(mMediaSwitchingController.isActiveItem(mMediaDevice1)).isFalse();
}
@Test
public void getNotificationSmallIcon_packageNameIsNull_returnsNull() {
- MediaOutputController testMediaOutputController =
- new MediaOutputController(
+ MediaSwitchingController testMediaSwitchingController =
+ new MediaSwitchingController(
mSpyContext,
"",
mSpyContext.getUser(),
@@ -652,23 +707,23 @@
mFlags,
mVolumePanelGlobalStateInteractor,
mUserTracker);
- testMediaOutputController.start(mCb);
+ testMediaSwitchingController.start(mCb);
reset(mCb);
- testMediaOutputController.getAppSourceName();
+ testMediaSwitchingController.getAppSourceName();
- assertThat(testMediaOutputController.getNotificationSmallIcon()).isNull();
+ assertThat(testMediaSwitchingController.getNotificationSmallIcon()).isNull();
}
@Test
public void refreshDataSetIfNeeded_needRefreshIsTrue_setsToFalse() {
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.mNeedRefresh = true;
+ mMediaSwitchingController.mNeedRefresh = true;
- mMediaOutputController.refreshDataSetIfNeeded();
+ mMediaSwitchingController.refreshDataSetIfNeeded();
- assertThat(mMediaOutputController.mNeedRefresh).isFalse();
+ assertThat(mMediaSwitchingController.mNeedRefresh).isFalse();
}
@Test
@@ -677,13 +732,13 @@
ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK));
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1);
- assertThat(mMediaOutputController.isCurrentConnectedDeviceRemote()).isTrue();
+ assertThat(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).isTrue();
}
@Test
public void addDeviceToPlayMedia_callsLocalMediaManager() {
- MediaOutputController testMediaOutputController =
- new MediaOutputController(
+ MediaSwitchingController testMediaSwitchingController =
+ new MediaSwitchingController(
mSpyContext,
null,
mSpyContext.getUser(),
@@ -702,16 +757,16 @@
mUserTracker);
LocalMediaManager mockLocalMediaManager = mock(LocalMediaManager.class);
- testMediaOutputController.mLocalMediaManager = mockLocalMediaManager;
+ testMediaSwitchingController.mLocalMediaManager = mockLocalMediaManager;
- testMediaOutputController.addDeviceToPlayMedia(mMediaDevice2);
+ testMediaSwitchingController.addDeviceToPlayMedia(mMediaDevice2);
verify(mockLocalMediaManager).addDeviceToPlayMedia(mMediaDevice2);
}
@Test
public void removeDeviceFromPlayMedia_callsLocalMediaManager() {
- MediaOutputController testMediaOutputController =
- new MediaOutputController(
+ MediaSwitchingController testMediaSwitchingController =
+ new MediaSwitchingController(
mSpyContext,
null,
mSpyContext.getUser(),
@@ -730,15 +785,15 @@
mUserTracker);
LocalMediaManager mockLocalMediaManager = mock(LocalMediaManager.class);
- testMediaOutputController.mLocalMediaManager = mockLocalMediaManager;
+ testMediaSwitchingController.mLocalMediaManager = mockLocalMediaManager;
- testMediaOutputController.removeDeviceFromPlayMedia(mMediaDevice2);
+ testMediaSwitchingController.removeDeviceFromPlayMedia(mMediaDevice2);
verify(mockLocalMediaManager).removeDeviceFromPlayMedia(mMediaDevice2);
}
@Test
public void getDeselectableMediaDevice_triggersFromLocalMediaManager() {
- mMediaOutputController.getDeselectableMediaDevice();
+ mMediaSwitchingController.getDeselectableMediaDevice();
verify(mLocalMediaManager).getDeselectableMediaDevice();
}
@@ -746,108 +801,108 @@
@Test
public void adjustSessionVolume_adjustWithoutId_triggersFromLocalMediaManager() {
int testVolume = 10;
- mMediaOutputController.adjustSessionVolume(testVolume);
+ mMediaSwitchingController.adjustSessionVolume(testVolume);
verify(mLocalMediaManager).adjustSessionVolume(testVolume);
}
@Test
public void logInteractionAdjustVolume_triggersFromMetricLogger() {
- MediaOutputMetricLogger spyMediaOutputMetricLogger = spy(
- mMediaOutputController.mMetricLogger);
- mMediaOutputController.mMetricLogger = spyMediaOutputMetricLogger;
+ MediaOutputMetricLogger spyMediaOutputMetricLogger =
+ spy(mMediaSwitchingController.mMetricLogger);
+ mMediaSwitchingController.mMetricLogger = spyMediaOutputMetricLogger;
- mMediaOutputController.logInteractionAdjustVolume(mMediaDevice1);
+ mMediaSwitchingController.logInteractionAdjustVolume(mMediaDevice1);
verify(spyMediaOutputMetricLogger).logInteractionAdjustVolume(mMediaDevice1);
}
@Test
public void getSessionVolumeMax_triggersFromLocalMediaManager() {
- mMediaOutputController.getSessionVolumeMax();
+ mMediaSwitchingController.getSessionVolumeMax();
verify(mLocalMediaManager).getSessionVolumeMax();
}
@Test
public void getSessionVolume_triggersFromLocalMediaManager() {
- mMediaOutputController.getSessionVolume();
+ mMediaSwitchingController.getSessionVolume();
verify(mLocalMediaManager).getSessionVolume();
}
@Test
public void getSessionName_triggersFromLocalMediaManager() {
- mMediaOutputController.getSessionName();
+ mMediaSwitchingController.getSessionName();
verify(mLocalMediaManager).getSessionName();
}
@Test
public void releaseSession_triggersFromLocalMediaManager() {
- mMediaOutputController.releaseSession();
+ mMediaSwitchingController.releaseSession();
verify(mLocalMediaManager).releaseSession();
}
@Test
public void isAnyDeviceTransferring_noDevicesStateIsConnecting_returnsFalse() {
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
- assertThat(mMediaOutputController.isAnyDeviceTransferring()).isFalse();
+ assertThat(mMediaSwitchingController.isAnyDeviceTransferring()).isFalse();
}
@Test
public void isAnyDeviceTransferring_deviceStateIsConnecting_returnsTrue() {
when(mMediaDevice1.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
- assertThat(mMediaOutputController.isAnyDeviceTransferring()).isTrue();
+ assertThat(mMediaSwitchingController.isAnyDeviceTransferring()).isTrue();
}
@Test
public void isAnyDeviceTransferring_advancedLayoutSupport() {
when(mMediaDevice1.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
- mMediaOutputController.start(mCb);
- mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+ mMediaSwitchingController.start(mCb);
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
- assertThat(mMediaOutputController.isAnyDeviceTransferring()).isTrue();
+ assertThat(mMediaSwitchingController.isAnyDeviceTransferring()).isTrue();
}
@Test
public void isPlaying_stateIsNull() {
when(mSessionMediaController.getPlaybackState()).thenReturn(null);
- assertThat(mMediaOutputController.isPlaying()).isFalse();
+ assertThat(mMediaSwitchingController.isPlaying()).isFalse();
}
@Test
public void onSelectedDeviceStateChanged_verifyCallback() {
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2);
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.connectDevice(mMediaDevice1);
+ mMediaSwitchingController.connectDevice(mMediaDevice1);
- mMediaOutputController.onSelectedDeviceStateChanged(mMediaDevice1,
- LocalMediaManager.MediaDeviceState.STATE_CONNECTED);
+ mMediaSwitchingController.onSelectedDeviceStateChanged(
+ mMediaDevice1, LocalMediaManager.MediaDeviceState.STATE_CONNECTED);
verify(mCb).onRouteChanged();
}
@Test
public void onDeviceAttributesChanged_verifyCallback() {
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.onDeviceAttributesChanged();
+ mMediaSwitchingController.onDeviceAttributesChanged();
verify(mCb).onRouteChanged();
}
@@ -855,11 +910,11 @@
@Test
public void onRequestFailed_verifyCallback() {
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1);
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaOutputController.connectDevice(mMediaDevice2);
+ mMediaSwitchingController.connectDevice(mMediaDevice2);
- mMediaOutputController.onRequestFailed(0 /* reason */);
+ mMediaSwitchingController.onRequestFailed(0 /* reason */);
verify(mCb, atLeastOnce()).onRouteChanged();
}
@@ -868,37 +923,40 @@
public void getHeaderTitle_withoutMetadata_returnDefaultString() {
when(mSessionMediaController.getMetadata()).thenReturn(null);
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
- assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo(
- mContext.getText(R.string.controls_media_title));
+ assertThat(
+ mMediaSwitchingController
+ .getHeaderTitle()
+ .equals(mContext.getText(R.string.controls_media_title)))
+ .isTrue();
}
@Test
public void getHeaderTitle_withMetadata_returnSongName() {
when(mSessionMediaController.getMetadata()).thenReturn(mMediaMetadata);
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
- assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo(TEST_SONG);
+ assertThat(mMediaSwitchingController.getHeaderTitle().equals(TEST_SONG)).isTrue();
}
@Test
public void getHeaderSubTitle_withoutMetadata_returnNull() {
when(mSessionMediaController.getMetadata()).thenReturn(null);
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
- assertThat(mMediaOutputController.getHeaderSubTitle()).isNull();
+ assertThat(mMediaSwitchingController.getHeaderSubTitle()).isNull();
}
@Test
public void getHeaderSubTitle_withMetadata_returnArtistName() {
when(mSessionMediaController.getMetadata()).thenReturn(mMediaMetadata);
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.start(mCb);
- assertThat(mMediaOutputController.getHeaderSubTitle()).isEqualTo(TEST_ARTIST);
+ assertThat(mMediaSwitchingController.getHeaderSubTitle().equals(TEST_ARTIST)).isTrue();
}
@Test
@@ -911,8 +969,8 @@
mRoutingSessionInfos.add(mRemoteSessionInfo);
when(mLocalMediaManager.getRemoteRoutingSessions()).thenReturn(mRoutingSessionInfos);
- assertThat(mMediaOutputController.getActiveRemoteMediaDevices()).containsExactly(
- mRemoteSessionInfo);
+ assertThat(mMediaSwitchingController.getActiveRemoteMediaDevices())
+ .containsExactly(mRemoteSessionInfo);
}
@Test
@@ -933,7 +991,8 @@
selectableMediaDevices.add(selectableMediaDevice2);
doReturn(selectedMediaDevices).when(mLocalMediaManager).getSelectedMediaDevice();
doReturn(selectableMediaDevices).when(mLocalMediaManager).getSelectableMediaDevice();
- final List<MediaDevice> groupMediaDevices = mMediaOutputController.getGroupMediaDevices();
+ final List<MediaDevice> groupMediaDevices =
+ mMediaSwitchingController.getGroupMediaDevices();
// Reset order
selectedMediaDevices.clear();
selectedMediaDevices.add(selectedMediaDevice2);
@@ -941,7 +1000,7 @@
selectableMediaDevices.clear();
selectableMediaDevices.add(selectableMediaDevice2);
selectableMediaDevices.add(selectableMediaDevice1);
- final List<MediaDevice> newDevices = mMediaOutputController.getGroupMediaDevices();
+ final List<MediaDevice> newDevices = mMediaSwitchingController.getGroupMediaDevices();
assertThat(newDevices.size()).isEqualTo(groupMediaDevices.size());
for (int i = 0; i < groupMediaDevices.size(); i++) {
@@ -970,7 +1029,8 @@
selectableMediaDevices.add(selectableMediaDevice2);
doReturn(selectedMediaDevices).when(mLocalMediaManager).getSelectedMediaDevice();
doReturn(selectableMediaDevices).when(mLocalMediaManager).getSelectableMediaDevice();
- final List<MediaDevice> groupMediaDevices = mMediaOutputController.getGroupMediaDevices();
+ final List<MediaDevice> groupMediaDevices =
+ mMediaSwitchingController.getGroupMediaDevices();
// Reset order
selectedMediaDevices.clear();
selectedMediaDevices.add(selectedMediaDevice2);
@@ -979,7 +1039,7 @@
selectableMediaDevices.add(selectableMediaDevice3);
selectableMediaDevices.add(selectableMediaDevice2);
selectableMediaDevices.add(selectableMediaDevice1);
- final List<MediaDevice> newDevices = mMediaOutputController.getGroupMediaDevices();
+ final List<MediaDevice> newDevices = mMediaSwitchingController.getGroupMediaDevices();
assertThat(newDevices.size()).isEqualTo(5);
for (int i = 0; i < groupMediaDevices.size(); i++) {
@@ -991,8 +1051,8 @@
@Test
public void getNotificationLargeIcon_withoutPackageName_returnsNull() {
- mMediaOutputController =
- new MediaOutputController(
+ mMediaSwitchingController =
+ new MediaSwitchingController(
mSpyContext,
null,
mSpyContext.getUser(),
@@ -1010,7 +1070,7 @@
mVolumePanelGlobalStateInteractor,
mUserTracker);
- assertThat(mMediaOutputController.getNotificationIcon()).isNull();
+ assertThat(mMediaSwitchingController.getNotificationIcon()).isNull();
}
@Test
@@ -1028,7 +1088,7 @@
when(notification.isMediaNotification()).thenReturn(true);
when(notification.getLargeIcon()).thenReturn(null);
- assertThat(mMediaOutputController.getNotificationIcon()).isNull();
+ assertThat(mMediaSwitchingController.getNotificationIcon()).isNull();
}
@Test
@@ -1047,7 +1107,7 @@
when(notification.isMediaNotification()).thenReturn(true);
when(notification.getLargeIcon()).thenReturn(icon);
- assertThat(mMediaOutputController.getNotificationIcon()).isInstanceOf(IconCompat.class);
+ assertThat(mMediaSwitchingController.getNotificationIcon()).isInstanceOf(IconCompat.class);
}
@Test
@@ -1066,7 +1126,7 @@
when(notification.isMediaNotification()).thenReturn(false);
when(notification.getLargeIcon()).thenReturn(icon);
- assertThat(mMediaOutputController.getNotificationIcon()).isNull();
+ assertThat(mMediaSwitchingController.getNotificationIcon()).isNull();
}
@Test
@@ -1084,7 +1144,7 @@
when(notification.isMediaNotification()).thenReturn(true);
when(notification.getSmallIcon()).thenReturn(null);
- assertThat(mMediaOutputController.getNotificationSmallIcon()).isNull();
+ assertThat(mMediaSwitchingController.getNotificationSmallIcon()).isNull();
}
@Test
@@ -1103,8 +1163,8 @@
when(notification.isMediaNotification()).thenReturn(true);
when(notification.getSmallIcon()).thenReturn(icon);
- assertThat(mMediaOutputController.getNotificationSmallIcon()).isInstanceOf(
- IconCompat.class);
+ assertThat(mMediaSwitchingController.getNotificationSmallIcon())
+ .isInstanceOf(IconCompat.class);
}
@Test
@@ -1112,8 +1172,8 @@
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2);
when(mMediaDevice1.getIcon()).thenReturn(mDrawable);
- assertThat(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).isInstanceOf(
- IconCompat.class);
+ assertThat(mMediaSwitchingController.getDeviceIconCompat(mMediaDevice1))
+ .isInstanceOf(IconCompat.class);
}
@Test
@@ -1121,13 +1181,13 @@
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2);
when(mMediaDevice1.getIcon()).thenReturn(null);
- assertThat(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).isInstanceOf(
- IconCompat.class);
+ assertThat(mMediaSwitchingController.getDeviceIconCompat(mMediaDevice1))
+ .isInstanceOf(IconCompat.class);
}
@Test
public void setColorFilter_setColorFilterToDrawable() {
- mMediaOutputController.setColorFilter(mDrawable, true);
+ mMediaSwitchingController.setColorFilter(mDrawable, true);
verify(mDrawable).setColorFilter(any(PorterDuffColorFilter.class));
}
@@ -1150,11 +1210,11 @@
selectableMediaDevices.add(selectableMediaDevice2);
doReturn(selectedMediaDevices).when(mLocalMediaManager).getSelectedMediaDevice();
doReturn(selectableMediaDevices).when(mLocalMediaManager).getSelectableMediaDevice();
- assertThat(mMediaOutputController.getGroupMediaDevices().isEmpty()).isFalse();
+ assertThat(mMediaSwitchingController.getGroupMediaDevices().isEmpty()).isFalse();
- mMediaOutputController.resetGroupMediaDevices();
+ mMediaSwitchingController.resetGroupMediaDevices();
- assertThat(mMediaOutputController.mGroupMediaDevices.isEmpty()).isTrue();
+ assertThat(mMediaSwitchingController.mGroupMediaDevices.isEmpty()).isTrue();
}
@Test
@@ -1164,7 +1224,7 @@
when(mMediaDevice1.isVolumeFixed()).thenReturn(true);
- assertThat(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).isFalse();
+ assertThat(mMediaSwitchingController.isVolumeControlEnabled(mMediaDevice1)).isFalse();
}
@Test
@@ -1174,7 +1234,7 @@
when(mMediaDevice1.isVolumeFixed()).thenReturn(false);
- assertThat(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).isTrue();
+ assertThat(mMediaSwitchingController.isVolumeControlEnabled(mMediaDevice1)).isTrue();
}
@Test
@@ -1187,7 +1247,7 @@
when(mMediaDevice2.getDeviceType()).thenReturn(
MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE);
- mMediaOutputController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2);
+ mMediaSwitchingController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2);
verify(mPowerExemptionManager).addToTemporaryAllowList(anyString(), anyInt(), anyString(),
anyLong());
@@ -1195,8 +1255,8 @@
@Test
public void setTemporaryAllowListExceptionIfNeeded_packageNameIsNull_NoAction() {
- MediaOutputController testMediaOutputController =
- new MediaOutputController(
+ MediaSwitchingController testMediaSwitchingController =
+ new MediaSwitchingController(
mSpyContext,
null,
mSpyContext.getUser(),
@@ -1214,7 +1274,7 @@
mVolumePanelGlobalStateInteractor,
mUserTracker);
- testMediaOutputController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2);
+ testMediaSwitchingController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2);
verify(mPowerExemptionManager, never()).addToTemporaryAllowList(anyString(), anyInt(),
anyString(),
@@ -1223,22 +1283,22 @@
@Test
public void onMetadataChanged_triggersOnMetadataChanged() {
- mMediaOutputController.mCallback = this.mCallback;
+ mMediaSwitchingController.mCallback = this.mCallback;
- mMediaOutputController.mCb.onMetadataChanged(mMediaMetadata);
+ mMediaSwitchingController.mCb.onMetadataChanged(mMediaMetadata);
- verify(mMediaOutputController.mCallback).onMediaChanged();
+ verify(mMediaSwitchingController.mCallback).onMediaChanged();
}
@Test
public void onPlaybackStateChanged_updateWithNullState_onMediaStoppedOrPaused() {
when(mPlaybackState.getState()).thenReturn(PlaybackState.STATE_PLAYING);
- mMediaOutputController.mCallback = this.mCallback;
- mMediaOutputController.start(mCb);
+ mMediaSwitchingController.mCallback = this.mCallback;
+ mMediaSwitchingController.start(mCb);
- mMediaOutputController.mCb.onPlaybackStateChanged(null);
+ mMediaSwitchingController.mCb.onPlaybackStateChanged(null);
- verify(mMediaOutputController.mCallback).onMediaStoppedOrPaused();
+ verify(mMediaSwitchingController.mCallback).onMediaStoppedOrPaused();
}
@Test
@@ -1246,9 +1306,9 @@
when(mDialogTransitionAnimator.createActivityTransitionController(mDialogLaunchView))
.thenReturn(mActivityTransitionAnimatorController);
when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
- mMediaOutputController.mCallback = this.mCallback;
+ mMediaSwitchingController.mCallback = this.mCallback;
- mMediaOutputController.launchBluetoothPairing(mDialogLaunchView);
+ mMediaSwitchingController.launchBluetoothPairing(mDialogLaunchView);
verify(mCallback).dismissDialog();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
index 70af5e7..755adc6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
@@ -26,7 +26,6 @@
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onChildAt
import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
@@ -40,6 +39,7 @@
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.shared.model.SizedTileImpl
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGrid
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.shared.model.TileCategory
@@ -57,7 +57,7 @@
@Composable
private fun EditTileGridUnderTest(
listState: EditTileListState,
- onSetTiles: (List<TileSpec>) -> Unit
+ onSetTiles: (List<TileSpec>) -> Unit,
) {
DefaultEditTileGrid(
currentListState = listState,
@@ -182,7 +182,7 @@
private fun ComposeContentTestRule.assertTileGridContainsExactly(specs: List<String>) {
onNodeWithTag(CURRENT_TILES_GRID_TEST_TAG).onChildren().apply {
fetchSemanticsNodes().forEachIndexed { index, _ ->
- get(index).onChildAt(0).assert(hasContentDescription(specs[index]))
+ get(index).assert(hasContentDescription(specs[index]))
}
}
}
@@ -198,7 +198,7 @@
icon =
Icon.Resource(
android.R.drawable.star_on,
- ContentDescription.Loaded(tileSpec)
+ ContentDescription.Loaded(tileSpec),
),
label = AnnotatedString(tileSpec),
appName = null,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 1e2648b22..ecc7909 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -20,7 +20,6 @@
import static android.media.AudioManager.RINGER_MODE_SILENT;
import static android.media.AudioManager.RINGER_MODE_VIBRATE;
-import static com.android.systemui.Flags.FLAG_HAPTIC_VOLUME_SLIDER;
import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN;
import static com.android.systemui.volume.Events.SHOW_REASON_UNKNOWN;
import static com.android.systemui.volume.VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST;
@@ -51,7 +50,6 @@
import android.media.AudioManager;
import android.media.AudioSystem;
import android.os.SystemClock;
-import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.provider.Settings;
import android.testing.TestableLooper;
@@ -285,23 +283,8 @@
}
@Test
- @DisableFlags(FLAG_HAPTIC_VOLUME_SLIDER)
- public void addSliderHaptics_withHapticsDisabled_doesNotDeliverOnProgressChangedHaptics() {
- // GIVEN that the slider haptics flag is disabled and we try to add haptics to volume rows
- mDialog.addSliderHapticsToRows();
-
- // WHEN haptics try to be delivered to a volume stream
- boolean canDeliverHaptics =
- mDialog.canDeliverProgressHapticsToStream(AudioSystem.STREAM_MUSIC, true, 50);
-
- // THEN the result is that haptics are not successfully delivered
- assertFalse(canDeliverHaptics);
- }
-
- @Test
- @EnableFlags(FLAG_HAPTIC_VOLUME_SLIDER)
- public void addSliderHaptics_withHapticsEnabled_canDeliverOnProgressChangedHaptics() {
- // GIVEN that the slider haptics flag is enabled and we try to add haptics to volume rows
+ public void addSliderHaptics_canDeliverOnProgressChangedHaptics() {
+ // GIVEN that the slider haptics are added to rows
mDialog.addSliderHapticsToRows();
// WHEN haptics try to be delivered to a volume stream
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
index 1ed10fbe..8922b2f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
@@ -20,6 +20,7 @@
import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
import com.android.systemui.flags.fakeSystemPropertiesHelper
import com.android.systemui.flags.systemPropertiesHelper
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.trustInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
@@ -37,5 +38,6 @@
powerInteractor = powerInteractor,
biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor,
systemPropertiesHelper = fakeSystemPropertiesHelper,
+ keyguardTransitionInteractor = keyguardTransitionInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
index 1d2bce2..1df3ef4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
@@ -19,7 +19,7 @@
import com.android.systemui.kosmos.Kosmos
import java.time.Instant
-var Kosmos.contextualEducationRepository: ContextualEducationRepository by
+var Kosmos.contextualEducationRepository: FakeContextualEducationRepository by
Kosmos.Fixture { FakeContextualEducationRepository() }
var Kosmos.fakeEduClock: FakeEduClock by Kosmos.Fixture { FakeEduClock(Instant.MIN) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt
index fb4e2fb..4667bf5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt
@@ -17,39 +17,77 @@
package com.android.systemui.education.data.repository
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.education.data.model.EduDeviceConnectionTime
import com.android.systemui.education.data.model.GestureEduModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
class FakeContextualEducationRepository : ContextualEducationRepository {
- private val userGestureMap = mutableMapOf<Int, GestureEduModel>()
- private val _gestureEduModels = MutableStateFlow(GestureEduModel(userId = 0))
- private val gestureEduModelsFlow = _gestureEduModels.asStateFlow()
+ private val userGestureMap = mutableMapOf<Int, MutableMap<GestureType, GestureEduModel>>()
+
+ private val _backGestureEduModels = MutableStateFlow(GestureEduModel(BACK, userId = 0))
+ private val backGestureEduModelsFlow = _backGestureEduModels.asStateFlow()
+
+ private val _homeGestureEduModels = MutableStateFlow(GestureEduModel(HOME, userId = 0))
+ private val homeEduModelsFlow = _homeGestureEduModels.asStateFlow()
+
+ private val _allAppsGestureEduModels = MutableStateFlow(GestureEduModel(ALL_APPS, userId = 0))
+ private val allAppsGestureEduModels = _allAppsGestureEduModels.asStateFlow()
+
+ private val _overviewsGestureEduModels = MutableStateFlow(GestureEduModel(OVERVIEW, userId = 0))
+ private val overviewsGestureEduModels = _overviewsGestureEduModels.asStateFlow()
private val userEduDeviceConnectionTimeMap = mutableMapOf<Int, EduDeviceConnectionTime>()
private val _eduDeviceConnectionTime = MutableStateFlow(EduDeviceConnectionTime())
private val eduDeviceConnectionTime = _eduDeviceConnectionTime.asStateFlow()
+ private val _keyboardShortcutTriggered = MutableStateFlow<GestureType?>(null)
+
private var currentUser: Int = 0
override fun setUser(userId: Int) {
if (!userGestureMap.contains(userId)) {
- userGestureMap[userId] = GestureEduModel(userId = userId)
+ userGestureMap[userId] = createGestureEduModelMap(userId = userId)
userEduDeviceConnectionTimeMap[userId] = EduDeviceConnectionTime()
}
// save data of current user to the map
- userGestureMap[currentUser] = _gestureEduModels.value
- userEduDeviceConnectionTimeMap[currentUser] = _eduDeviceConnectionTime.value
+ val currentUserMap = userGestureMap[currentUser]!!
+ currentUserMap[BACK] = _backGestureEduModels.value
+ currentUserMap[HOME] = _homeGestureEduModels.value
+ currentUserMap[ALL_APPS] = _allAppsGestureEduModels.value
+ currentUserMap[OVERVIEW] = _overviewsGestureEduModels.value
+
// switch to data of new user
- _gestureEduModels.value = userGestureMap[userId]!!
+ val newUserGestureMap = userGestureMap[userId]!!
+ newUserGestureMap[BACK]?.let { _backGestureEduModels.value = it }
+ newUserGestureMap[HOME]?.let { _homeGestureEduModels.value = it }
+ newUserGestureMap[ALL_APPS]?.let { _allAppsGestureEduModels.value = it }
+ newUserGestureMap[OVERVIEW]?.let { _overviewsGestureEduModels.value = it }
+
+ userEduDeviceConnectionTimeMap[currentUser] = _eduDeviceConnectionTime.value
_eduDeviceConnectionTime.value = userEduDeviceConnectionTimeMap[userId]!!
}
+ private fun createGestureEduModelMap(userId: Int): MutableMap<GestureType, GestureEduModel> {
+ val gestureModelMap = mutableMapOf<GestureType, GestureEduModel>()
+ GestureType.values().forEach { gestureModelMap[it] = GestureEduModel(it, userId = userId) }
+ return gestureModelMap
+ }
+
override fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> {
- return gestureEduModelsFlow
+ return when (gestureType) {
+ BACK -> backGestureEduModelsFlow
+ HOME -> homeEduModelsFlow
+ ALL_APPS -> allAppsGestureEduModels
+ OVERVIEW -> overviewsGestureEduModels
+ }
}
override fun readEduDeviceConnectionTime(): Flow<EduDeviceConnectionTime> {
@@ -60,8 +98,16 @@
gestureType: GestureType,
transform: (GestureEduModel) -> GestureEduModel
) {
- val currentModel = _gestureEduModels.value
- _gestureEduModels.value = transform(currentModel)
+ val gestureModels =
+ when (gestureType) {
+ BACK -> _backGestureEduModels
+ HOME -> _homeGestureEduModels
+ ALL_APPS -> _allAppsGestureEduModels
+ OVERVIEW -> _overviewsGestureEduModels
+ }
+
+ val currentModel = gestureModels.value
+ gestureModels.value = transform(currentModel)
}
override suspend fun updateEduDeviceConnectionTime(
@@ -70,4 +116,11 @@
val currentModel = _eduDeviceConnectionTime.value
_eduDeviceConnectionTime.value = transform(currentModel)
}
+
+ override val keyboardShortcutTriggered: Flow<GestureType>
+ get() = _keyboardShortcutTriggered.filterNotNull()
+
+ fun setKeyboardShortcutTriggered(gestureType: GestureType) {
+ _keyboardShortcutTriggered.value = gestureType
+ }
}
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 80f6fc2..2d275f9 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
@@ -40,8 +40,7 @@
touchpadRepository,
userRepository
),
- clock = fakeEduClock,
- inputManager = mockEduInputManager
+ clock = fakeEduClock
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
index c252924..c0152b26 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
@@ -17,7 +17,6 @@
package com.android.systemui.flags
import android.platform.test.annotations.EnableFlags
-import com.android.systemui.Flags.FLAG_COMPOSE_LOCKSCREEN
import com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR
import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
@@ -31,7 +30,6 @@
* that feature. It is also picked up by [SceneContainerRule] to set non-aconfig prerequisites.
*/
@EnableFlags(
- FLAG_COMPOSE_LOCKSCREEN,
FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
FLAG_KEYGUARD_WM_STATE_REFACTOR,
FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/FakeMSDLPlayer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/FakeMSDLPlayer.kt
index 5ad973a..2b81da3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/FakeMSDLPlayer.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/FakeMSDLPlayer.kt
@@ -20,8 +20,10 @@
import com.google.android.msdl.data.model.MSDLToken
import com.google.android.msdl.domain.InteractionProperties
import com.google.android.msdl.domain.MSDLPlayer
+import com.google.android.msdl.logging.MSDLEvent
class FakeMSDLPlayer : MSDLPlayer {
+ private val history = arrayListOf<MSDLEvent>()
var currentFeedbackLevel = FeedbackLevel.DEFAULT
var latestTokenPlayed: MSDLToken? = null
private set
@@ -34,5 +36,8 @@
override fun playToken(token: MSDLToken, properties: InteractionProperties?) {
latestTokenPlayed = token
latestPropertiesPlayed = properties
+ history.add(MSDLEvent(token, properties))
}
+
+ override fun getHistory(): List<MSDLEvent> = history
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt
index ca748b66..80db1e9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.haptics.qs
+import com.android.systemui.classifier.fakeFalsingManager
import com.android.systemui.haptics.vibratorHelper
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.log.core.FakeLogBuffer
@@ -26,6 +27,7 @@
QSLongPressEffect(
vibratorHelper,
keyguardStateController,
+ fakeFalsingManager,
FakeLogBuffer.Factory.create(),
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorKosmos.kt
deleted file mode 100644
index edbc4c1..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorKosmos.kt
+++ /dev/null
@@ -1,34 +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.systemui.qs.panels.domain.interactor
-
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.log.core.FakeLogBuffer
-import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
-
-val Kosmos.gridConsistencyInteractor by
- Kosmos.Fixture {
- GridConsistencyInteractor(
- gridLayoutTypeInteractor,
- currentTilesInteractor,
- gridConsistencyInteractorsMap,
- noopGridConsistencyInteractor,
- FakeLogBuffer.Factory.create(),
- applicationCoroutineScope,
- )
- }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorKosmos.kt
index 34e99d3..c951642 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorKosmos.kt
@@ -27,6 +27,3 @@
val Kosmos.gridLayoutMap: Map<GridLayoutType, GridLayout> by
Kosmos.Fixture { mapOf(Pair(InfiniteGridLayoutType, infiniteGridLayout)) }
-
-var Kosmos.gridConsistencyInteractorsMap: Map<GridLayoutType, GridTypeConsistencyInteractor> by
- Kosmos.Fixture { mapOf(Pair(InfiniteGridLayoutType, infiniteGridConsistencyInteractor)) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt
deleted file mode 100644
index 320c2ec..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt
+++ /dev/null
@@ -1,24 +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.systemui.qs.panels.domain.interactor
-
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.infiniteGridConsistencyInteractor by
- Kosmos.Fixture {
- InfiniteGridConsistencyInteractor(iconTilesInteractor, fixedColumnsSizeInteractor)
- }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
index be00152..3f62b4d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
@@ -17,7 +17,7 @@
package com.android.systemui.qs.panels.domain.interactor
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout
import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractorKosmos.kt
deleted file mode 100644
index e3beff7..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractorKosmos.kt
+++ /dev/null
@@ -1,21 +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.systemui.qs.panels.domain.interactor
-
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.noopGridConsistencyInteractor by Kosmos.Fixture { NoopGridConsistencyInteractor() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt
index c416ea1..91602c2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt
@@ -16,8 +16,13 @@
package com.android.systemui.statusbar.data.repository
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
class FakeRemoteInputRepository : RemoteInputRepository {
override val isRemoteInputActive = MutableStateFlow(false)
+ override val remoteInputRowBottomBound: Flow<Float?> = flowOf(null)
+
+ override fun setRemoteInputRowBottomBound(bottom: Float?) {}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt
index 6370a5d..7244d46 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt
@@ -22,6 +22,7 @@
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
val Kosmos.notificationScrollViewModel by Fixture {
@@ -29,6 +30,7 @@
dumpManager = dumpManager,
stackAppearanceInteractor = notificationStackAppearanceInteractor,
shadeInteractor = shadeInteractor,
+ remoteInputInteractor = remoteInputInteractor,
sceneInteractor = sceneInteractor,
keyguardInteractor = { keyguardInteractor },
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
index 8bfc390..e5cf0a9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
@@ -22,6 +22,7 @@
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
@@ -31,6 +32,7 @@
sceneInteractor = sceneInteractor,
shadeInteractor = shadeInteractor,
headsUpNotificationInteractor = headsUpNotificationInteractor,
+ remoteInputInteractor = remoteInputInteractor,
featureFlags = featureFlagsClassic,
dumpManager = dumpManager,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
index 61b53c9..99cd830 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
@@ -22,6 +22,8 @@
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.shared.notifications.data.repository.notificationSettingsRepository
+import com.android.systemui.statusbar.policy.data.repository.deviceProvisioningRepository
+import com.android.systemui.statusbar.policy.data.repository.userSetupRepository
import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
val Kosmos.zenModeInteractor by Fixture {
@@ -31,5 +33,7 @@
notificationSettingsRepository = notificationSettingsRepository,
bgDispatcher = testDispatcher,
iconLoader = zenIconLoader,
+ deviceProvisioningRepository = deviceProvisioningRepository,
+ userSetupRepository = userSetupRepository,
)
}
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index d1a3bf9..10e4f38 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -343,6 +343,8 @@
data: [
":framework-res",
":ravenwood-empty-res",
+ ":framework-platform-compat-config",
+ ":services-platform-compat-config",
],
libs: [
"100-framework-minus-apex.ravenwood",
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java
index c3b7087..1f98334 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java
@@ -16,7 +16,15 @@
package com.android.server.appfunctions;
+import android.annotation.NonNull;
+import android.os.UserHandle;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+
import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -33,5 +41,50 @@
/* unit= */ TimeUnit.SECONDS,
/* workQueue= */ new LinkedBlockingQueue<>());
+ /** A map of per-user executors for queued work. */
+ @GuardedBy("sLock")
+ private static final SparseArray<ExecutorService> mPerUserExecutorsLocked = new SparseArray<>();
+
+ private static final Object sLock = new Object();
+
+ /**
+ * Returns a per-user executor for queued metadata sync request.
+ *
+ * <p>The work submitted to these executor (Sync request) needs to be synchronous per user hence
+ * the use of a single thread.
+ *
+ * <p>Note: Use a different executor if not calling {@code submitSyncRequest} on a {@code
+ * MetadataSyncAdapter}.
+ */
+ // TODO(b/357551503): Restrict the scope of this executor to the MetadataSyncAdapter itself.
+ public static ExecutorService getPerUserSyncExecutor(@NonNull UserHandle user) {
+ synchronized (sLock) {
+ ExecutorService executor = mPerUserExecutorsLocked.get(user.getIdentifier(), null);
+ if (executor == null) {
+ executor = Executors.newSingleThreadExecutor();
+ mPerUserExecutorsLocked.put(user.getIdentifier(), executor);
+ }
+ return executor;
+ }
+ }
+
+ /**
+ * Shuts down and removes the per-user executor for queued work.
+ *
+ * <p>This should be called when the user is removed.
+ */
+ public static void shutDownAndRemoveUserExecutor(@NonNull UserHandle user)
+ throws InterruptedException {
+ ExecutorService executor;
+ synchronized (sLock) {
+ executor = mPerUserExecutorsLocked.get(user.getIdentifier());
+ mPerUserExecutorsLocked.remove(user.getIdentifier());
+ }
+ if (executor != null) {
+ executor.shutdown();
+ var unused = executor.awaitTermination(30, TimeUnit.SECONDS);
+ }
+ }
+
private AppFunctionExecutors() {}
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java
index 02800cb..c293087 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java
@@ -16,6 +16,7 @@
package com.android.server.appfunctions;
+import android.annotation.NonNull;
import android.app.appfunctions.AppFunctionManagerConfiguration;
import android.content.Context;
@@ -36,4 +37,14 @@
publishBinderService(Context.APP_FUNCTION_SERVICE, mServiceImpl);
}
}
+
+ @Override
+ public void onUserUnlocked(@NonNull TargetUser user) {
+ mServiceImpl.onUserUnlocked(user);
+ }
+
+ @Override
+ public void onUserStopping(@NonNull TargetUser user) {
+ mServiceImpl.onUserStopping(user);
+ }
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 2362b91..cf039df 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -19,29 +19,35 @@
import static com.android.server.appfunctions.AppFunctionExecutors.THREAD_POOL_EXECUTOR;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appfunctions.AppFunctionStaticMetadataHelper;
import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
import android.app.appfunctions.ExecuteAppFunctionResponse;
import android.app.appfunctions.IAppFunctionManager;
import android.app.appfunctions.IAppFunctionService;
import android.app.appfunctions.IExecuteAppFunctionCallback;
import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.observer.DocumentChangeInfo;
+import android.app.appsearch.observer.ObserverCallback;
+import android.app.appsearch.observer.ObserverSpec;
+import android.app.appsearch.observer.SchemaChangeInfo;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Slog;
-import android.app.appsearch.AppSearchResult;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemService.TargetUser;
import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback;
import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener;
+import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.CompletionException;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
/** Implementation of the AppFunctionManagerService. */
public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
@@ -51,9 +57,11 @@
private final CallerValidator mCallerValidator;
private final ServiceHelper mInternalServiceHelper;
private final ServiceConfig mServiceConfig;
+ private final Context mContext;
public AppFunctionManagerServiceImpl(@NonNull Context context) {
this(
+ context,
new RemoteServiceCallerImpl<>(
context, IAppFunctionService.Stub::asInterface, THREAD_POOL_EXECUTOR),
new CallerValidatorImpl(context),
@@ -63,10 +71,12 @@
@VisibleForTesting
AppFunctionManagerServiceImpl(
+ Context context,
RemoteServiceCaller<IAppFunctionService> remoteServiceCaller,
CallerValidator callerValidator,
ServiceHelper appFunctionInternalServiceHelper,
ServiceConfig serviceConfig) {
+ mContext = Objects.requireNonNull(context);
mRemoteServiceCaller = Objects.requireNonNull(remoteServiceCaller);
mCallerValidator = Objects.requireNonNull(callerValidator);
mInternalServiceHelper = Objects.requireNonNull(appFunctionInternalServiceHelper);
@@ -90,6 +100,26 @@
}
}
+ /** Called when the user is unlocked. */
+ public void onUserUnlocked(TargetUser user) {
+ Objects.requireNonNull(user);
+
+ registerAppSearchObserver(user);
+ trySyncRuntimeMetadata(user);
+ }
+
+ /** Called when the user is stopping. */
+ public void onUserStopping(@NonNull TargetUser user) {
+ Objects.requireNonNull(user);
+
+ try {
+ AppFunctionExecutors.shutDownAndRemoveUserExecutor(user.getUserHandle());
+ MetadataSyncPerUser.removeUserSyncAdapter(user.getUserHandle());
+ } catch (InterruptedException e) {
+ Slog.e(TAG, "Unable to remove data for: " + user.getUserHandle(), e);
+ }
+ }
+
private void executeAppFunctionInternal(
ExecuteAppFunctionAidlRequest requestInternal,
SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) {
@@ -132,53 +162,55 @@
return;
}
- var unused = mCallerValidator
- .verifyCallerCanExecuteAppFunction(
- validatedCallingPackage,
- targetPackageName,
- requestInternal.getClientRequest().getFunctionIdentifier())
- .thenAccept(
- canExecute -> {
- if (!canExecute) {
- safeExecuteAppFunctionCallback.onResult(
- ExecuteAppFunctionResponse.newFailure(
- ExecuteAppFunctionResponse.RESULT_DENIED,
- "Caller does not have permission to execute the"
- + " appfunction",
- /* extras= */ null));
- return;
- }
- Intent serviceIntent =
- mInternalServiceHelper.resolveAppFunctionService(
- targetPackageName, targetUser);
- if (serviceIntent == null) {
- safeExecuteAppFunctionCallback.onResult(
- ExecuteAppFunctionResponse.newFailure(
- ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
- "Cannot find the target service.",
- /* extras= */ null));
- return;
- }
- final long token = Binder.clearCallingIdentity();
- try {
- bindAppFunctionServiceUnchecked(
- requestInternal,
- serviceIntent,
- targetUser,
- safeExecuteAppFunctionCallback,
- /* bindFlags= */ Context.BIND_AUTO_CREATE,
- /* timeoutInMillis= */ mServiceConfig
- .getExecuteAppFunctionTimeoutMillis());
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- })
- .exceptionally(
- ex -> {
- safeExecuteAppFunctionCallback.onResult(
- mapExceptionToExecuteAppFunctionResponse(ex));
- return null;
- });
+ var unused =
+ mCallerValidator
+ .verifyCallerCanExecuteAppFunction(
+ validatedCallingPackage,
+ targetPackageName,
+ requestInternal.getClientRequest().getFunctionIdentifier())
+ .thenAccept(
+ canExecute -> {
+ if (!canExecute) {
+ safeExecuteAppFunctionCallback.onResult(
+ ExecuteAppFunctionResponse.newFailure(
+ ExecuteAppFunctionResponse.RESULT_DENIED,
+ "Caller does not have permission to execute"
+ + " the appfunction",
+ /* extras= */ null));
+ return;
+ }
+ Intent serviceIntent =
+ mInternalServiceHelper.resolveAppFunctionService(
+ targetPackageName, targetUser);
+ if (serviceIntent == null) {
+ safeExecuteAppFunctionCallback.onResult(
+ ExecuteAppFunctionResponse.newFailure(
+ ExecuteAppFunctionResponse
+ .RESULT_INTERNAL_ERROR,
+ "Cannot find the target service.",
+ /* extras= */ null));
+ return;
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ bindAppFunctionServiceUnchecked(
+ requestInternal,
+ serviceIntent,
+ targetUser,
+ safeExecuteAppFunctionCallback,
+ /* bindFlags= */ Context.BIND_AUTO_CREATE,
+ /* timeoutInMillis= */ mServiceConfig
+ .getExecuteAppFunctionTimeoutMillis());
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ })
+ .exceptionally(
+ ex -> {
+ safeExecuteAppFunctionCallback.onResult(
+ mapExceptionToExecuteAppFunctionResponse(ex));
+ return null;
+ });
}
private void bindAppFunctionServiceUnchecked(
@@ -256,7 +288,7 @@
}
private ExecuteAppFunctionResponse mapExceptionToExecuteAppFunctionResponse(Throwable e) {
- if(e instanceof CompletionException) {
+ if (e instanceof CompletionException) {
e = e.getCause();
}
@@ -291,4 +323,103 @@
}
return ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR;
}
+
+ private void registerAppSearchObserver(@NonNull TargetUser user) {
+ AppSearchManager perUserAppSearchManager =
+ mContext.createContextAsUser(user.getUserHandle(), /* flags= */ 0)
+ .getSystemService(AppSearchManager.class);
+ if (perUserAppSearchManager == null) {
+ Slog.d(TAG, "AppSearch Manager not found for user: " + user.getUserIdentifier());
+ return;
+ }
+ try (FutureGlobalSearchSession futureGlobalSearchSession =
+ new FutureGlobalSearchSession(
+ perUserAppSearchManager, AppFunctionExecutors.THREAD_POOL_EXECUTOR)) {
+ AppFunctionMetadataObserver appFunctionMetadataObserver =
+ new AppFunctionMetadataObserver(
+ user.getUserHandle(),
+ mContext.createContextAsUser(user.getUserHandle(), /* flags= */ 0));
+ var unused =
+ futureGlobalSearchSession
+ .registerObserverCallbackAsync(
+ "android",
+ new ObserverSpec.Builder().build(),
+ THREAD_POOL_EXECUTOR,
+ appFunctionMetadataObserver)
+ .whenComplete(
+ (voidResult, ex) -> {
+ if (ex != null) {
+ Slog.e(TAG, "Failed to register observer: ", ex);
+ }
+ });
+
+ } catch (IOException ex) {
+ Slog.e(TAG, "Failed to close observer session: ", ex);
+ }
+ }
+
+ private void trySyncRuntimeMetadata(@NonNull TargetUser user) {
+ MetadataSyncAdapter metadataSyncAdapter =
+ MetadataSyncPerUser.getPerUserMetadataSyncAdapter(
+ user.getUserHandle(),
+ mContext.createContextAsUser(user.getUserHandle(), /* flags= */ 0));
+ if (metadataSyncAdapter != null) {
+ var unused =
+ metadataSyncAdapter
+ .submitSyncRequest()
+ .whenComplete(
+ (isSuccess, ex) -> {
+ if (ex != null || !isSuccess) {
+ Slog.e(TAG, "Sync was not successful");
+ }
+ });
+ }
+ }
+
+ private static class AppFunctionMetadataObserver implements ObserverCallback {
+ @Nullable private final MetadataSyncAdapter mPerUserMetadataSyncAdapter;
+
+ AppFunctionMetadataObserver(@NonNull UserHandle userHandle, @NonNull Context userContext) {
+ mPerUserMetadataSyncAdapter =
+ MetadataSyncPerUser.getPerUserMetadataSyncAdapter(userHandle, userContext);
+ }
+
+ @Override
+ public void onDocumentChanged(@NonNull DocumentChangeInfo documentChangeInfo) {
+ if (mPerUserMetadataSyncAdapter == null) {
+ return;
+ }
+ if (documentChangeInfo
+ .getDatabaseName()
+ .equals(AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_METADATA_DB)
+ && documentChangeInfo
+ .getNamespace()
+ .equals(
+ AppFunctionStaticMetadataHelper
+ .APP_FUNCTION_STATIC_NAMESPACE)) {
+ var unused = mPerUserMetadataSyncAdapter.submitSyncRequest();
+ }
+ }
+
+ @Override
+ public void onSchemaChanged(@NonNull SchemaChangeInfo schemaChangeInfo) {
+ if (mPerUserMetadataSyncAdapter == null) {
+ return;
+ }
+ if (schemaChangeInfo
+ .getDatabaseName()
+ .equals(AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_METADATA_DB)) {
+ boolean shouldInitiateSync = false;
+ for (String schemaName : schemaChangeInfo.getChangedSchemaNames()) {
+ if (schemaName.startsWith(AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE)) {
+ shouldInitiateSync = true;
+ break;
+ }
+ }
+ if (shouldInitiateSync) {
+ var unused = mPerUserMetadataSyncAdapter.submitSyncRequest();
+ }
+ }
+ }
+ }
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
index e2573590..8c6f50e 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
@@ -24,6 +24,8 @@
import android.app.appfunctions.AppFunctionRuntimeMetadata;
import android.app.appfunctions.AppFunctionStaticMetadataHelper;
import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchManager.SearchContext;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.PackageIdentifier;
@@ -61,9 +63,8 @@
*/
public class MetadataSyncAdapter {
private static final String TAG = MetadataSyncAdapter.class.getSimpleName();
- private final FutureAppSearchSession mRuntimeMetadataSearchSession;
- private final FutureAppSearchSession mStaticMetadataSearchSession;
private final Executor mSyncExecutor;
+ private final AppSearchManager mAppSearchManager;
private final PackageManager mPackageManager;
// Hidden constants in {@link SetSchemaRequest} that restricts runtime metadata visibility
@@ -73,13 +74,11 @@
public MetadataSyncAdapter(
@NonNull Executor syncExecutor,
- @NonNull FutureAppSearchSession runtimeMetadataSearchSession,
- @NonNull FutureAppSearchSession staticMetadataSearchSession,
- @NonNull PackageManager packageManager) {
+ @NonNull PackageManager packageManager,
+ @NonNull AppSearchManager appSearchManager) {
mSyncExecutor = Objects.requireNonNull(syncExecutor);
- mRuntimeMetadataSearchSession = Objects.requireNonNull(runtimeMetadataSearchSession);
- mStaticMetadataSearchSession = Objects.requireNonNull(staticMetadataSearchSession);
mPackageManager = Objects.requireNonNull(packageManager);
+ mAppSearchManager = Objects.requireNonNull(appSearchManager);
}
/**
@@ -89,31 +88,54 @@
* synchronization was successful.
*/
public AndroidFuture<Boolean> submitSyncRequest() {
+ SearchContext staticMetadataSearchContext =
+ new SearchContext.Builder(
+ AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_METADATA_DB)
+ .build();
+ SearchContext runtimeMetadataSearchContext =
+ new SearchContext.Builder(
+ AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_METADATA_DB)
+ .build();
AndroidFuture<Boolean> settableSyncStatus = new AndroidFuture<>();
mSyncExecutor.execute(
() -> {
- try {
- trySyncAppFunctionMetadataBlocking();
+ try (FutureAppSearchSession staticMetadataSearchSession =
+ new FutureAppSearchSessionImpl(
+ mAppSearchManager,
+ AppFunctionExecutors.THREAD_POOL_EXECUTOR,
+ staticMetadataSearchContext);
+ FutureAppSearchSession runtimeMetadataSearchSession =
+ new FutureAppSearchSessionImpl(
+ mAppSearchManager,
+ AppFunctionExecutors.THREAD_POOL_EXECUTOR,
+ runtimeMetadataSearchContext)) {
+
+ trySyncAppFunctionMetadataBlocking(
+ staticMetadataSearchSession, runtimeMetadataSearchSession);
settableSyncStatus.complete(true);
- } catch (Exception e) {
- settableSyncStatus.completeExceptionally(e);
+
+ } catch (Exception ex) {
+ settableSyncStatus.completeExceptionally(ex);
}
});
return settableSyncStatus;
}
@WorkerThread
- private void trySyncAppFunctionMetadataBlocking()
+ @VisibleForTesting
+ void trySyncAppFunctionMetadataBlocking(
+ @NonNull FutureAppSearchSession staticMetadataSearchSession,
+ @NonNull FutureAppSearchSession runtimeMetadataSearchSession)
throws ExecutionException, InterruptedException {
ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap =
getPackageToFunctionIdMap(
- mStaticMetadataSearchSession,
+ staticMetadataSearchSession,
AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE,
AppFunctionStaticMetadataHelper.PROPERTY_FUNCTION_ID,
AppFunctionStaticMetadataHelper.PROPERTY_PACKAGE_NAME);
ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap =
getPackageToFunctionIdMap(
- mRuntimeMetadataSearchSession,
+ runtimeMetadataSearchSession,
RUNTIME_SCHEMA_TYPE,
AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID,
AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME);
@@ -134,7 +156,7 @@
RemoveByDocumentIdRequest removeByDocumentIdRequest =
buildRemoveRuntimeMetadataRequest(removedFunctionsDiffMap);
AppSearchBatchResult<String, Void> removeDocumentBatchResult =
- mRuntimeMetadataSearchSession.remove(removeByDocumentIdRequest).get();
+ runtimeMetadataSearchSession.remove(removeByDocumentIdRequest).get();
if (!removeDocumentBatchResult.isSuccess()) {
throw convertFailedAppSearchResultToException(
removeDocumentBatchResult.getFailures().values());
@@ -144,13 +166,14 @@
if (!addedFunctionsDiffMap.isEmpty()) {
// TODO(b/357551503): only set schema on package diff
SetSchemaRequest addSetSchemaRequest =
- buildSetSchemaRequestForRuntimeMetadataSchemas(appRuntimeMetadataSchemas);
+ buildSetSchemaRequestForRuntimeMetadataSchemas(
+ mPackageManager, appRuntimeMetadataSchemas);
Objects.requireNonNull(
- mRuntimeMetadataSearchSession.setSchema(addSetSchemaRequest).get());
+ runtimeMetadataSearchSession.setSchema(addSetSchemaRequest).get());
PutDocumentsRequest putDocumentsRequest =
buildPutRuntimeMetadataRequest(addedFunctionsDiffMap);
AppSearchBatchResult<String, Void> putDocumentBatchResult =
- mRuntimeMetadataSearchSession.put(putDocumentsRequest).get();
+ runtimeMetadataSearchSession.put(putDocumentsRequest).get();
if (!putDocumentBatchResult.isSuccess()) {
throw convertFailedAppSearchResultToException(
putDocumentBatchResult.getFailures().values());
@@ -211,6 +234,7 @@
@NonNull
private SetSchemaRequest buildSetSchemaRequestForRuntimeMetadataSchemas(
+ @NonNull PackageManager packageManager,
@NonNull Set<AppSearchSchema> metadataSchemaSet) {
Objects.requireNonNull(metadataSchemaSet);
SetSchemaRequest.Builder setSchemaRequestBuilder =
@@ -220,7 +244,7 @@
String packageName =
AppFunctionRuntimeMetadata.getPackageNameFromSchema(
runtimeMetadataSchema.getSchemaType());
- byte[] packageCert = getCertificate(packageName);
+ byte[] packageCert = getCertificate(packageManager, packageName);
if (packageCert == null) {
continue;
}
@@ -399,13 +423,15 @@
/** Gets the SHA-256 certificate from a {@link PackageManager}, or null if it is not found. */
@Nullable
- private byte[] getCertificate(@NonNull String packageName) {
+ private byte[] getCertificate(
+ @NonNull PackageManager packageManager, @NonNull String packageName) {
+ Objects.requireNonNull(packageManager);
Objects.requireNonNull(packageName);
PackageInfo packageInfo;
try {
packageInfo =
Objects.requireNonNull(
- mPackageManager.getPackageInfo(
+ packageManager.getPackageInfo(
packageName,
PackageManager.GET_META_DATA
| PackageManager.GET_SIGNING_CERTIFICATES));
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java
new file mode 100644
index 0000000..f421527
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java
@@ -0,0 +1,80 @@
+/*
+ * 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.appfunctions;
+
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+
+/** A Singleton class that manages per-user metadata sync adapters. */
+public final class MetadataSyncPerUser {
+ private static final String TAG = MetadataSyncPerUser.class.getSimpleName();
+
+ /** A map of per-user adapter for synchronizing appFunction metadata. */
+ @GuardedBy("sLock")
+ private static final SparseArray<MetadataSyncAdapter> sPerUserMetadataSyncAdapter =
+ new SparseArray<>();
+
+ private static final Object sLock = new Object();
+
+ /**
+ * Returns the per-user metadata sync adapter for the given user.
+ *
+ * @param user The user for which to get the metadata sync adapter.
+ * @param userContext The user context for the given user.
+ * @return The metadata sync adapter for the given user.
+ */
+ @Nullable
+ public static MetadataSyncAdapter getPerUserMetadataSyncAdapter(
+ UserHandle user, Context userContext) {
+ synchronized (sLock) {
+ MetadataSyncAdapter metadataSyncAdapter =
+ sPerUserMetadataSyncAdapter.get(user.getIdentifier(), null);
+ if (metadataSyncAdapter == null) {
+ AppSearchManager perUserAppSearchManager =
+ userContext.getSystemService(AppSearchManager.class);
+ PackageManager perUserPackageManager = userContext.getPackageManager();
+ if (perUserAppSearchManager != null) {
+ metadataSyncAdapter =
+ new MetadataSyncAdapter(
+ AppFunctionExecutors.getPerUserSyncExecutor(user),
+ perUserPackageManager,
+ perUserAppSearchManager);
+ sPerUserMetadataSyncAdapter.put(user.getIdentifier(), metadataSyncAdapter);
+ return metadataSyncAdapter;
+ }
+ }
+ return metadataSyncAdapter;
+ }
+ }
+
+ /**
+ * Removes the per-user metadata sync adapter for the given user.
+ *
+ * @param user The user for which to remove the metadata sync adapter.
+ */
+ public static void removeUserSyncAdapter(UserHandle user) {
+ synchronized (sLock) {
+ sPerUserMetadataSyncAdapter.remove(user.getIdentifier());
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index e145c90..55d9c6e 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -533,7 +533,8 @@
AudioDeviceInfo.TYPE_BLE_SPEAKER,
AudioDeviceInfo.TYPE_LINE_ANALOG,
AudioDeviceInfo.TYPE_HDMI,
- AudioDeviceInfo.TYPE_AUX_LINE
+ AudioDeviceInfo.TYPE_AUX_LINE,
+ AudioDeviceInfo.TYPE_BUS
};
/*package */ static boolean isValidCommunicationDevice(@NonNull AudioDeviceInfo device) {
diff --git a/services/core/java/com/android/server/display/DisplayControl.java b/services/core/java/com/android/server/display/DisplayControl.java
index 38eb416..ddea285 100644
--- a/services/core/java/com/android/server/display/DisplayControl.java
+++ b/services/core/java/com/android/server/display/DisplayControl.java
@@ -109,7 +109,7 @@
/**
* Sets the HDR conversion mode for the device.
*
- * Returns the system preferred Hdr output type nn case when HDR conversion mode is
+ * Returns the system preferred HDR output type in case when HDR conversion mode is
* {@link android.hardware.display.HdrConversionMode#HDR_CONVERSION_SYSTEM}.
* Returns Hdr::INVALID in other cases.
* @hide
diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
index 93bd926..acf4db3 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
@@ -318,13 +318,16 @@
*/
public Display.HdrCapabilities hdrCapabilities;
+ /** When true, all HDR capabilities are hidden from public APIs */
+ public boolean isForceSdr;
+
/**
* Indicates whether this display supports Auto Low Latency Mode.
*/
public boolean allmSupported;
/**
- * Indicates whether this display suppors Game content type.
+ * Indicates whether this display supports Game content type.
*/
public boolean gameContentTypeSupported;
@@ -516,6 +519,7 @@
|| !Arrays.equals(supportedModes, other.supportedModes)
|| !Arrays.equals(supportedColorModes, other.supportedColorModes)
|| !Objects.equals(hdrCapabilities, other.hdrCapabilities)
+ || isForceSdr != other.isForceSdr
|| allmSupported != other.allmSupported
|| gameContentTypeSupported != other.gameContentTypeSupported
|| densityDpi != other.densityDpi
@@ -560,6 +564,7 @@
colorMode = other.colorMode;
supportedColorModes = other.supportedColorModes;
hdrCapabilities = other.hdrCapabilities;
+ isForceSdr = other.isForceSdr;
allmSupported = other.allmSupported;
gameContentTypeSupported = other.gameContentTypeSupported;
densityDpi = other.densityDpi;
@@ -603,6 +608,7 @@
sb.append(", colorMode ").append(colorMode);
sb.append(", supportedColorModes ").append(Arrays.toString(supportedColorModes));
sb.append(", hdrCapabilities ").append(hdrCapabilities);
+ sb.append(", isForceSdr ").append(isForceSdr);
sb.append(", allmSupported ").append(allmSupported);
sb.append(", gameContentTypeSupported ").append(gameContentTypeSupported);
sb.append(", density ").append(densityDpi);
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 3c2167e..e7fd8f7 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -48,6 +48,7 @@
import static android.provider.Settings.Secure.RESOLUTION_MODE_FULL;
import static android.provider.Settings.Secure.RESOLUTION_MODE_HIGH;
import static android.provider.Settings.Secure.RESOLUTION_MODE_UNKNOWN;
+import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID;
import static com.android.server.display.layout.Layout.Display.POSITION_REAR;
@@ -284,7 +285,7 @@
@GuardedBy("mSyncRoot")
private int[] mUserDisabledHdrTypes = {};
@Display.HdrCapabilities.HdrType
- private int[] mSupportedHdrOutputType;
+ private int[] mSupportedHdrOutputTypes;
@GuardedBy("mSyncRoot")
private boolean mAreUserDisabledHdrTypesAllowed = true;
@@ -299,10 +300,10 @@
// HDR conversion mode chosen by user
@GuardedBy("mSyncRoot")
private HdrConversionMode mHdrConversionMode = null;
- // Actual HDR conversion mode, which takes app overrides into account.
- private HdrConversionMode mOverrideHdrConversionMode = null;
+ // Whether app has disabled HDR conversion
+ private boolean mShouldDisableHdrConversion = false;
@GuardedBy("mSyncRoot")
- private int mSystemPreferredHdrOutputType = Display.HdrCapabilities.HDR_TYPE_INVALID;
+ private int mSystemPreferredHdrOutputType = HDR_TYPE_INVALID;
// The synchronization root for the display manager.
@@ -1419,7 +1420,8 @@
}
}
- private void setUserDisabledHdrTypesInternal(int[] userDisabledHdrTypes) {
+ @VisibleForTesting
+ void setUserDisabledHdrTypesInternal(int[] userDisabledHdrTypes) {
synchronized (mSyncRoot) {
if (userDisabledHdrTypes == null) {
Slog.e(TAG, "Null is not an expected argument to "
@@ -1437,6 +1439,7 @@
if (Arrays.equals(mUserDisabledHdrTypes, userDisabledHdrTypes)) {
return;
}
+
String userDisabledFormatsString = "";
if (userDisabledHdrTypes.length != 0) {
userDisabledFormatsString = TextUtils.join(",",
@@ -1452,6 +1455,15 @@
handleLogicalDisplayChangedLocked(display);
});
}
+ /* Note: it may be expected to reset the Conversion Mode when an HDR type is enabled
+ and the Conversion Mode is set to System Preferred. This is handled in the Settings
+ code because in the special case where HDR is indirectly disabled by Force SDR
+ Conversion, manually enabling HDR is not recognized as an action that reduces the
+ disabled HDR count. Thus, this case needs to be checked in the Settings code when we
+ know we're enabling an HDR mode. If we split checking for SystemConversion and
+ isForceSdr in two places, we may have duplicate calls to resetting to System Conversion
+ and get two black screens.
+ */
}
}
@@ -1464,19 +1476,20 @@
return true;
}
- private void setAreUserDisabledHdrTypesAllowedInternal(
+ @VisibleForTesting
+ void setAreUserDisabledHdrTypesAllowedInternal(
boolean areUserDisabledHdrTypesAllowed) {
synchronized (mSyncRoot) {
if (mAreUserDisabledHdrTypesAllowed == areUserDisabledHdrTypesAllowed) {
return;
}
mAreUserDisabledHdrTypesAllowed = areUserDisabledHdrTypesAllowed;
- if (mUserDisabledHdrTypes.length == 0) {
- return;
- }
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.ARE_USER_DISABLED_HDR_FORMATS_ALLOWED,
areUserDisabledHdrTypesAllowed ? 1 : 0);
+ if (mUserDisabledHdrTypes.length == 0) {
+ return;
+ }
int userDisabledHdrTypes[] = {};
if (!mAreUserDisabledHdrTypesAllowed) {
userDisabledHdrTypes = mUserDisabledHdrTypes;
@@ -1487,6 +1500,14 @@
display.setUserDisabledHdrTypes(finalUserDisabledHdrTypes);
handleLogicalDisplayChangedLocked(display);
});
+ // When HDR conversion mode is set to SYSTEM, modification to
+ // areUserDisabledHdrTypesAllowed requires refreshing the HDR conversion mode to tell
+ // the system which HDR types it is not allowed to use.
+ if (getHdrConversionModeInternal().getConversionMode()
+ == HdrConversionMode.HDR_CONVERSION_SYSTEM) {
+ setHdrConversionModeInternal(
+ new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM));
+ }
}
}
@@ -2357,7 +2378,7 @@
final int preferredHdrOutputType =
hdrConversionMode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_FORCE
? hdrConversionMode.getPreferredHdrOutputType()
- : Display.HdrCapabilities.HDR_TYPE_INVALID;
+ : HDR_TYPE_INVALID;
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.HDR_FORCE_CONVERSION_TYPE, preferredHdrOutputType);
}
@@ -2370,7 +2391,7 @@
? Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.HDR_FORCE_CONVERSION_TYPE,
Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION)
- : Display.HdrCapabilities.HDR_TYPE_INVALID;
+ : HDR_TYPE_INVALID;
mHdrConversionMode = new HdrConversionMode(conversionMode, preferredHdrOutputType);
setHdrConversionModeInternal(mHdrConversionMode);
}
@@ -2507,22 +2528,38 @@
});
}
+ /**
+ * Returns the HDR output types that are supported by the device's HDR conversion capabilities,
+ * stripping out any user-disabled HDR types if mAreUserDisabledHdrTypesAllowed is false.
+ */
@GuardedBy("mSyncRoot")
- private int[] getEnabledAutoHdrTypesLocked() {
- IntArray autoHdrOutputTypesArray = new IntArray();
+ @VisibleForTesting
+ int[] getEnabledHdrOutputTypesLocked() {
+ if (mAreUserDisabledHdrTypesAllowed) {
+ return getSupportedHdrOutputTypesInternal();
+ }
+ // Strip out all HDR formats that are currently user-disabled
+ IntArray enabledHdrOutputTypesArray = new IntArray();
for (int type : getSupportedHdrOutputTypesInternal()) {
- boolean isDisabled = false;
+ boolean isEnabled = true;
for (int disabledType : mUserDisabledHdrTypes) {
if (type == disabledType) {
- isDisabled = true;
+ isEnabled = false;
break;
}
}
- if (!isDisabled) {
- autoHdrOutputTypesArray.add(type);
+ if (isEnabled) {
+ enabledHdrOutputTypesArray.add(type);
}
}
- return autoHdrOutputTypesArray.toArray();
+ return enabledHdrOutputTypesArray.toArray();
+ }
+
+ @VisibleForTesting
+ int[] getEnabledHdrOutputTypes() {
+ synchronized (mSyncRoot) {
+ return getEnabledHdrOutputTypesLocked();
+ }
}
@GuardedBy("mSyncRoot")
@@ -2531,7 +2568,7 @@
final int preferredHdrOutputType =
mode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_SYSTEM
? mSystemPreferredHdrOutputType : mode.getPreferredHdrOutputType();
- if (preferredHdrOutputType != Display.HdrCapabilities.HDR_TYPE_INVALID) {
+ if (preferredHdrOutputType != HDR_TYPE_INVALID) {
int[] hdrTypesWithLatency = mInjector.getHdrOutputTypesWithLatency();
return ArrayUtils.contains(hdrTypesWithLatency, preferredHdrOutputType);
}
@@ -2565,41 +2602,57 @@
if (!mInjector.getHdrOutputConversionSupport()) {
return;
}
- int[] autoHdrOutputTypes = null;
+
synchronized (mSyncRoot) {
if (hdrConversionMode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_SYSTEM
&& hdrConversionMode.getPreferredHdrOutputType()
- != Display.HdrCapabilities.HDR_TYPE_INVALID) {
+ != HDR_TYPE_INVALID) {
throw new IllegalArgumentException("preferredHdrOutputType must not be set if"
+ " the conversion mode is HDR_CONVERSION_SYSTEM");
}
mHdrConversionMode = hdrConversionMode;
storeHdrConversionModeLocked(mHdrConversionMode);
- // For auto mode, all supported HDR types are allowed except the ones specifically
- // disabled by the user.
+ // If the HDR conversion is HDR_CONVERSION_SYSTEM, all supported HDR types are allowed
+ // except the ones specifically disabled by the user.
+ int[] enabledHdrOutputTypes = null;
if (hdrConversionMode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_SYSTEM) {
- autoHdrOutputTypes = getEnabledAutoHdrTypesLocked();
+ enabledHdrOutputTypes = getEnabledHdrOutputTypesLocked();
}
int conversionMode = hdrConversionMode.getConversionMode();
int preferredHdrType = hdrConversionMode.getPreferredHdrOutputType();
+
// If the HDR conversion is disabled by an app through WindowManager.LayoutParams, then
// set HDR conversion mode to HDR_CONVERSION_PASSTHROUGH.
- if (mOverrideHdrConversionMode == null) {
- // HDR_CONVERSION_FORCE with HDR_TYPE_INVALID is used to represent forcing SDR type.
- // But, internally SDR is selected by using passthrough mode.
- if (conversionMode == HdrConversionMode.HDR_CONVERSION_FORCE
- && preferredHdrType == Display.HdrCapabilities.HDR_TYPE_INVALID) {
- conversionMode = HdrConversionMode.HDR_CONVERSION_PASSTHROUGH;
- }
+ if (mShouldDisableHdrConversion) {
+ conversionMode = HdrConversionMode.HDR_CONVERSION_PASSTHROUGH;
+ preferredHdrType = -1;
+ enabledHdrOutputTypes = null;
} else {
- conversionMode = mOverrideHdrConversionMode.getConversionMode();
- preferredHdrType = mOverrideHdrConversionMode.getPreferredHdrOutputType();
- autoHdrOutputTypes = null;
+ // HDR_CONVERSION_FORCE with HDR_TYPE_INVALID is used to represent forcing SDR type.
+ // But, internally SDR is forced by using passthrough mode and not reporting any
+ // HDR capabilities to apps.
+ if (conversionMode == HdrConversionMode.HDR_CONVERSION_FORCE
+ && preferredHdrType == HDR_TYPE_INVALID) {
+ conversionMode = HdrConversionMode.HDR_CONVERSION_PASSTHROUGH;
+ mLogicalDisplayMapper.forEachLocked(
+ logicalDisplay -> {
+ if (logicalDisplay.setIsForceSdr(true)) {
+ handleLogicalDisplayChangedLocked(logicalDisplay);
+ }
+ });
+ } else {
+ mLogicalDisplayMapper.forEachLocked(
+ logicalDisplay -> {
+ if (logicalDisplay.setIsForceSdr(false)) {
+ handleLogicalDisplayChangedLocked(logicalDisplay);
+ }
+ });
+ }
}
mSystemPreferredHdrOutputType = mInjector.setHdrConversionMode(
- conversionMode, preferredHdrType, autoHdrOutputTypes);
+ conversionMode, preferredHdrType, enabledHdrOutputTypes);
}
}
@@ -2621,8 +2674,8 @@
}
HdrConversionMode mode;
synchronized (mSyncRoot) {
- mode = mOverrideHdrConversionMode != null
- ? mOverrideHdrConversionMode
+ mode = mShouldDisableHdrConversion
+ ? new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_PASSTHROUGH)
: mHdrConversionMode;
// Handle default: PASSTHROUGH. Don't include the system-preferred type.
if (mode == null
@@ -2630,8 +2683,6 @@
return new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_PASSTHROUGH);
}
// Handle default or current mode: SYSTEM. Include the system preferred type.
- // mOverrideHdrConversionMode and mHdrConversionMode do not include the system
- // preferred type, it is kept separately in mSystemPreferredHdrOutputType.
if (mode == null
|| mode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_SYSTEM) {
return new HdrConversionMode(
@@ -2642,10 +2693,10 @@
}
private @Display.HdrCapabilities.HdrType int[] getSupportedHdrOutputTypesInternal() {
- if (mSupportedHdrOutputType == null) {
- mSupportedHdrOutputType = mInjector.getSupportedHdrOutputTypes();
+ if (mSupportedHdrOutputTypes == null) {
+ mSupportedHdrOutputTypes = mInjector.getSupportedHdrOutputTypes();
}
- return mSupportedHdrOutputType;
+ return mSupportedHdrOutputTypes;
}
void setShouldAlwaysRespectAppRequestedModeInternal(boolean enabled) {
@@ -2831,15 +2882,9 @@
// HDR conversion is disabled in two cases:
// - HDR conversion introduces latency and minimal post-processing is requested
// - app requests to disable HDR conversion
- if (mOverrideHdrConversionMode == null && (disableHdrConversion
- || disableHdrConversionForLatency)) {
- mOverrideHdrConversionMode =
- new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_PASSTHROUGH);
- setHdrConversionModeInternal(mHdrConversionMode);
- handleLogicalDisplayChangedLocked(display);
- } else if (mOverrideHdrConversionMode != null && !disableHdrConversion
- && !disableHdrConversionForLatency) {
- mOverrideHdrConversionMode = null;
+ boolean previousShouldDisableHdrConversion = mShouldDisableHdrConversion;
+ mShouldDisableHdrConversion = disableHdrConversion || disableHdrConversionForLatency;
+ if (previousShouldDisableHdrConversion != mShouldDisableHdrConversion) {
setHdrConversionModeInternal(mHdrConversionMode);
handleLogicalDisplayChangedLocked(display);
}
@@ -3530,9 +3575,9 @@
}
int setHdrConversionMode(int conversionMode, int preferredHdrOutputType,
- int[] autoHdrTypes) {
+ int[] allowedHdrOutputTypes) {
return DisplayControl.setHdrConversionMode(conversionMode, preferredHdrOutputType,
- autoHdrTypes);
+ allowedHdrOutputTypes);
}
@Display.HdrCapabilities.HdrType
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index e8be8a4..007e3a8 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -518,6 +518,7 @@
deviceInfo.supportedColorModes,
deviceInfo.supportedColorModes.length);
mBaseDisplayInfo.hdrCapabilities = deviceInfo.hdrCapabilities;
+ mBaseDisplayInfo.isForceSdr = deviceInfo.isForceSdr;
mBaseDisplayInfo.userDisabledHdrTypes = mUserDisabledHdrTypes;
mBaseDisplayInfo.minimalPostProcessingSupported =
deviceInfo.allmSupported || deviceInfo.gameContentTypeSupported;
@@ -899,6 +900,29 @@
}
/**
+ * Checks whether display is of the type where HDR settings are relevant, and then sets
+ * whether Force SDR conversion mode is active. isForceSdr is checked by the Display when
+ * returning HDR capabilities.
+ *
+ * @param isForceSdr Whether Force SDR conversion mode is active
+ * @return Whether Display Manager should call handleLogicalDisplayChangedLocked()
+ */
+ public boolean setIsForceSdr(boolean isForceSdr) {
+ int displayType = getDisplayInfoLocked().type;
+ boolean isTargetDisplayType = displayType == Display.TYPE_INTERNAL
+ || displayType == Display.TYPE_EXTERNAL
+ || displayType == Display.TYPE_OVERLAY;
+
+ boolean handleLogicalDisplayChangedLocked = false;
+ if (isTargetDisplayType && mBaseDisplayInfo.isForceSdr != isForceSdr) {
+ mBaseDisplayInfo.isForceSdr = isForceSdr;
+ mInfo.set(null);
+ handleLogicalDisplayChangedLocked = true;
+ }
+ return handleLogicalDisplayChangedLocked;
+ }
+
+ /**
* Swap the underlying {@link DisplayDevice} with the specified LogicalDisplay.
*
* @param targetDisplay The display with which to swap display-devices.
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 164b230..ac75ef7 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -1380,7 +1380,8 @@
@ServiceThreadOnly
private List<Integer> getCecLocalDeviceTypes() {
ArrayList<Integer> allLocalDeviceTypes = new ArrayList<>(mCecLocalDevices);
- if (isDsmEnabled() && !allLocalDeviceTypes.contains(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM)
+ if (!isTvDevice() && isDsmEnabled()
+ && !allLocalDeviceTypes.contains(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM)
&& isArcSupported() && mSoundbarModeFeatureFlagEnabled) {
allLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
}
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
index 3f11e78..5ff8568 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
@@ -29,7 +29,9 @@
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
+import android.view.SurfaceControl;
import android.view.ViewConfiguration;
+import android.view.ViewRootImpl;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -49,6 +51,7 @@
private static final float DEFAULT_RES_X = 47f;
private static final float DEFAULT_RES_Y = 45f;
private static final int TEXT_PADDING_DP = 12;
+ private static final int ROUNDED_CORNER_RADIUS_DP = 24;
/**
* Input device ID for the touchpad that this debug view is displaying.
@@ -152,6 +155,30 @@
}
@Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ postDelayed(() -> {
+ final ViewRootImpl viewRootImpl = getRootView().getViewRootImpl();
+ if (viewRootImpl == null) {
+ Slog.d("TouchpadDebugView", "ViewRootImpl is null.");
+ return;
+ }
+
+ SurfaceControl surfaceControl = viewRootImpl.getSurfaceControl();
+ if (surfaceControl != null && surfaceControl.isValid()) {
+ try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) {
+ transaction.setCornerRadius(surfaceControl,
+ TypedValue.applyDimension(COMPLEX_UNIT_DIP,
+ ROUNDED_CORNER_RADIUS_DP,
+ getResources().getDisplayMetrics())).apply();
+ }
+ } else {
+ Slog.d("TouchpadDebugView", "SurfaceControl is invalid or has been released.");
+ }
+ }, 100);
+ }
+
+ @Override
public boolean onTouchEvent(MotionEvent event) {
float deltaX;
float deltaY;
diff --git a/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java b/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java
index 67c3621..2eed9ba 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java
@@ -27,20 +27,28 @@
import com.android.server.input.TouchpadHardwareProperties;
import com.android.server.input.TouchpadHardwareState;
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.Map;
+
public class TouchpadVisualizationView extends View {
private static final String TAG = "TouchpadVizMain";
private static final boolean DEBUG = true;
private static final float DEFAULT_RES_X = 47f;
private static final float DEFAULT_RES_Y = 45f;
+ private static final float MAX_TRACE_HISTORY_DURATION_SECONDS = 1f;
private final TouchpadHardwareProperties mTouchpadHardwareProperties;
private float mScaleFactor;
- TouchpadHardwareState mLatestHardwareState = new TouchpadHardwareState(0, 0, 0, 0,
- new TouchpadFingerState[]{});
+ private final ArrayDeque<TouchpadHardwareState> mHardwareStateHistory =
+ new ArrayDeque<TouchpadHardwareState>();
+ private final Map<Integer, TouchpadFingerState> mTempFingerStatesByTrackingId = new HashMap<>();
private final Paint mOvalStrokePaint;
private final Paint mOvalFillPaint;
+ private final Paint mTracePaint;
+ private final Paint mCenterPointPaint;
private final RectF mTempOvalRect = new RectF();
public TouchpadVisualizationView(Context context,
@@ -55,6 +63,29 @@
mOvalFillPaint = new Paint();
mOvalFillPaint.setAntiAlias(true);
mOvalFillPaint.setARGB(255, 0, 0, 0);
+ mTracePaint = new Paint();
+ mTracePaint.setAntiAlias(false);
+ mTracePaint.setARGB(255, 0, 0, 255);
+ mTracePaint.setStyle(Paint.Style.STROKE);
+ mTracePaint.setStrokeWidth(2);
+ mCenterPointPaint = new Paint();
+ mCenterPointPaint.setAntiAlias(true);
+ mCenterPointPaint.setARGB(255, 255, 0, 0);
+ mCenterPointPaint.setStrokeWidth(2);
+ }
+
+ private void removeOldPoints() {
+ float latestTimestamp = mHardwareStateHistory.getLast().getTimestamp();
+
+ while (!mHardwareStateHistory.isEmpty()) {
+ TouchpadHardwareState oldestPoint = mHardwareStateHistory.getFirst();
+ float onScreenTime = latestTimestamp - oldestPoint.getTimestamp();
+ if (onScreenTime >= MAX_TRACE_HISTORY_DURATION_SECONDS) {
+ mHardwareStateHistory.removeFirst();
+ } else {
+ break;
+ }
+ }
}
private void drawOval(Canvas canvas, float x, float y, float major, float minor, float angle) {
@@ -71,19 +102,22 @@
@Override
protected void onDraw(Canvas canvas) {
+ if (mHardwareStateHistory.isEmpty()) {
+ return;
+ }
+
+ TouchpadHardwareState latestHardwareState = mHardwareStateHistory.getLast();
+
float maximumPressure = 0;
- for (TouchpadFingerState touchpadFingerState : mLatestHardwareState.getFingerStates()) {
+ for (TouchpadFingerState touchpadFingerState : latestHardwareState.getFingerStates()) {
maximumPressure = Math.max(maximumPressure, touchpadFingerState.getPressure());
}
- for (TouchpadFingerState touchpadFingerState : mLatestHardwareState.getFingerStates()) {
- float newX = translateRange(mTouchpadHardwareProperties.getLeft(),
- mTouchpadHardwareProperties.getRight(), 0, getWidth(),
- touchpadFingerState.getPositionX());
+ // Visualizing fingers as ovals
+ for (TouchpadFingerState touchpadFingerState : latestHardwareState.getFingerStates()) {
+ float newX = translateX(touchpadFingerState.getPositionX());
- float newY = translateRange(mTouchpadHardwareProperties.getTop(),
- mTouchpadHardwareProperties.getBottom(), 0, getHeight(),
- touchpadFingerState.getPositionY());
+ float newY = translateY(touchpadFingerState.getPositionY());
float newAngle = translateRange(0, mTouchpadHardwareProperties.getOrientationMaximum(),
0, 90, touchpadFingerState.getOrientation());
@@ -102,6 +136,28 @@
drawOval(canvas, newX, newY, newTouchMajor, newTouchMinor, newAngle);
}
+
+ mTempFingerStatesByTrackingId.clear();
+
+ // Drawing the trace
+ for (TouchpadHardwareState currentHardwareState : mHardwareStateHistory) {
+ for (TouchpadFingerState currentFingerState : currentHardwareState.getFingerStates()) {
+ TouchpadFingerState prevFingerState = mTempFingerStatesByTrackingId.put(
+ currentFingerState.getTrackingId(), currentFingerState);
+
+ if (prevFingerState == null) {
+ continue;
+ }
+
+ float currentX = translateX(currentFingerState.getPositionX());
+ float currentY = translateY(currentFingerState.getPositionY());
+ float prevX = translateX(prevFingerState.getPositionX());
+ float prevY = translateY(prevFingerState.getPositionY());
+
+ canvas.drawLine(prevX, prevY, currentX, currentY, mTracePaint);
+ canvas.drawPoint(currentX, currentY, mCenterPointPaint);
+ }
+ }
}
/**
@@ -114,7 +170,18 @@
logHardwareState(schs);
}
- mLatestHardwareState = schs;
+ if (!mHardwareStateHistory.isEmpty()
+ && mHardwareStateHistory.getLast().getFingerCount() == 0
+ && schs.getFingerCount() > 0) {
+ mHardwareStateHistory.clear();
+ }
+
+ mHardwareStateHistory.addLast(schs);
+ removeOldPoints();
+
+ if (DEBUG) {
+ logFingerTrace();
+ }
invalidate();
}
@@ -128,6 +195,16 @@
mScaleFactor = scaleFactor;
}
+ private float translateX(float x) {
+ return translateRange(mTouchpadHardwareProperties.getLeft(),
+ mTouchpadHardwareProperties.getRight(), 0, getWidth(), x);
+ }
+
+ private float translateY(float y) {
+ return translateRange(mTouchpadHardwareProperties.getTop(),
+ mTouchpadHardwareProperties.getBottom(), 0, getHeight(), y);
+ }
+
private float translateRange(float rangeBeforeMin, float rangeBeforeMax,
float rangeAfterMin, float rangeAfterMax, float value) {
return rangeAfterMin + (value - rangeBeforeMin) / (rangeBeforeMax - rangeBeforeMin) * (
@@ -154,4 +231,10 @@
}
}
-}
+ private void logFingerTrace() {
+ Slog.d(TAG, "Trace size= " + mHardwareStateHistory.size());
+ for (TouchpadFingerState tfs : mHardwareStateHistory.getLast().getFingerStates()) {
+ Slog.d(TAG, "ID= " + tfs.getTrackingId());
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/notification/ZenModeConditions.java b/services/core/java/com/android/server/notification/ZenModeConditions.java
index d495ef5..50bfbc3 100644
--- a/services/core/java/com/android/server/notification/ZenModeConditions.java
+++ b/services/core/java/com/android/server/notification/ZenModeConditions.java
@@ -157,7 +157,7 @@
}
// empty rule? disable and bail early
if (rule.component == null && rule.enabler == null) {
- if (!android.app.Flags.modesUi() || (android.app.Flags.modesUi() && !isManual)) {
+ if (!isManual) {
Log.w(TAG, "No component found for automatic rule: " + rule.conditionId);
rule.enabled = false;
}
diff --git a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java
index 4135161..5aea356 100644
--- a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java
+++ b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java
@@ -18,6 +18,7 @@
import static android.media.AudioAttributes.USAGE_ALARM;
+import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.Notification;
@@ -42,6 +43,8 @@
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
+import java.util.List;
+
public class BackgroundUserSoundNotifier {
private static final boolean DEBUG = false;
@@ -49,11 +52,21 @@
private static final String BUSN_CHANNEL_ID = "bg_user_sound_channel";
private static final String BUSN_CHANNEL_NAME = "BackgroundUserSound";
public static final String ACTION_MUTE_SOUND = "com.android.server.ACTION_MUTE_BG_USER";
- private static final String EXTRA_NOTIFICATION_ID = "com.android.server.EXTRA_CLIENT_UID";
- private static final String EXTRA_CURRENT_USER_ID = "com.android.server.EXTRA_CURRENT_USER_ID";
private static final String ACTION_SWITCH_USER = "com.android.server.ACTION_SWITCH_TO_USER";
- /** ID of user with notification displayed, -1 if notification is not showing*/
- private int mUserWithNotification = -1;
+ private static final String ACTION_DISMISS_NOTIFICATION =
+ "com.android.server.ACTION_DISMISS_NOTIFICATION";
+ /**
+ * The clientUid from the AudioFocusInfo of the background user,
+ * for which an active notification is currently displayed.
+ * Set to -1 if no notification is being shown.
+ * TODO: b/367615180 - add support for multiple simultaneous alarms
+ */
+ @VisibleForTesting
+ int mNotificationClientUid = -1;
+ @VisibleForTesting
+ AudioPolicy mFocusControlAudioPolicy;
+ @VisibleForTesting
+ BackgroundUserListener mBgUserListener;
private final Context mSystemUserContext;
@VisibleForTesting
final NotificationManager mNotificationManager;
@@ -67,11 +80,18 @@
mSystemUserContext = context;
mNotificationManager = mSystemUserContext.getSystemService(NotificationManager.class);
mUserManager = mSystemUserContext.getSystemService(UserManager.class);
+ createNotificationChannel();
+ setupFocusControlAudioPolicy();
+ }
+
+ /**
+ * Creates a dedicated channel for background user related notifications.
+ */
+ private void createNotificationChannel() {
NotificationChannel channel = new NotificationChannel(BUSN_CHANNEL_ID, BUSN_CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH);
channel.setSound(null, null);
mNotificationManager.createNotificationChannel(channel);
- setupFocusControlAudioPolicy();
}
private void setupFocusControlAudioPolicy() {
@@ -81,15 +101,16 @@
ActivityManager am = mSystemUserContext.getSystemService(ActivityManager.class);
registerReceiver(am);
- BackgroundUserListener bgUserListener = new BackgroundUserListener(mSystemUserContext);
+ mBgUserListener = new BackgroundUserListener(mSystemUserContext);
AudioPolicy.Builder focusControlPolicyBuilder = new AudioPolicy.Builder(mSystemUserContext);
focusControlPolicyBuilder.setLooper(Looper.getMainLooper());
- focusControlPolicyBuilder.setAudioPolicyFocusListener(bgUserListener);
+ focusControlPolicyBuilder.setAudioPolicyFocusListener(mBgUserListener);
- AudioPolicy mFocusControlAudioPolicy = focusControlPolicyBuilder.build();
+ mFocusControlAudioPolicy = focusControlPolicyBuilder.build();
int status = mSystemUserContext.getSystemService(AudioManager.class)
.registerAudioPolicy(mFocusControlAudioPolicy);
+
if (status != AudioManager.SUCCESS) {
Log.w(LOG_TAG , "Could not register the service's focus"
+ " control audio policy, error: " + status);
@@ -117,123 +138,170 @@
@SuppressLint("MissingPermission")
public void onAudioFocusLoss(AudioFocusInfo afi, boolean wasNotified) {
- BackgroundUserSoundNotifier.this.dismissNotificationIfNecessary(afi);
+ BackgroundUserSoundNotifier.this.dismissNotificationIfNecessary();
}
}
+ @VisibleForTesting
+ BackgroundUserListener getAudioPolicyFocusListener() {
+ return mBgUserListener;
+ }
+
/**
* Registers a BroadcastReceiver for actions related to background user sound notifications.
* When ACTION_MUTE_SOUND is received, it mutes a background user's alarm sound.
* When ACTION_SWITCH_USER is received, a switch to the background user with alarm is started.
*/
- private void registerReceiver(ActivityManager service) {
+ private void registerReceiver(ActivityManager activityManager) {
BroadcastReceiver backgroundUserNotificationBroadcastReceiver = new BroadcastReceiver() {
@SuppressLint("MissingPermission")
@Override
public void onReceive(Context context, Intent intent) {
- if (!(intent.hasExtra(EXTRA_NOTIFICATION_ID)
- && intent.hasExtra(EXTRA_CURRENT_USER_ID)
- && intent.hasExtra(Intent.EXTRA_USER_ID))) {
+ if (mNotificationClientUid == -1) {
return;
}
- final int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1);
+ dismissNotification();
if (DEBUG) {
- Log.d(LOG_TAG,
- "User with alarm id " + intent.getIntExtra(Intent.EXTRA_USER_ID,
- -1) + " current user id " + intent.getIntExtra(
- EXTRA_CURRENT_USER_ID, -1));
+ final int actionIndex = intent.getAction().lastIndexOf(".") + 1;
+ final String action = intent.getAction().substring(actionIndex);
+ Log.d(LOG_TAG, "Action requested: " + action + ", by userId "
+ + ActivityManager.getCurrentUser() + " for alarm on user "
+ + UserHandle.getUserHandleForUid(mNotificationClientUid));
}
- mUserWithNotification = -1;
- mNotificationManager.cancelAsUser(LOG_TAG, notificationId,
- UserHandle.of(intent.getIntExtra(EXTRA_CURRENT_USER_ID, -1)));
+
if (ACTION_MUTE_SOUND.equals(intent.getAction())) {
- final AudioManager audioManager =
- mSystemUserContext.getSystemService(AudioManager.class);
- if (audioManager != null) {
- for (AudioPlaybackConfiguration apc :
- audioManager.getActivePlaybackConfigurations()) {
- if (apc.getAudioAttributes().getUsage() == USAGE_ALARM) {
- if (apc.getPlayerProxy() != null) {
- apc.getPlayerProxy().stop();
- }
- }
- }
- }
+ muteAlarmSounds(mSystemUserContext);
} else if (ACTION_SWITCH_USER.equals(intent.getAction())) {
- service.switchUser(intent.getIntExtra(Intent.EXTRA_USER_ID, -1));
+ activityManager.switchUser(UserHandle.getUserId(mNotificationClientUid));
}
+
+ mNotificationClientUid = -1;
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_MUTE_SOUND);
filter.addAction(ACTION_SWITCH_USER);
+ filter.addAction(ACTION_DISMISS_NOTIFICATION);
mSystemUserContext.registerReceiver(backgroundUserNotificationBroadcastReceiver, filter,
Context.RECEIVER_NOT_EXPORTED);
}
/**
+ * Stop player proxy for the ongoing alarm and drop focus for its AudioFocusInfo.
+ */
+ @VisibleForTesting
+ void muteAlarmSounds(Context context) {
+ AudioManager audioManager = context.getSystemService(AudioManager.class);
+ if (audioManager != null) {
+ for (AudioPlaybackConfiguration apc : audioManager.getActivePlaybackConfigurations()) {
+ if (apc.getClientUid() == mNotificationClientUid && apc.getPlayerProxy() != null) {
+ apc.getPlayerProxy().stop();
+ }
+ }
+ }
+ }
+
+ /**
* Check if sound is coming from background user and show notification is required.
*/
@VisibleForTesting
- void notifyForegroundUserAboutSoundIfNecessary(AudioFocusInfo afi, Context
- foregroundContext) throws RemoteException {
+ void notifyForegroundUserAboutSoundIfNecessary(AudioFocusInfo afi, Context foregroundContext)
+ throws RemoteException {
final int userId = UserHandle.getUserId(afi.getClientUid());
final int usage = afi.getAttributes().getUsage();
UserInfo userInfo = mUserManager.getUserInfo(userId);
- if (userInfo != null && userId != foregroundContext.getUserId()) {
+ // Only show notification if the sound is coming from background user and the notification
+ // is not already shown.
+ if (userInfo != null && userId != foregroundContext.getUserId()
+ && mNotificationClientUid == -1) {
//TODO: b/349138482 - Add handling of cases when usage == USAGE_NOTIFICATION_RINGTONE
if (usage == USAGE_ALARM) {
- Intent muteIntent = createIntent(ACTION_MUTE_SOUND, afi, foregroundContext, userId);
- PendingIntent mutePI = PendingIntent.getBroadcast(mSystemUserContext, 0,
- muteIntent, PendingIntent.FLAG_UPDATE_CURRENT
- | PendingIntent.FLAG_IMMUTABLE);
- Intent switchIntent = createIntent(ACTION_SWITCH_USER, afi, foregroundContext,
- userId);
- PendingIntent switchPI = PendingIntent.getBroadcast(mSystemUserContext, 0,
- switchIntent, PendingIntent.FLAG_UPDATE_CURRENT
- | PendingIntent.FLAG_IMMUTABLE);
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Alarm ringing on background user " + userId
+ + ", displaying notification for current user "
+ + foregroundContext.getUserId());
+ }
- mUserWithNotification = foregroundContext.getUserId();
- mNotificationManager.notifyAsUser(LOG_TAG, afi.getClientUid(),
- createNotification(userInfo.name, mutePI, switchPI, foregroundContext),
+ mNotificationClientUid = afi.getClientUid();
+
+ mNotificationManager.notifyAsUser(LOG_TAG, mNotificationClientUid,
+ createNotification(userInfo.name, foregroundContext),
foregroundContext.getUser());
}
}
}
/**
- * If notification is present, dismisses it. To be called when the relevant sound loses focus.
+ * Dismisses notification if the associated focus has been removed from the focus stack.
+ * Notification remains if the focus is temporarily lost due to another client taking over the
+ * focus ownership.
*/
- private void dismissNotificationIfNecessary(AudioFocusInfo afi) {
- if (mUserWithNotification >= 0) {
- mNotificationManager.cancelAsUser(LOG_TAG, afi.getClientUid(),
- UserHandle.of(mUserWithNotification));
+ @VisibleForTesting
+ void dismissNotificationIfNecessary() {
+ if (getAudioFocusInfoForNotification() == null && mNotificationClientUid >= 0) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Alarm ringing on background user "
+ + UserHandle.getUserHandleForUid(mNotificationClientUid).getIdentifier()
+ + " left focus stack, dismissing notification");
+ }
+ dismissNotification();
+ mNotificationClientUid = -1;
}
- mUserWithNotification = -1;
}
- private Intent createIntent(String intentAction, AudioFocusInfo afi, Context fgUserContext,
- int userId) {
+ /**
+ * Dismisses notification for all users in case user switch occurred after notification was
+ * shown.
+ */
+ @SuppressLint("MissingPermission")
+ private void dismissNotification() {
+ mNotificationManager.cancelAsUser(LOG_TAG, mNotificationClientUid, UserHandle.ALL);
+ }
+
+ /**
+ * Returns AudioFocusInfo associated with the current notification.
+ */
+ @SuppressLint("MissingPermission")
+ @VisibleForTesting
+ @Nullable
+ AudioFocusInfo getAudioFocusInfoForNotification() {
+ if (mNotificationClientUid >= 0) {
+ List<AudioFocusInfo> stack = mFocusControlAudioPolicy.getFocusStack();
+ for (int i = stack.size() - 1; i >= 0; i--) {
+ if (stack.get(i).getClientUid() == mNotificationClientUid) {
+ return stack.get(i);
+ }
+ }
+ }
+ return null;
+ }
+
+ private PendingIntent createPendingIntent(String intentAction) {
final Intent intent = new Intent(intentAction);
- intent.putExtra(EXTRA_CURRENT_USER_ID, fgUserContext.getUserId());
- intent.putExtra(EXTRA_NOTIFICATION_ID, afi.getClientUid());
- intent.putExtra(Intent.EXTRA_USER_ID, userId);
- return intent;
+ PendingIntent resultPI = PendingIntent.getBroadcast(mSystemUserContext, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+ return resultPI;
}
- private Notification createNotification(String userName, PendingIntent muteIntent,
- PendingIntent switchIntent, Context fgContext) {
+ @VisibleForTesting
+ Notification createNotification(String userName, Context fgContext) {
final String title = fgContext.getString(R.string.bg_user_sound_notification_title_alarm,
userName);
final int icon = R.drawable.ic_audio_alarm;
+
+ PendingIntent mutePI = createPendingIntent(ACTION_MUTE_SOUND);
+ PendingIntent switchPI = createPendingIntent(ACTION_SWITCH_USER);
+ PendingIntent dismissNotificationPI = createPendingIntent(ACTION_DISMISS_NOTIFICATION);
+
final Notification.Action mute = new Notification.Action.Builder(null,
fgContext.getString(R.string.bg_user_sound_notification_button_mute),
- muteIntent).build();
+ mutePI).build();
final Notification.Action switchUser = new Notification.Action.Builder(null,
fgContext.getString(R.string.bg_user_sound_notification_button_switch_user),
- switchIntent).build();
+ switchPI).build();
+
Notification.Builder notificationBuilder = new Notification.Builder(mSystemUserContext,
BUSN_CHANNEL_ID)
.setSmallIcon(icon)
@@ -243,16 +311,18 @@
.setOngoing(true)
.setColor(fgContext.getColor(R.color.system_notification_accent_color))
.setContentTitle(title)
- .setContentIntent(muteIntent)
+ .setContentIntent(mutePI)
.setAutoCancel(true)
+ .setDeleteIntent(dismissNotificationPI)
.setVisibility(Notification.VISIBILITY_PUBLIC);
+
if (mUserManager.isUserSwitcherEnabled() && (mUserManager.getUserSwitchability(
- UserHandle.of(fgContext.getUserId())) == UserManager.SWITCHABILITY_STATUS_OK)) {
+ fgContext.getUser()) == UserManager.SWITCHABILITY_STATUS_OK)) {
notificationBuilder.setActions(mute, switchUser);
} else {
notificationBuilder.setActions(mute);
}
+
return notificationBuilder.build();
}
}
-
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index ee15bec..efd58ed 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -92,7 +92,6 @@
import android.multiuser.Flags;
import android.net.Uri;
import android.os.Binder;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IInterface;
@@ -215,7 +214,7 @@
@VisibleForTesting
static class LauncherAppsImpl extends ILauncherApps.Stub {
- private static final boolean DEBUG = Build.IS_DEBUGGABLE;
+ private static final boolean DEBUG = false;
private static final String TAG = "LauncherAppsService";
private static final String NAMESPACE_MULTIUSER = "multiuser";
private static final String FLAG_NON_SYSTEM_ACCESS_TO_HIDDEN_PROFILES =
@@ -496,28 +495,8 @@
private boolean canAccessProfile(int callingUid, int callingUserId, int callingPid,
int targetUserId, String message) {
- if (DEBUG) {
- final AndroidPackage callingPackage =
- mPackageManagerInternal.getPackage(callingUid);
- final String callingPackageName = callingPackage == null
- ? null : callingPackage.getPackageName();
- Slog.v(TAG, "canAccessProfile called by " + callingPackageName
- + " for user " + callingUserId
- + " requesting to access user "
- + targetUserId + " when invoking " + message);
- }
- if (targetUserId == callingUserId) {
- if (DEBUG) {
- Slog.v(TAG, message + " passed canAccessProfile for targetuser"
- + targetUserId + " because it is the same as the calling user");
- }
- return true;
- }
+ if (targetUserId == callingUserId) return true;
if (injectHasInteractAcrossUsersFullPermission(callingPid, callingUid)) {
- if (DEBUG) {
- Slog.v(TAG, message + " passed because calling process"
- + "has permission to interact across users");
- }
return true;
}
@@ -535,25 +514,11 @@
if (isHiddenProfile(UserHandle.of(targetUserId))
&& !canAccessHiddenProfile(callingUid, callingPid)) {
- Slog.w(TAG, message + " for hidden profile user " + targetUserId
- + " from " + callingUserId + " not allowed");
-
return false;
}
- final boolean ret = mUserManagerInternal.isProfileAccessible(
- callingUserId, targetUserId, message, true);
- if (DEBUG) {
- final AndroidPackage callingPackage =
- mPackageManagerInternal.getPackage(callingUid);
- final String callingPackageName = callingPackage == null
- ? null : callingPackage.getPackageName();
- Slog.v(TAG, "canAccessProfile returned " + ret + " for " + callingPackageName
- + " for user " + callingUserId
- + " requesting to access user "
- + targetUserId + " when invoking " + message);
- }
- return ret;
+ return mUserManagerInternal.isProfileAccessible(callingUserId, targetUserId,
+ message, true);
}
private boolean isHiddenProfile(UserHandle targetUser) {
@@ -1376,10 +1341,6 @@
@Override
public void pinShortcuts(String callingPackage, String packageName, List<String> ids,
UserHandle targetUser) {
- if (DEBUG) {
- Slog.v(TAG, "pinShortcuts: " + callingPackage + " is pinning shortcuts from "
- + packageName + " for user " + targetUser);
- }
if (!mShortcutServiceInternal
.areShortcutsSupportedOnHomeScreen(targetUser.getIdentifier())) {
// Requires strict ACCESS_SHORTCUTS permission for user-profiles with items
@@ -1390,11 +1351,6 @@
}
ensureShortcutPermission(callingPackage);
if (!canAccessProfile(targetUser.getIdentifier(), "Cannot pin shortcuts")) {
- if (DEBUG) {
- Slog.v(TAG, "pinShortcuts: " + callingPackage
- + " is pinning shortcuts from " + packageName
- + " for user " + targetUser + " but cannot access profile");
- }
return;
}
@@ -2451,7 +2407,7 @@
final int callbackUserId = callbackUser.getIdentifier();
final int shortcutUserId = shortcutUser.getIdentifier();
- if (shortcutUser == callbackUser) return true;
+ if ((shortcutUser.equals(callbackUser))) return true;
return mUserManagerInternal.isProfileAccessible(callbackUserId, shortcutUserId,
null, false);
}
@@ -2485,16 +2441,28 @@
final BroadcastCookie cookie =
(BroadcastCookie) mListeners.getBroadcastCookie(i);
if (!isEnabledProfileOf(cookie, user, "onPackageRemoved")) {
+ // b/350144057
+ Slog.d(TAG, "onPackageRemoved: Skipping - profile not enabled"
+ + " or not accessible for user=" + user
+ + ", packageName=" + packageName);
continue;
}
if (!isCallingAppIdAllowed(appIdAllowList, UserHandle.getAppId(
cookie.callingUid))) {
+ // b/350144057
+ Slog.d(TAG, "onPackageRemoved: Skipping - appId not allowed"
+ + " for user=" + user
+ + ", packageName=" + packageName);
continue;
}
try {
+ // b/350144057
+ Slog.d(TAG, "onPackageRemoved: triggering onPackageRemoved"
+ + " for user=" + user
+ + ", packageName=" + packageName);
listener.onPackageRemoved(user, packageName);
} catch (RemoteException re) {
- Slog.d(TAG, "Callback failed ", re);
+ Slog.d(TAG, "onPackageRemoved: Callback failed ", re);
}
}
} finally {
@@ -2524,15 +2492,27 @@
IOnAppsChangedListener listener = mListeners.getBroadcastItem(i);
BroadcastCookie cookie = (BroadcastCookie) mListeners.getBroadcastCookie(i);
if (!isEnabledProfileOf(cookie, user, "onPackageAdded")) {
+ // b/350144057
+ Slog.d(TAG, "onPackageAdded: Skipping - profile not enabled"
+ + " or not accessible for user=" + user
+ + ", packageName=" + packageName);
continue;
}
if (!isPackageVisibleToListener(packageName, cookie, user)) {
+ // b/350144057
+ Slog.d(TAG, "onPackageAdded: Skipping - package filtered"
+ + " for user=" + user
+ + ", packageName=" + packageName);
continue;
}
try {
+ // b/350144057
+ Slog.d(TAG, "onPackageAdded: triggering onPackageAdded"
+ + " for user=" + user
+ + ", packageName=" + packageName);
listener.onPackageAdded(user, packageName);
} catch (RemoteException re) {
- Slog.d(TAG, "Callback failed ", re);
+ Slog.d(TAG, "onPackageAdded: Callback failed ", re);
}
}
} finally {
@@ -2566,7 +2546,7 @@
try {
listener.onPackageChanged(user, packageName);
} catch (RemoteException re) {
- Slog.d(TAG, "Callback failed ", re);
+ Slog.d(TAG, "onPackageChanged: Callback failed ", re);
}
}
} finally {
diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java
index d65e30b..045d4db 100644
--- a/services/core/java/com/android/server/pm/ShortcutLauncher.java
+++ b/services/core/java/com/android/server/pm/ShortcutLauncher.java
@@ -42,7 +42,6 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
-import java.util.stream.Collectors;
/**
* Launcher information used by {@link ShortcutService}.
@@ -129,15 +128,9 @@
*/
public void pinShortcuts(@UserIdInt int packageUserId,
@NonNull String packageName, @NonNull List<String> ids, boolean forPinRequest) {
- if (ShortcutService.DEBUG) {
- Slog.v(TAG, "ShortcutLauncher#pinShortcuts: pin shortcuts from " + packageName
- + " with userId=" + packageUserId + " shortcutIds="
- + ids.stream().collect(Collectors.joining(", ", "[", "]")));
- }
final ShortcutPackage packageShortcuts =
mShortcutUser.getPackageShortcutsIfExists(packageName);
if (packageShortcuts == null) {
- Slog.w(TAG, "ShortcutLauncher#pinShortcuts packageShortcuts is null");
return; // No need to instantiate.
}
@@ -162,10 +155,6 @@
final String id = ids.get(i);
final ShortcutInfo si = packageShortcuts.findShortcutById(id);
if (si == null) {
- if (ShortcutService.DEBUG) {
- Slog.w(TAG, "ShortcutLauncher#pinShortcuts: cannot pin "
- + id + " because it does not exist");
- }
continue;
}
if (si.isDynamic() || si.isLongLived()
@@ -185,13 +174,6 @@
}
}
}
- if (ShortcutService.DEBUG) {
- Slog.v(TAG, "ShortcutLauncher#pinShortcuts: "
- + " newSet: " + newSet.stream().collect(
- Collectors.joining(", ", "[", "]"))
- + " floatingSet: " + floatingSet.stream().collect(
- Collectors.joining(", ", "[", "]")));
- }
mPinnedShortcuts.put(up, newSet);
}
}
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index c9ad498..60056eb 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -729,11 +729,6 @@
}
pinnedShortcuts.addAll(pinned);
});
- if (ShortcutService.DEBUG) {
- Slog.v(TAG, "ShortcutPackage#refreshPinnedFlags: "
- + " pinnedShortcuts: " + pinnedShortcuts.stream().collect(
- Collectors.joining(", ", "[", "]")));
- }
// Secondly, update the pinned state if necessary.
final List<ShortcutInfo> pinned = findAll(pinnedShortcuts);
if (pinned != null) {
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index ea495c9..a3ff195 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -169,7 +169,7 @@
public class ShortcutService extends IShortcutService.Stub {
static final String TAG = "ShortcutService";
- static final boolean DEBUG = Build.IS_DEBUGGABLE; // STOPSHIP if true
+ static final boolean DEBUG = false; // STOPSHIP if true
static final boolean DEBUG_LOAD = false; // STOPSHIP if true
static final boolean DEBUG_PROCSTATE = false; // STOPSHIP if true
static final boolean DEBUG_REBOOT = Build.IS_DEBUGGABLE;
@@ -3206,11 +3206,6 @@
public void pinShortcuts(int launcherUserId,
@NonNull String callingPackage, @NonNull String packageName,
@NonNull List<String> shortcutIds, int userId) {
- if (DEBUG) {
- Slog.v(TAG, "pinShortcuts: " + callingPackage + ", with userId=" + launcherUserId
- + ", is trying to pin shortcuts from " + packageName
- + " with userId=" + userId);
- }
// Calling permission must be checked by LauncherAppsImpl.
Preconditions.checkStringNotEmpty(packageName, "packageName");
Objects.requireNonNull(shortcutIds, "shortcutIds");
@@ -3235,11 +3230,6 @@
&& !si.isDeclaredInManifest(),
ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO,
callingPackage, launcherUserId, false);
- } else {
- if (DEBUG) {
- Slog.w(TAG, "specified package " + packageName + ", with userId=" + userId
- + ", doesn't exist.");
- }
}
// Get list of shortcuts that will get unpinned.
ArraySet<String> oldPinnedIds = launcher.getPinnedShortcutIds(packageName, userId);
@@ -5458,17 +5448,6 @@
*/
private List<ShortcutInfo> prepareChangedShortcuts(ArraySet<String> changedIds,
ArraySet<String> newIds, List<ShortcutInfo> deletedList, final ShortcutPackage ps) {
- if (DEBUG) {
- Slog.v(TAG, "prepareChangedShortcuts: "
- + " changedIds=" + (changedIds == null
- ? "n/a" : changedIds.stream().collect(Collectors.joining(", ", "[", "]")))
- + " newIds=" + (newIds == null
- ? "n/a" : newIds.stream().collect(Collectors.joining(", ", "[", "]")))
- + " deletedList=" + (deletedList == null
- ? "n/a" : deletedList.stream().map(ShortcutInfo::getId).collect(
- Collectors.joining(", ", "[", "]")))
- + " ps=" + (ps == null ? "n/a" : ps.getPackageName()));
- }
if (ps == null) {
// This can happen when package restore is not finished yet.
return null;
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index a683a8c..89417f3 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1090,6 +1090,21 @@
mUser0Allocations = DBG_ALLOCATION ? new AtomicInteger() : null;
mPrivateSpaceAutoLockSettingsObserver = new SettingsObserver(mHandler);
emulateSystemUserModeIfNeeded();
+ initPropertyInvalidatedCaches();
+ }
+
+ /**
+ * This method is used to invalidate the caches at server statup,
+ * so that caches can start working.
+ */
+ private static final void initPropertyInvalidatedCaches() {
+ if (android.multiuser.Flags.cachesNotInvalidatedAtStartReadOnly()) {
+ UserManager.invalidateIsUserUnlockedCache();
+ UserManager.invalidateQuietModeEnabledCache();
+ UserManager.invalidateStaticUserProperties();
+ UserManager.invalidateUserPropertiesCache();
+ UserManager.invalidateUserSerialNumberCache();
+ }
}
private boolean doesDeviceHardwareSupportPrivateSpace() {
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index 953aae9..457196b 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -89,6 +89,7 @@
import com.android.internal.widget.LockSettingsStateListener;
import com.android.server.LocalServices;
import com.android.server.SystemService;
+import com.android.server.pm.UserManagerInternal;
import com.android.server.servicewatcher.CurrentUserServiceSupplier;
import com.android.server.servicewatcher.ServiceWatcher;
import com.android.server.utils.Slogf;
@@ -170,6 +171,7 @@
private final ActivityManager mActivityManager;
private FingerprintManager mFingerprintManager;
private FaceManager mFaceManager;
+ private UserManagerInternal mUserManagerInternal;
private enum TrustState {
// UNTRUSTED means that TrustManagerService is currently *not* giving permission for the
@@ -1064,6 +1066,8 @@
Log.w(TAG, "Unable to check keyguard lock state", e);
}
currentUserIsUnlocked = unlockedUser == id;
+ } else if (isVisibleBackgroundUser(id)) {
+ showingKeyguard = !mUserManager.isUserUnlocked(id);
}
final boolean deviceLocked = secure && showingKeyguard && !trusted
&& !biometricAuthenticated;
@@ -1095,6 +1099,16 @@
}
}
+ private boolean isVisibleBackgroundUser(int userId) {
+ if (!mUserManager.isVisibleBackgroundUsersSupported()) {
+ return false;
+ }
+ if (mUserManagerInternal == null) {
+ mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
+ }
+ return mUserManagerInternal.isVisibleBackgroundFullUser(userId);
+ }
+
private void notifyTrustAgentsOfDeviceLockState(int userId, boolean isLocked) {
for (int i = 0; i < mActiveAgents.size(); i++) {
AgentInfo agent = mActiveAgents.valueAt(i);
diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
index 3be266e..f069dcd 100644
--- a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
@@ -145,11 +145,13 @@
}
}
- void updateSizeCompatScale(@NonNull Rect resolvedAppBounds, @NonNull Rect containerAppBounds) {
+ void updateSizeCompatScale(@NonNull Rect resolvedAppBounds, @NonNull Rect containerAppBounds,
+ @NonNull Configuration newParentConfig) {
mSizeCompatScale = mActivityRecord.mAppCompatController.getTransparentPolicy()
.findOpaqueNotFinishingActivityBelow()
.map(activityRecord -> mSizeCompatScale)
- .orElseGet(() -> calculateSizeCompatScale(resolvedAppBounds, containerAppBounds));
+ .orElseGet(() -> calculateSizeCompatScale(
+ resolvedAppBounds, containerAppBounds, newParentConfig));
}
void clearSizeCompatModeAttributes() {
@@ -290,7 +292,7 @@
// Calculates the scale the size compatibility bounds into the region which is available
// to application.
final float lastSizeCompatScale = mSizeCompatScale;
- updateSizeCompatScale(resolvedAppBounds, containerAppBounds);
+ updateSizeCompatScale(resolvedAppBounds, containerAppBounds, newParentConfiguration);
final int containerTopInset = containerAppBounds.top - containerBounds.top;
final boolean topNotAligned =
@@ -423,7 +425,7 @@
}
private float calculateSizeCompatScale(@NonNull Rect resolvedAppBounds,
- @NonNull Rect containerAppBounds) {
+ @NonNull Rect containerAppBounds, @NonNull Configuration newParentConfig) {
final int contentW = resolvedAppBounds.width();
final int contentH = resolvedAppBounds.height();
final int viewportW = containerAppBounds.width();
@@ -432,7 +434,8 @@
// original container or if it's a freeform window in desktop mode.
boolean shouldAllowUpscaling = !(contentW <= viewportW && contentH <= viewportH)
|| (canEnterDesktopMode(mActivityRecord.mAtmService.mContext)
- && mActivityRecord.getWindowingMode() == WINDOWING_MODE_FREEFORM);
+ && newParentConfig.windowConfiguration.getWindowingMode()
+ == WINDOWING_MODE_FREEFORM);
return shouldAllowUpscaling ? Math.min(
(float) viewportW / contentW, (float) viewportH / contentH) : 1f;
}
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index 1994174..3a2cffb 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -422,6 +422,7 @@
|| first.brightnessMaximum != second.brightnessMaximum
|| first.brightnessDefault != second.brightnessDefault
|| first.installOrientation != second.installOrientation
+ || first.isForceSdr != second.isForceSdr
|| !Objects.equals(first.layoutLimitedRefreshRate, second.layoutLimitedRefreshRate)
|| !BrightnessSynchronizer.floatEquals(first.hdrSdrRatio, second.hdrSdrRatio)
|| !first.thermalRefreshRateThrottling.contentEquals(
diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
index 5514294e..e007b1d 100644
--- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java
+++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
@@ -181,22 +181,30 @@
return true;
}
- boolean transferToHost(@NonNull InputTransferToken embeddedWindowToken,
+ boolean transferToHost(int callingUid, @NonNull InputTransferToken embeddedWindowToken,
@NonNull WindowState transferToHostWindowState) {
EmbeddedWindow ew = getByInputTransferToken(embeddedWindowToken);
if (!isValidTouchGestureParams(transferToHostWindowState, ew)) {
return false;
}
+ if (callingUid != ew.mOwnerUid) {
+ throw new SecurityException(
+ "Transfer request must originate from owner of transferFromToken");
+ }
return mInputManagerService.transferTouchGesture(ew.getInputChannelToken(),
transferToHostWindowState.mInputChannelToken);
}
- boolean transferToEmbedded(WindowState hostWindowState,
+ boolean transferToEmbedded(int callingUid, WindowState hostWindowState,
@NonNull InputTransferToken transferToToken) {
final EmbeddedWindowController.EmbeddedWindow ew = getByInputTransferToken(transferToToken);
if (!isValidTouchGestureParams(hostWindowState, ew)) {
return false;
}
+ if (callingUid != hostWindowState.mOwnerUid) {
+ throw new SecurityException(
+ "Transfer request must originate from owner of transferFromToken");
+ }
return mInputManagerService.transferTouchGesture(hostWindowState.mInputChannelToken,
ew.getInputChannelToken());
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 33f2dd1..b8f47cc 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9212,6 +9212,8 @@
final InputApplicationHandle applicationHandle;
final String name;
Objects.requireNonNull(outInputChannel);
+ Objects.requireNonNull(inputTransferToken);
+
synchronized (mGlobalLock) {
WindowState hostWindowState = hostInputTransferToken != null
? mInputToWindowMap.get(hostInputTransferToken.getToken()) : null;
@@ -9236,6 +9238,7 @@
Objects.requireNonNull(transferFromToken);
Objects.requireNonNull(transferToToken);
+ final int callingUid = Binder.getCallingUid();
final long identity = Binder.clearCallingIdentity();
boolean didTransfer;
try {
@@ -9245,12 +9248,14 @@
// represents an embedded window so transfer from host to embedded.
WindowState windowStateTo = mInputToWindowMap.get(transferToToken.getToken());
if (windowStateTo != null) {
- didTransfer = mEmbeddedWindowController.transferToHost(transferFromToken,
+ didTransfer = mEmbeddedWindowController.transferToHost(callingUid,
+ transferFromToken,
windowStateTo);
} else {
WindowState windowStateFrom = mInputToWindowMap.get(
transferFromToken.getToken());
- didTransfer = mEmbeddedWindowController.transferToEmbedded(windowStateFrom,
+ didTransfer = mEmbeddedWindowController.transferToEmbedded(callingUid,
+ windowStateFrom,
transferToToken);
}
}
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
index 63cf7bf..c05c381 100644
--- a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
@@ -139,16 +139,15 @@
runtimeSearchSession.put(putDocumentsRequest).get()
staticSearchSession.put(putDocumentsRequest).get()
val metadataSyncAdapter =
- MetadataSyncAdapter(
- testExecutor,
- runtimeSearchSession,
+ MetadataSyncAdapter(testExecutor, packageManager, appSearchManager)
+
+ val submitSyncRequest =
+ metadataSyncAdapter.trySyncAppFunctionMetadataBlocking(
staticSearchSession,
- packageManager,
+ runtimeSearchSession,
)
- val submitSyncRequest = metadataSyncAdapter.submitSyncRequest()
-
- assertThat(submitSyncRequest.get()).isTrue()
+ assertThat(submitSyncRequest).isInstanceOf(Unit::class.java)
}
@Test
@@ -182,16 +181,15 @@
PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build()
staticSearchSession.put(putDocumentsRequest).get()
val metadataSyncAdapter =
- MetadataSyncAdapter(
- testExecutor,
- runtimeSearchSession,
+ MetadataSyncAdapter(testExecutor, packageManager, appSearchManager)
+
+ val submitSyncRequest =
+ metadataSyncAdapter.trySyncAppFunctionMetadataBlocking(
staticSearchSession,
- packageManager,
+ runtimeSearchSession,
)
- val submitSyncRequest = metadataSyncAdapter.submitSyncRequest()
-
- assertThat(submitSyncRequest.get()).isTrue()
+ assertThat(submitSyncRequest).isInstanceOf(Unit::class.java)
}
@Test
@@ -239,16 +237,15 @@
PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build()
runtimeSearchSession.put(putDocumentsRequest).get()
val metadataSyncAdapter =
- MetadataSyncAdapter(
- testExecutor,
- runtimeSearchSession,
+ MetadataSyncAdapter(testExecutor, packageManager, appSearchManager)
+
+ val submitSyncRequest =
+ metadataSyncAdapter.trySyncAppFunctionMetadataBlocking(
staticSearchSession,
- packageManager,
+ runtimeSearchSession,
)
- val submitSyncRequest = metadataSyncAdapter.submitSyncRequest()
-
- assertThat(submitSyncRequest.get()).isTrue()
+ assertThat(submitSyncRequest).isInstanceOf(Unit::class.java)
}
@Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 8b80f85..255dcb0 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -27,6 +27,7 @@
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
+import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.server.display.ExternalDisplayPolicy.ENABLE_ON_CONNECT;
@@ -195,8 +196,8 @@
private static final String VIRTUAL_DISPLAY_NAME = "Test Virtual Display";
private static final String PACKAGE_NAME = "com.android.frameworks.displayservicetests";
private static final long STANDARD_DISPLAY_EVENTS = DisplayManager.EVENT_FLAG_DISPLAY_ADDED
- | DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
- | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED;
+ | DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
+ | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED;
private static final long STANDARD_AND_CONNECTION_DISPLAY_EVENTS =
STANDARD_DISPLAY_EVENTS | DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED;
@@ -238,6 +239,8 @@
private UserManager mUserManager;
+ private int[] mAllowedHdrOutputTypes;
+
private final DisplayManagerService.Injector mShortMockedInjector =
new DisplayManagerService.Injector() {
@Override
@@ -256,11 +259,12 @@
displayAdapterListener, flags,
mMockedDisplayNotificationManager,
new LocalDisplayAdapter.Injector() {
- @Override
- public LocalDisplayAdapter.SurfaceControlProxy getSurfaceControlProxy() {
- return mSurfaceControlProxy;
- }
- });
+ @Override
+ public LocalDisplayAdapter.SurfaceControlProxy
+ getSurfaceControlProxy() {
+ return mSurfaceControlProxy;
+ }
+ });
}
@Override
@@ -320,7 +324,7 @@
@Override
int setHdrConversionMode(int conversionMode, int preferredHdrOutputType,
- int[] autoHdrTypes) {
+ int[] allowedHdrOutputTypes) {
mHdrConversionMode = conversionMode;
mPreferredHdrOutputType = preferredHdrOutputType;
return Display.HdrCapabilities.HDR_TYPE_INVALID;
@@ -1295,11 +1299,11 @@
.setUniqueId("uniqueId --- mirror display");
assertThrows(SecurityException.class, () -> {
localService.createVirtualDisplay(
- builder.build(),
- mMockAppToken /* callback */,
- null /* virtualDeviceToken */,
- mock(DisplayWindowPolicyController.class),
- PACKAGE_NAME);
+ builder.build(),
+ mMockAppToken /* callback */,
+ null /* virtualDeviceToken */,
+ mock(DisplayWindowPolicyController.class),
+ PACKAGE_NAME);
});
}
@@ -1433,7 +1437,7 @@
// The virtual display should not have FLAG_ALWAYS_UNLOCKED set.
assertEquals(0, (displayManager.getDisplayDeviceInfoInternal(displayId).flags
- & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED));
+ & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED));
}
/**
@@ -1466,7 +1470,7 @@
// The virtual display should not have FLAG_PRESENTATION set.
assertEquals(0, (displayManager.getDisplayDeviceInfoInternal(displayId).flags
- & DisplayDeviceInfo.FLAG_PRESENTATION));
+ & DisplayDeviceInfo.FLAG_PRESENTATION));
}
@Test
@@ -2358,6 +2362,7 @@
HdrConversionMode.HDR_CONVERSION_FORCE,
Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION);
displayManager.setHdrConversionModeInternal(mode);
+
assertEquals(mode, displayManager.getHdrConversionModeSettingInternal());
assertEquals(mode.getConversionMode(), mHdrConversionMode);
assertEquals(mode.getPreferredHdrOutputType(), mPreferredHdrOutputType);
@@ -2402,6 +2407,86 @@
}
@Test
+ public void testSetAreUserDisabledHdrTypesAllowed_withFalse_whenHdrDisabled_stripsHdrType() {
+ DisplayManagerService displayManager = new DisplayManagerService(
+ mContext, new BasicInjector() {
+ @Override
+ int setHdrConversionMode(int conversionMode, int preferredHdrOutputType,
+ int[] allowedTypes) {
+ mAllowedHdrOutputTypes = allowedTypes;
+ return Display.HdrCapabilities.HDR_TYPE_INVALID;
+ }
+
+ // Overriding this method to capture the allowed HDR type
+ @Override
+ int[] getSupportedHdrOutputTypes() {
+ return new int[]{Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION};
+ }
+ });
+
+ // Setup: no HDR types disabled, userDisabledTypes allowed, system conversion
+ displayManager.setUserDisabledHdrTypesInternal(new int [0]);
+ displayManager.setAreUserDisabledHdrTypesAllowedInternal(true);
+ displayManager.setHdrConversionModeInternal(
+ new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM));
+
+ assertEquals(1, mAllowedHdrOutputTypes.length);
+ assertTrue(Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION == mAllowedHdrOutputTypes[0]);
+
+ // Action: disable Dolby Vision, set userDisabledTypes not allowed
+ displayManager.setUserDisabledHdrTypesInternal(
+ new int [] {Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION});
+ displayManager.setAreUserDisabledHdrTypesAllowedInternal(false);
+
+ assertEquals(0, mAllowedHdrOutputTypes.length);
+ }
+
+ @Test
+ public void testGetEnabledHdrTypesLocked_whenTypesDisabled_stripsDisabledTypes() {
+ DisplayManagerService displayManager = new DisplayManagerService(
+ mContext, new BasicInjector() {
+ @Override
+ int[] getSupportedHdrOutputTypes() {
+ return new int[]{Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION};
+ }
+ });
+
+ displayManager.setUserDisabledHdrTypesInternal(new int [0]);
+ displayManager.setAreUserDisabledHdrTypesAllowedInternal(true);
+ int [] enabledHdrOutputTypes = displayManager.getEnabledHdrOutputTypes();
+ assertEquals(1, enabledHdrOutputTypes.length);
+ assertTrue(Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION == enabledHdrOutputTypes[0]);
+
+ displayManager.setAreUserDisabledHdrTypesAllowedInternal(false);
+ enabledHdrOutputTypes = displayManager.getEnabledHdrOutputTypes();
+ assertEquals(1, enabledHdrOutputTypes.length);
+ assertTrue(Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION == enabledHdrOutputTypes[0]);
+
+ displayManager.setUserDisabledHdrTypesInternal(
+ new int [] {Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION});
+ enabledHdrOutputTypes = displayManager.getEnabledHdrOutputTypes();
+ assertEquals(0, enabledHdrOutputTypes.length);
+ }
+
+ @Test
+ public void testSetHdrConversionModeInternal_isForceSdrIsUpdated() {
+ DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+ LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+ FakeDisplayDevice displayDevice =
+ createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL);
+ LogicalDisplay logicalDisplay =
+ logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+
+ displayManager.setHdrConversionModeInternal(
+ new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, HDR_TYPE_INVALID));
+ assertTrue(logicalDisplay.getDisplayInfoLocked().isForceSdr);
+
+ displayManager.setHdrConversionModeInternal(
+ new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM));
+ assertFalse(logicalDisplay.getDisplayInfoLocked().isForceSdr);
+ }
+
+ @Test
public void testReturnsRefreshRateForDisplayAndSensor_proximitySensorSet() {
DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
DisplayManagerInternal localService = displayManager.new LocalService();
@@ -3505,7 +3590,7 @@
}
private FakeDisplayDevice createFakeDisplayDevice(DisplayManagerService displayManager,
- Display.Mode[] modes) {
+ Display.Mode[] modes) {
FakeDisplayDevice displayDevice = new FakeDisplayDevice();
DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
displayDeviceInfo.supportedModes = modes;
@@ -3761,9 +3846,9 @@
public void setUserPreferredDisplayModeLocked(Display.Mode preferredMode) {
for (Display.Mode mode : mDisplayDeviceInfo.supportedModes) {
if (mode.matchesIfValid(
- preferredMode.getPhysicalWidth(),
- preferredMode.getPhysicalHeight(),
- preferredMode.getRefreshRate())) {
+ preferredMode.getPhysicalWidth(),
+ preferredMode.getPhysicalHeight(),
+ preferredMode.getRefreshRate())) {
mPreferredMode = mode;
break;
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java
index a82658b..3062d51 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java
@@ -16,13 +16,19 @@
package com.android.server.pm;
+import static android.media.AudioAttributes.USAGE_ALARM;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.testng.AssertJUnit.assertEquals;
import android.app.Notification;
import android.app.NotificationManager;
@@ -31,6 +37,9 @@
import android.media.AudioAttributes;
import android.media.AudioFocusInfo;
import android.media.AudioManager;
+import android.media.AudioPlaybackConfiguration;
+import android.media.PlayerProxy;
+import android.media.audiopolicy.AudioPolicy;
import android.os.Build;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -45,6 +54,10 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Stack;
+
@RunWith(JUnit4.class)
public class BackgroundUserSoundNotifierTest {
@@ -63,7 +76,10 @@
MockitoAnnotations.initMocks(this);
mSpiedContext = spy(mRealContext);
mUsersToRemove = new ArraySet<>();
- mUserManager = UserManager.get(mRealContext);
+
+ mUserManager = spy(mSpiedContext.getSystemService(UserManager.class));
+ doReturn(mUserManager)
+ .when(mSpiedContext).getSystemService(UserManager.class);
doReturn(mNotificationManager)
.when(mSpiedContext).getSystemService(NotificationManager.class);
mBackgroundUserSoundNotifier = new BackgroundUserSoundNotifier(mSpiedContext);
@@ -74,12 +90,9 @@
mUsersToRemove.stream().toList().forEach(this::removeUser);
}
@Test
- public void testAlarmOnBackgroundUser_ForegroundUserNotified() throws RemoteException {
- AudioAttributes aa = new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_ALARM).build();
- UserInfo user = createUser("User",
- UserManager.USER_TYPE_FULL_SECONDARY,
- 0);
+ public void testAlarmOnBackgroundUser_foregroundUserNotified() throws RemoteException {
+ AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build();
+ UserInfo user = createUser("User", UserManager.USER_TYPE_FULL_SECONDARY, 0);
final int fgUserId = mSpiedContext.getUserId();
final int bgUserUid = user.id * 100000;
doReturn(UserHandle.of(fgUserId)).when(mSpiedContext).getUser();
@@ -95,10 +108,9 @@
}
@Test
- public void testMediaOnBackgroundUser_ForegroundUserNotNotified() throws RemoteException {
+ public void testMediaOnBackgroundUser_foregroundUserNotNotified() throws RemoteException {
AudioAttributes aa = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA).build();
- UserInfo user = createUser("User", UserManager.USER_TYPE_FULL_SECONDARY, 0);
final int bgUserUid = mSpiedContext.getUserId() * 100000;
AudioFocusInfo afi = new AudioFocusInfo(aa, bgUserUid, "",
/* packageName= */ "com.android.car.audio", AudioManager.AUDIOFOCUS_GAIN,
@@ -109,9 +121,9 @@
}
@Test
- public void testAlarmOnForegroundUser_ForegroundUserNotNotified() throws RemoteException {
+ public void testAlarmOnForegroundUser_foregroundUserNotNotified() throws RemoteException {
AudioAttributes aa = new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_ALARM).build();
+ .setUsage(USAGE_ALARM).build();
final int fgUserId = mSpiedContext.getUserId();
final int fgUserUid = fgUserId * 100000;
doReturn(UserHandle.of(fgUserId)).when(mSpiedContext).getUser();
@@ -123,6 +135,109 @@
verifyZeroInteractions(mNotificationManager);
}
+ @Test
+ public void testMuteAlarmSounds() {
+ final int fgUserId = mSpiedContext.getUserId();
+ int bgUserId = fgUserId + 1;
+ int bgUserUid = bgUserId * 100000;
+ mBackgroundUserSoundNotifier.mNotificationClientUid = bgUserUid;
+
+ AudioManager mockAudioManager = mock(AudioManager.class);
+ when(mSpiedContext.getSystemService(AudioManager.class)).thenReturn(mockAudioManager);
+
+ AudioPlaybackConfiguration apc1 = mock(AudioPlaybackConfiguration.class);
+ when(apc1.getClientUid()).thenReturn(bgUserUid);
+ when(apc1.getPlayerProxy()).thenReturn(mock(PlayerProxy.class));
+
+ AudioPlaybackConfiguration apc2 = mock(AudioPlaybackConfiguration.class);
+ when(apc2.getClientUid()).thenReturn(bgUserUid + 1);
+ when(apc2.getPlayerProxy()).thenReturn(mock(PlayerProxy.class));
+
+ List<AudioPlaybackConfiguration> configs = new ArrayList<>();
+ configs.add(apc1);
+ configs.add(apc2);
+ when(mockAudioManager.getActivePlaybackConfigurations()).thenReturn(configs);
+
+ AudioPolicy mockAudioPolicy = mock(AudioPolicy.class);
+
+ AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build();
+ AudioFocusInfo afi = new AudioFocusInfo(aa, bgUserUid, "", /* packageName= */ "",
+ AudioManager.AUDIOFOCUS_GAIN, AudioManager.AUDIOFOCUS_NONE, /* flags= */ 0,
+ Build.VERSION.SDK_INT);
+ Stack<AudioFocusInfo> focusStack = new Stack<>();
+ focusStack.add(afi);
+ doReturn(focusStack).when(mockAudioPolicy).getFocusStack();
+ mBackgroundUserSoundNotifier.mFocusControlAudioPolicy = mockAudioPolicy;
+
+ mBackgroundUserSoundNotifier.muteAlarmSounds(mSpiedContext);
+
+ verify(apc1.getPlayerProxy()).stop();
+ verify(apc2.getPlayerProxy(), never()).stop();
+ }
+
+ @Test
+ public void testOnAudioFocusGrant_alarmOnBackgroundUser_notifiesForegroundUser() {
+ final int fgUserId = mSpiedContext.getUserId();
+ UserInfo bgUser = createUser("Background User", UserManager.USER_TYPE_FULL_SECONDARY, 0);
+ int bgUserUid = bgUser.id * 100000;
+
+ AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build();
+ AudioFocusInfo afi = new AudioFocusInfo(aa, bgUserUid, "", "",
+ AudioManager.AUDIOFOCUS_GAIN, 0, 0, Build.VERSION.SDK_INT);
+
+ mBackgroundUserSoundNotifier.getAudioPolicyFocusListener()
+ .onAudioFocusGrant(afi, AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+
+ verify(mNotificationManager)
+ .notifyAsUser(eq(BackgroundUserSoundNotifier.class.getSimpleName()),
+ eq(afi.getClientUid()), any(Notification.class),
+ eq(UserHandle.of(fgUserId)));
+ }
+
+
+ @Test
+ public void testCreateNotification_UserSwitcherEnabled_bothActionsAvailable() {
+ String userName = "BgUser";
+
+ doReturn(true).when(mUserManager).isUserSwitcherEnabled();
+ doReturn(UserManager.SWITCHABILITY_STATUS_OK)
+ .when(mUserManager).getUserSwitchability(any());
+
+ Notification notification = mBackgroundUserSoundNotifier.createNotification(userName,
+ mSpiedContext);
+
+ assertEquals("Alarm for BgUser", notification.extras.getString(
+ Notification.EXTRA_TITLE));
+ assertEquals(Notification.CATEGORY_REMINDER, notification.category);
+ assertEquals(Notification.VISIBILITY_PUBLIC, notification.visibility);
+ assertEquals(com.android.internal.R.drawable.ic_audio_alarm,
+ notification.getSmallIcon().getResId());
+
+ assertEquals(2, notification.actions.length);
+ assertEquals(mSpiedContext.getString(
+ com.android.internal.R.string.bg_user_sound_notification_button_mute),
+ notification.actions[0].title);
+ assertEquals(mSpiedContext.getString(
+ com.android.internal.R.string.bg_user_sound_notification_button_switch_user),
+ notification.actions[1].title);
+ }
+
+ @Test
+ public void testCreateNotification_UserSwitcherDisabled_onlyMuteActionAvailable() {
+ String userName = "BgUser";
+
+ doReturn(false).when(mUserManager).isUserSwitcherEnabled();
+ doReturn(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED)
+ .when(mUserManager).getUserSwitchability(any());
+
+ Notification notification = mBackgroundUserSoundNotifier.createNotification(userName,
+ mSpiedContext);
+
+ assertEquals(1, notification.actions.length);
+ assertEquals(mSpiedContext.getString(
+ com.android.internal.R.string.bg_user_sound_notification_button_mute),
+ notification.actions[0].title);
+ }
private UserInfo createUser(String name, String userType, int flags) {
UserInfo user = mUserManager.createUser(name, userType, flags);
diff --git a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java
index 1a398c5..e0c393c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java
@@ -100,6 +100,7 @@
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.SystemServiceManager;
+import com.android.server.pm.UserManagerInternal;
import org.junit.After;
import org.junit.Before;
@@ -145,6 +146,7 @@
private static final String URI_SCHEME_PACKAGE = "package";
private static final int TEST_USER_ID = 50;
+ private static final int TEST_VISIBLE_BACKGROUND_USER_ID = 51;
private static final UserInfo TEST_USER =
new UserInfo(TEST_USER_ID, "user", UserInfo.FLAG_FULL);
private static final int PARENT_USER_ID = 60;
@@ -170,6 +172,7 @@
private @Mock KeyStoreAuthorization mKeyStoreAuthorization;
private @Mock LockPatternUtils mLockPatternUtils;
private @Mock LockSettingsInternal mLockSettingsInternal;
+ private @Mock UserManagerInternal mUserManagerInternal;
private @Mock PackageManager mPackageManager;
private @Mock UserManager mUserManager;
private @Mock IWindowManager mWindowManager;
@@ -224,6 +227,7 @@
when(mUserManager.getAliveUsers()).thenReturn(List.of(TEST_USER));
when(mUserManager.getEnabledProfileIds(TEST_USER_ID)).thenReturn(new int[0]);
when(mUserManager.getUserInfo(TEST_USER_ID)).thenReturn(TEST_USER);
+ when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(false);
when(mWindowManager.isKeyguardLocked()).thenReturn(true);
@@ -593,6 +597,54 @@
verify(mTrustListener, never()).onTrustManagedChanged(anyBoolean(), anyInt());
}
+ @Test
+ public void testDeviceLocked_visibleBackgroundUser_userLocked() throws RemoteException {
+ setupVisibleBackgroundUser(/* visible= */ true, /* unlocked= */ false);
+ mService.waitForIdle();
+ mTrustManager.reportEnabledTrustAgentsChanged(TEST_VISIBLE_BACKGROUND_USER_ID);
+ mService.waitForIdle();
+ assertThat(mService.isDeviceLockedInner(TEST_VISIBLE_BACKGROUND_USER_ID)).isTrue();
+ }
+
+ @Test
+ public void testDeviceLocked_visibleBackgroundUser_userUnlocked() throws RemoteException {
+ setupVisibleBackgroundUser(/* visible= */ true, /* unlocked= */ true);
+ mService.waitForIdle();
+ mTrustManager.reportEnabledTrustAgentsChanged(TEST_VISIBLE_BACKGROUND_USER_ID);
+ mService.waitForIdle();
+ assertThat(mService.isDeviceLockedInner(TEST_VISIBLE_BACKGROUND_USER_ID)).isFalse();
+ }
+
+ @Test
+ public void testDeviceLocked_invisibleBackgroundUser_userUnlocked() throws RemoteException {
+ setupVisibleBackgroundUser(/* visible= */ false, /* unlocked= */ true);
+ mService.waitForIdle();
+ mTrustManager.reportEnabledTrustAgentsChanged(TEST_VISIBLE_BACKGROUND_USER_ID);
+ mService.waitForIdle();
+ assertThat(mService.isDeviceLockedInner(TEST_VISIBLE_BACKGROUND_USER_ID)).isTrue();
+ }
+
+ private void setupVisibleBackgroundUser(boolean visible, boolean unlocked) {
+ UserInfo info = new UserInfo(TEST_VISIBLE_BACKGROUND_USER_ID, "visible bg user",
+ UserInfo.FLAG_FULL);
+
+ when(mActivityManager.isUserRunning(TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(true);
+
+ when(mLockPatternUtils.isSecure(TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(true);
+
+ when(mUserManager.getAliveUsers()).thenReturn(List.of(TEST_USER, info));
+ when(mUserManager.getEnabledProfileIds(TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(
+ new int[0]);
+ when(mUserManager.getUserInfo(TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(info);
+ when(mUserManager.isUserUnlocked(TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(unlocked);
+ when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(true);
+
+ LocalServices.removeServiceForTest(UserManagerInternal.class);
+ LocalServices.addService(UserManagerInternal.class, mUserManagerInternal);
+ when(mUserManagerInternal.isVisibleBackgroundFullUser(
+ TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(visible);
+ }
+
private void setUpRenewableTrust(ITrustAgentService trustAgent) throws RemoteException {
ITrustAgentServiceCallback callback = getCallback(trustAgent);
callback.setManagingTrust(true);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index 8ee7e03..84c4f62 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -32,6 +32,7 @@
import static android.service.notification.Condition.STATE_TRUE;
import static android.service.notification.NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_ON;
import static android.service.notification.ZenModeConfig.XML_VERSION_MODES_API;
+import static android.service.notification.ZenModeConfig.XML_VERSION_MODES_UI;
import static android.service.notification.ZenModeConfig.ZEN_TAG;
import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE;
import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_NONE;
@@ -1169,6 +1170,23 @@
assertThat(suppressedEffectsOf(result)).isEqualTo(suppressedEffectsOf(policy));
}
+ @Test
+ public void readXml_fixesWronglyDisabledManualRule() throws Exception {
+ ZenModeConfig config = getCustomConfig();
+ if (!Flags.modesUi()) {
+ config.manualRule = new ZenModeConfig.ZenRule();
+ config.manualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+ }
+ config.manualRule.enabled = false;
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ writeConfigXml(config, XML_VERSION_MODES_UI, /* forBackup= */ false, baos);
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ ZenModeConfig fromXml = readConfigXml(bais);
+
+ assertThat(fromXml.manualRule.enabled).isTrue();
+ }
+
private static String suppressedEffectsOf(Policy policy) {
return suppressedEffectsToString(policy.suppressedVisualEffects) + "("
+ policy.suppressedVisualEffects + ")";
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index f743401..7bce828 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -1640,7 +1640,7 @@
.build();
setUpApp(display);
prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT);
- mActivity.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FREEFORM);
assertFalse(mActivity.inSizeCompatMode());
// Resize app to make original app bounds larger than parent bounds.
@@ -1667,7 +1667,7 @@
.build();
setUpApp(display);
prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT);
- mActivity.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FREEFORM);
assertFalse(mActivity.inSizeCompatMode());
// Resize app to make original app bounds smaller than parent bounds.
@@ -1692,7 +1692,7 @@
.build();
setUpApp(display);
prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT);
- mActivity.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FREEFORM);
assertFalse(mActivity.inSizeCompatMode());
final Rect originalAppBounds = mActivity.getBounds();
@@ -1705,6 +1705,38 @@
assertEquals(originalAppBounds, mActivity.getBounds());
}
+ /**
+ * Test that when desktop mode is enabled, a freeform unresizeable activity is not up-scaled
+ * when exiting freeform despite its larger parent bounds.
+ */
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ public void testCompatScaling_freeformUnresizeableApp_exitFreeform_notScaled() {
+ doReturn(true).when(() ->
+ DesktopModeHelper.canEnterDesktopMode(any()));
+ final int dw = 600;
+ final int dh = 800;
+ final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh)
+ .setWindowingMode(WINDOWING_MODE_FREEFORM)
+ .build();
+ setUpApp(display);
+ prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT);
+ mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FREEFORM);
+ final Rect originalAppBounds = mActivity.getBounds();
+
+ assertFalse(mActivity.inSizeCompatMode());
+
+ // Resize app to make original app bounds smaller than parent bounds.
+ mTask.getWindowConfiguration().setAppBounds(
+ new Rect(0, 0, dw + 300, dh + 400));
+ // Change windowing mode from freeform to fullscreen
+ mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ mActivity.onConfigurationChanged(mTask.getConfiguration());
+ // App should enter size compat mode but remain its original size.
+ assertTrue(mActivity.inSizeCompatMode());
+ assertEquals(originalAppBounds, mActivity.getBounds());
+ }
+
@Test
public void testGetLetterboxInnerBounds_noScalingApplied() {
// Set up a display in portrait and ignoring orientation request.
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt
index 92b6b93..82e53c8 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt
@@ -54,7 +54,7 @@
}
transitions {
device.sleep()
- wmHelper.StateSyncBuilder().withoutTopVisibleAppWindows().waitForAndVerify()
+ wmHelper.StateSyncBuilder().withKeyguardShowing().waitForAndVerify()
UnlockScreenRule.unlockScreen(device)
wmHelper.StateSyncBuilder().withImeShown().waitForAndVerify()
}
diff --git a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
index aa73c39..c61a250 100644
--- a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
+++ b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
@@ -36,7 +36,6 @@
import com.android.cts.input.inputeventmatchers.withPressure
import com.android.cts.input.inputeventmatchers.withRawCoords
import com.android.cts.input.inputeventmatchers.withSource
-import java.io.InputStream
import junit.framework.Assert.fail
import org.hamcrest.Matchers.allOf
import org.junit.Before
@@ -130,17 +129,18 @@
scenario.virtualDisplay.display.uniqueId!!,
)
- injectUinputEvents()
+ injectUinputEvents().use {
+ if (DEBUG_RECEIVED_EVENTS) {
+ printReceivedEventsToLogcat(scenario.activity)
+ fail("Test cannot pass in debug mode!")
+ }
- if (DEBUG_RECEIVED_EVENTS) {
- printReceivedEventsToLogcat(scenario.activity)
- fail("Test cannot pass in debug mode!")
+ val verifier = EventVerifier(
+ BatchedEventSplitter { scenario.activity.getInputEvent() }
+ )
+ verifyEvents(verifier)
+ scenario.activity.assertNoEvents()
}
-
- val verifier =
- EventVerifier(BatchedEventSplitter { scenario.activity.getInputEvent() })
- verifyEvents(verifier)
- scenario.activity.assertNoEvents()
} finally {
inputManager.removeUniqueIdAssociationByPort(inputPort)
}
@@ -162,14 +162,32 @@
}
}
- private fun injectUinputEvents() {
+ /**
+ * Plays back the evemu recording associated with the current test case by injecting it via
+ * the `uinput` shell command in interactive mode. The recording playback will begin
+ * immediately, and the shell command (and the associated input device) will remain alive
+ * until the returned [AutoCloseable] is closed.
+ */
+ private fun injectUinputEvents(): AutoCloseable {
val fds = instrumentation.uiAutomation!!.executeShellCommandRw("uinput -")
+ // We do not need to use stdout in this test.
+ fds[0].close()
- ParcelFileDescriptor.AutoCloseOutputStream(fds[1]).use { stdIn ->
- val inputStream: InputStream = instrumentation.context.resources.openRawResource(
+ return ParcelFileDescriptor.AutoCloseOutputStream(fds[1]).also { stdin ->
+ instrumentation.context.resources.openRawResource(
testData.uinputRecordingResource,
- )
- stdIn.write(inputStream.readBytes())
+ ).use { inputStream ->
+ stdin.write(inputStream.readBytes())
+
+ // TODO(b/367419268): Remove extra event injection when uinput parsing is fixed.
+ // Inject an extra sync event with an arbitrarily large timestamp, because the
+ // uinput command will not process the last event until either the next event is
+ // parsed, or fd is closed. Injecting this sync allows us complete injection of
+ // the evemu recording and extend the lifetime of the input device by keeping this
+ // fd open.
+ stdin.write("\nE: 9999.99 0 0 0\n".toByteArray())
+ stdin.flush()
+ }
}
}