Merge "Launch language settings activity for user" into main
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
index dc5e341..93904a7 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
@@ -20,6 +20,7 @@
import android.annotation.Nullable;
import android.os.Environment;
import android.os.SystemClock;
+import android.os.UserHandle;
import android.util.AtomicFile;
import android.util.IndentingPrintWriter;
import android.util.Pair;
@@ -113,15 +114,18 @@
}
/**
- * Add user wakeup for the alarm.
+ * Add user wakeup for the alarm if needed.
* @param userId Id of the user that scheduled alarm.
* @param alarmTime time when alarm is expected to trigger.
*/
public void addUserWakeup(int userId, long alarmTime) {
- synchronized (mUserWakeupLock) {
- mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset());
+ // SYSTEM user is always running, so no need to schedule wakeup for it.
+ if (userId != UserHandle.USER_SYSTEM) {
+ synchronized (mUserWakeupLock) {
+ mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset());
+ }
+ updateUserListFile();
}
- updateUserListFile();
}
/**
diff --git a/core/api/current.txt b/core/api/current.txt
index 0f23721..d610f4c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -26569,7 +26569,7 @@
package android.media.projection {
public final class MediaProjection {
- method public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, int, int, int, int, @Nullable android.view.Surface, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler);
+ method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, int, int, int, int, @Nullable android.view.Surface, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler);
method public void registerCallback(@NonNull android.media.projection.MediaProjection.Callback, @Nullable android.os.Handler);
method public void stop();
method public void unregisterCallback(@NonNull android.media.projection.MediaProjection.Callback);
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index c2c7b81..5a39702 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -327,3 +327,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "fix_large_display_private_space_settings"
+ namespace: "profile_experiences"
+ description: "Fix tablet and foldable specific bugs for private space"
+ bug: "342563741"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 9024569..91ac4ff 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -101,6 +101,13 @@
}
flag {
+ name: "enable_cascading_windows"
+ namespace: "lse_desktop_experience"
+ description: "Whether to apply cascading effect for placing multiple windows when first launched"
+ bug: "325240051"
+}
+
+flag {
name: "enable_camera_compat_for_desktop_windowing"
namespace: "lse_desktop_experience"
description: "Whether to apply Camera Compat treatment to fixed-orientation apps in desktop windowing mode"
diff --git a/core/java/com/android/internal/protolog/IProtoLogClient.aidl b/core/java/com/android/internal/protolog/IProtoLogClient.aidl
new file mode 100644
index 0000000..969ed99
--- /dev/null
+++ b/core/java/com/android/internal/protolog/IProtoLogClient.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.protolog;
+
+/**
+ * The ProtoLog client interface.
+ *
+ * These clients will communicate bi-directionally with the ProtoLog service
+ * (@see IProtoLogService.aidl) running in the system process.
+ *
+ * {@hide}
+ */
+interface IProtoLogClient {
+ void toggleLogcat(boolean enabled, in String[] groups);
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/protolog/IProtoLogService.aidl b/core/java/com/android/internal/protolog/IProtoLogService.aidl
new file mode 100644
index 0000000..cc349ea
--- /dev/null
+++ b/core/java/com/android/internal/protolog/IProtoLogService.aidl
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.protolog;
+
+import com.android.internal.protolog.IProtoLogClient;
+
+/**
+ * The ProtoLog Service interface.
+ *
+ * This service runs in system server.
+ *
+ * All ProtoLog clients (in theory one per process) will register themselves to this service.
+ * This service will then serve as the entry point for any shell command based interactions with
+ * protolog and get/forward any required information/actions from/to all the registered ProtoLog
+ * clients.
+ *
+ * Clients will be responsible for directly sending all their trace data to Perfetto without passing
+ * through this service, except viewer config data, the mappings from messages hashes (generated by
+ * the ProtoLog Tool if and when the tool processed the source code). So, this service is
+ * responsible for dumping the viewer config data from all clients. The reason for this is because
+ * we do this on Perfetto's onFlush callback when a tracing instance is stopped. However, if a
+ * client process is frozen then this will not dump; system server, where this service runs cannot
+ * be frozen. Secondly many processes (i.e. apps) will run the same code with the same viewer config
+ * data, so this service allows us to orchestrate which config gets dumped so we don't duplicate
+ * this information in the trace and waste valuable trace space.
+ *
+ * {@hide}
+ */
+interface IProtoLogService {
+ interface IRegisterClientArgs {
+ String[] getGroups();
+ boolean[] getGroupsDefaultLogcatStatus();
+ String getViewerConfigFile();
+ }
+
+ void registerClient(IProtoLogClient client, IRegisterClientArgs args);
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 4d71723..8f7d703 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -66,7 +66,6 @@
import com.android.internal.protolog.common.LogDataType;
import com.android.internal.protolog.common.LogLevel;
-import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -89,7 +88,7 @@
/**
* A service for the ProtoLog logging system.
*/
-public class PerfettoProtoLogImpl implements IProtoLog {
+public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProtoLog {
private static final String LOG_TAG = "ProtoLog";
public static final String NULL_STRING = "null";
private final AtomicInteger mTracingInstances = new AtomicInteger();
@@ -204,13 +203,23 @@
return mTracingInstances.get() > 0;
}
+ @Override
+ public void toggleLogcat(boolean enabled, String[] groups) {
+ final ILogger logger = (message) -> Log.d(LOG_TAG, message);
+ if (enabled) {
+ startLoggingToLogcat(groups, logger);
+ } else {
+ stopLoggingToLogcat(groups, logger);
+ }
+ }
+
/**
* Start text logging
* @param groups Groups to start text logging for
* @param logger A logger to write status updates to
* @return status code
*/
- public int startLoggingToLogcat(String[] groups, ILogger logger) {
+ public int startLoggingToLogcat(String[] groups, @NonNull ILogger logger) {
if (mViewerConfigReader != null) {
mViewerConfigReader.loadViewerConfig(groups, logger);
}
@@ -223,7 +232,7 @@
* @param logger A logger to write status updates to
* @return status code
*/
- public int stopLoggingToLogcat(String[] groups, ILogger logger) {
+ public int stopLoggingToLogcat(String[] groups, @NonNull ILogger logger) {
if (mViewerConfigReader != null) {
mViewerConfigReader.unloadViewerConfig(groups, logger);
}
@@ -257,8 +266,15 @@
/**
* Responds to a shell command.
*/
+ @Deprecated
public int onShellCommand(ShellCommand shell) {
PrintWriter pw = shell.getOutPrintWriter();
+
+ if (android.tracing.Flags.clientSideProtoLogging()) {
+ pw.println("Command deprecated. Please use 'cmd protolog' instead.");
+ return -1;
+ }
+
String cmd = shell.getNextArg();
if (cmd == null) {
return unknownCommand(pw);
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index e6cb3a0..5135e9e 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -239,6 +239,9 @@
"wmshell.protolog.json.gz",
"wmshell.protolog.pb",
],
+ flags_packages: [
+ "com_android_wm_shell_flags",
+ ],
kotlincflags: ["-Xjvm-default=all"],
manifest: "AndroidManifest.xml",
plugins: ["dagger2-compiler"],
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index 52ae93f..bbbc23e 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -34,6 +34,7 @@
<activity
android:name=".bubbles.shortcut.CreateBubbleShortcutActivity"
+ android:featureFlag="com.android.wm.shell.enable_retrievable_bubbles"
android:exported="true"
android:excludeFromRecents="true"
android:theme="@android:style/Theme.NoDisplay"
@@ -47,6 +48,7 @@
<activity
android:name=".bubbles.shortcut.ShowBubblesActivity"
+ android:featureFlag="com.android.wm.shell.enable_retrievable_bubbles"
android:exported="true"
android:excludeFromRecents="true"
android:theme="@android:style/Theme.NoDisplay" >
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 e713af6..5ffcb07 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
@@ -211,6 +211,7 @@
@ShellMainThread ShellExecutor mainExecutor,
@ShellMainThread Handler mainHandler,
@ShellMainThread Choreographer mainChoreographer,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
ShellInit shellInit,
IWindowManager windowManager,
ShellCommandHandler shellCommandHandler,
@@ -229,6 +230,7 @@
mainExecutor,
mainHandler,
mainChoreographer,
+ bgExecutor,
shellInit,
shellCommandHandler,
windowManager,
@@ -246,6 +248,7 @@
context,
mainHandler,
mainExecutor,
+ bgExecutor,
mainChoreographer,
windowManager,
shellInit,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index 1bf1259..da212e7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -66,7 +66,7 @@
idealSize
}
} else {
- maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio)
+ maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio)
}
}
ORIENTATION_PORTRAIT -> {
@@ -85,13 +85,13 @@
} else {
if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
// Apply custom app width and calculate maximum size
- maximumSizeMaintainingAspectRatio(
+ maximizeSizeGivenAspectRatio(
taskInfo,
Size(customPortraitWidthForLandscapeApp, idealSize.height),
appAspectRatio
)
} else {
- maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio)
+ maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio)
}
}
}
@@ -107,7 +107,7 @@
* Calculates the largest size that can fit in a given area while maintaining a specific aspect
* ratio.
*/
-fun maximumSizeMaintainingAspectRatio(
+fun maximizeSizeGivenAspectRatio(
taskInfo: RunningTaskInfo,
targetArea: Size,
aspectRatio: Float
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 31f797a..a91edaa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -671,7 +671,7 @@
} else {
// if non-resizable then calculate max bounds according to aspect ratio
val activityAspectRatio = calculateAspectRatio(taskInfo)
- val newSize = maximumSizeMaintainingAspectRatio(taskInfo,
+ val newSize = maximizeSizeGivenAspectRatio(taskInfo,
Size(stableBounds.width(), stableBounds.height()), activityAspectRatio)
val newBounds = centerInArea(
newSize, stableBounds, stableBounds.left, stableBounds.top)
@@ -1079,7 +1079,6 @@
wct: WindowContainerTransaction,
taskInfo: RunningTaskInfo
) {
- val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
val targetWindowingMode =
@@ -1089,9 +1088,6 @@
} else {
WINDOWING_MODE_FREEFORM
}
- if (Flags.enableWindowingDynamicInitialBounds()) {
- wct.setBounds(taskInfo.token, calculateInitialBounds(displayLayout, taskInfo))
- }
wct.setWindowingMode(taskInfo.token, targetWindowingMode)
wct.reorder(taskInfo.token, true /* onTop */)
if (useDesktopOverrideDensity()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index b9cb6d3..5c230c0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -56,6 +56,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
@@ -72,6 +73,7 @@
private final IWindowManager mWindowManager;
private final Context mContext;
private final Handler mMainHandler;
+ private final @ShellBackgroundThread ShellExecutor mBgExecutor;
private final ShellExecutor mMainExecutor;
private final Choreographer mMainChoreographer;
private final DisplayController mDisplayController;
@@ -108,6 +110,7 @@
public CaptionWindowDecorViewModel(
Context context,
Handler mainHandler,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
ShellExecutor shellExecutor,
Choreographer mainChoreographer,
IWindowManager windowManager,
@@ -120,6 +123,7 @@
mContext = context;
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
+ mBgExecutor = bgExecutor;
mWindowManager = windowManager;
mMainChoreographer = mainChoreographer;
mTaskOrganizer = taskOrganizer;
@@ -289,6 +293,7 @@
taskInfo,
taskSurface,
mMainHandler,
+ mBgExecutor,
mMainChoreographer,
mSyncQueue);
mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 7e1b973..cf42a49 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -21,6 +21,7 @@
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize;
import android.annotation.NonNull;
+import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.WindowConfiguration;
@@ -48,7 +49,9 @@
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
/**
@@ -58,6 +61,7 @@
*/
public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
private final Handler mHandler;
+ private final @ShellBackgroundThread ShellExecutor mBgExecutor;
private final Choreographer mChoreographer;
private final SyncTransactionQueue mSyncQueue;
@@ -78,10 +82,12 @@
RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
Handler handler,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
Choreographer choreographer,
SyncTransactionQueue syncQueue) {
super(context, displayController, taskOrganizer, taskInfo, taskSurface);
mHandler = handler;
+ mBgExecutor = bgExecutor;
mChoreographer = choreographer;
mSyncQueue = syncQueue;
}
@@ -218,6 +224,7 @@
relayoutParams.mOccludingCaptionElements.add(controlsElement);
}
+ @SuppressLint("MissingPermission")
void relayout(RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition) {
@@ -235,7 +242,7 @@
relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
// After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
- mTaskOrganizer.applyTransaction(wct);
+ mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct));
if (mResult.mRootView == null) {
// This means something blocks the window decor from showing, e.g. the task is hidden.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 5397625..3f0e875 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -98,6 +98,7 @@
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
import com.android.wm.shell.desktopmode.DesktopWallpaperActivity;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.splitscreen.SplitScreen;
import com.android.wm.shell.splitscreen.SplitScreen.StageType;
@@ -132,6 +133,7 @@
private final ShellController mShellController;
private final Context mContext;
private final Handler mMainHandler;
+ private final @ShellBackgroundThread ShellExecutor mBgExecutor;
private final Choreographer mMainChoreographer;
private final DisplayController mDisplayController;
private final SyncTransactionQueue mSyncQueue;
@@ -183,6 +185,7 @@
ShellExecutor shellExecutor,
Handler mainHandler,
Choreographer mainChoreographer,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
ShellInit shellInit,
ShellCommandHandler shellCommandHandler,
IWindowManager windowManager,
@@ -201,6 +204,7 @@
shellExecutor,
mainHandler,
mainChoreographer,
+ bgExecutor,
shellInit,
shellCommandHandler,
windowManager,
@@ -225,6 +229,7 @@
ShellExecutor shellExecutor,
Handler mainHandler,
Choreographer mainChoreographer,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
ShellInit shellInit,
ShellCommandHandler shellCommandHandler,
IWindowManager windowManager,
@@ -245,6 +250,7 @@
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
mMainChoreographer = mainChoreographer;
+ mBgExecutor = bgExecutor;
mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
mTaskOrganizer = taskOrganizer;
mShellController = shellController;
@@ -1095,6 +1101,7 @@
taskInfo,
taskSurface,
mMainHandler,
+ mBgExecutor,
mMainChoreographer,
mSyncQueue,
mRootTaskDisplayAreaOrganizer);
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 5d662b2..7988983 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
@@ -29,6 +29,7 @@
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize;
import android.annotation.NonNull;
+import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.WindowConfiguration.WindowingMode;
import android.content.ComponentName;
@@ -68,7 +69,9 @@
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener;
@@ -95,6 +98,7 @@
static final long CLOSE_MAXIMIZE_MENU_DELAY_MS = 150L;
private final Handler mHandler;
+ private final @ShellBackgroundThread ShellExecutor mBgExecutor;
private final Choreographer mChoreographer;
private final SyncTransactionQueue mSyncQueue;
@@ -151,11 +155,12 @@
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
Handler handler,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
Choreographer choreographer,
SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
this (context, displayController, taskOrganizer, taskInfo, taskSurface,
- handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer,
+ handler, bgExecutor, choreographer, syncQueue, rootTaskDisplayAreaOrganizer,
SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
WindowContainerTransaction::new, SurfaceControl::new,
new SurfaceControlViewHostFactory() {}, DefaultMaximizeMenuFactory.INSTANCE);
@@ -168,6 +173,7 @@
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
Handler handler,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
Choreographer choreographer,
SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
@@ -182,6 +188,7 @@
windowContainerTransactionSupplier, surfaceControlSupplier,
surfaceControlViewHostFactory);
mHandler = handler;
+ mBgExecutor = bgExecutor;
mChoreographer = choreographer;
mSyncQueue = syncQueue;
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
@@ -327,6 +334,7 @@
mHandler.post(mCurrentViewHostRunnable);
}
+ @SuppressLint("MissingPermission")
private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
@@ -337,7 +345,7 @@
}
if (isHandleMenuActive()) {
- mHandleMenu.relayout(startT);
+ mHandleMenu.relayout(startT, mResult.mCaptionX);
}
updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw,
@@ -353,7 +361,7 @@
// After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
Trace.beginSection("DesktopModeWindowDecoration#relayout-applyWCT");
- mTaskOrganizer.applyTransaction(wct);
+ mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct));
Trace.endSection();
if (mResult.mRootView == null) {
@@ -863,7 +871,9 @@
splitScreenController,
DesktopModeStatus.canEnterDesktopMode(mContext),
browserLinkAvailable(),
- mResult.mCaptionHeight
+ mResult.mCaptionWidth,
+ mResult.mCaptionHeight,
+ mResult.mCaptionX
);
mWindowDecorViewHolder.onHandleMenuOpened();
mHandleMenu.show();
@@ -1147,6 +1157,7 @@
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
Handler handler,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
Choreographer choreographer,
SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
@@ -1157,6 +1168,7 @@
taskInfo,
taskSurface,
handler,
+ bgExecutor,
choreographer,
syncQueue,
rootTaskDisplayAreaOrganizer);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index b51b700..e174e83 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -69,7 +69,9 @@
private val splitScreenController: SplitScreenController,
private val shouldShowWindowingPill: Boolean,
private val shouldShowBrowserPill: Boolean,
- private val captionHeight: Int
+ private val captionWidth: Int,
+ private val captionHeight: Int,
+ captionX: Int
) {
private val context: Context = parentDecor.mDecorWindowContext
private val taskInfo: ActivityManager.RunningTaskInfo = parentDecor.mTaskInfo
@@ -105,7 +107,7 @@
private val globalMenuPosition: Point = Point()
init {
- updateHandleMenuPillPositions()
+ updateHandleMenuPillPositions(captionX)
}
fun show() {
@@ -262,11 +264,11 @@
/**
* Updates handle menu's position variables to reflect its next position.
*/
- private fun updateHandleMenuPillPositions() {
+ private fun updateHandleMenuPillPositions(captionX: Int) {
val menuX: Int
val menuY: Int
val taskBounds = taskInfo.getConfiguration().windowConfiguration.bounds
- updateGlobalMenuPosition(taskBounds)
+ updateGlobalMenuPosition(taskBounds, captionX)
if (layoutResId == R.layout.desktop_mode_app_header) {
// Align the handle menu to the left side of the caption.
menuX = marginMenuStart
@@ -287,7 +289,8 @@
handleMenuPosition.set(menuX.toFloat(), menuY.toFloat())
}
- private fun updateGlobalMenuPosition(taskBounds: Rect) {
+ private fun updateGlobalMenuPosition(taskBounds: Rect, captionX: Int) {
+ val nonFreeformX = captionX + (captionWidth / 2) - (menuWidth / 2)
when {
taskInfo.isFreeform -> {
globalMenuPosition.set(
@@ -297,7 +300,7 @@
}
taskInfo.isFullscreen -> {
globalMenuPosition.set(
- /* x = */ taskBounds.width() / 2 - (menuWidth / 2),
+ /* x = */ nonFreeformX,
/* y = */ marginMenuTop
)
}
@@ -311,16 +314,13 @@
when (splitPosition) {
SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -> {
globalMenuPosition.set(
- /* x = */ leftOrTopStageBounds.width()
- + (rightOrBottomStageBounds.width() / 2)
- - (menuWidth / 2),
+ /* x = */ leftOrTopStageBounds.width() + nonFreeformX,
/* y = */ marginMenuTop
)
}
SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -> {
globalMenuPosition.set(
- /* x = */ (leftOrTopStageBounds.width() / 2)
- - (menuWidth / 2),
+ /* x = */ nonFreeformX,
/* y = */ marginMenuTop
)
}
@@ -332,9 +332,12 @@
/**
* Update pill layout, in case task changes have caused positioning to change.
*/
- fun relayout(t: SurfaceControl.Transaction) {
+ fun relayout(
+ t: SurfaceControl.Transaction,
+ captionX: Int
+ ) {
handleMenuViewContainer?.let { container ->
- updateHandleMenuPillPositions()
+ updateHandleMenuPillPositions(captionX)
container.setPosition(t, handleMenuPosition.x, handleMenuPosition.y)
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
index 4897f76..03d2421 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
@@ -38,6 +38,7 @@
height: Int
) : AdditionalViewContainer() {
override val view: View
+ val windowManager: WindowManager? = context.getSystemService(WindowManager::class.java)
init {
view = LayoutInflater.from(context).inflate(layoutId, null)
@@ -51,12 +52,11 @@
gravity = Gravity.LEFT or Gravity.TOP
setTrustedOverlay()
}
- val wm: WindowManager? = context.getSystemService(WindowManager::class.java)
- wm?.addView(view, lp)
+ windowManager?.addView(view, lp)
}
override fun releaseView() {
- context.getSystemService(WindowManager::class.java)?.removeViewImmediate(view)
+ windowManager?.removeViewImmediate(view)
}
override fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) {
@@ -64,6 +64,6 @@
this.x = x.toInt()
this.y = y.toInt()
}
- view.layoutParams = lp
+ windowManager?.updateViewLayout(view, lp)
}
}
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 6002c21..8421365 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
@@ -580,138 +580,6 @@
}
@Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
- val task = setUpFullscreenTask()
- setUpLandscapeDisplay()
-
- controller.moveToDesktop(task, transitionSource = UNKNOWN)
- val wct = getLatestEnterDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() {
- val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
- setUpLandscapeDisplay()
-
- controller.moveToDesktop(task, transitionSource = UNKNOWN)
- val wct = getLatestEnterDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() {
- val task =
- setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true)
- setUpLandscapeDisplay()
-
- controller.moveToDesktop(task, transitionSource = UNKNOWN)
- val wct = getLatestEnterDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() {
- val task =
- setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
- setUpLandscapeDisplay()
-
- controller.moveToDesktop(task, transitionSource = UNKNOWN)
- val wct = getLatestEnterDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() {
- val task =
- setUpFullscreenTask(
- isResizable = false,
- screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
- shouldLetterbox = true)
- setUpLandscapeDisplay()
-
- controller.moveToDesktop(task, transitionSource = UNKNOWN)
- val wct = getLatestEnterDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() {
- val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
- setUpPortraitDisplay()
-
- controller.moveToDesktop(task, transitionSource = UNKNOWN)
- val wct = getLatestEnterDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() {
- val task =
- setUpFullscreenTask(
- deviceOrientation = ORIENTATION_PORTRAIT,
- screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
- setUpPortraitDisplay()
-
- controller.moveToDesktop(task, transitionSource = UNKNOWN)
- val wct = getLatestEnterDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() {
- val task =
- setUpFullscreenTask(
- deviceOrientation = ORIENTATION_PORTRAIT,
- screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
- shouldLetterbox = true)
- setUpPortraitDisplay()
-
- controller.moveToDesktop(task, transitionSource = UNKNOWN)
- val wct = getLatestEnterDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() {
- val task =
- setUpFullscreenTask(
- isResizable = false,
- deviceOrientation = ORIENTATION_PORTRAIT,
- screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
- setUpPortraitDisplay()
-
- controller.moveToDesktop(task, transitionSource = UNKNOWN)
- val wct = getLatestEnterDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() {
- val task =
- setUpFullscreenTask(
- isResizable = false,
- deviceOrientation = ORIENTATION_PORTRAIT,
- screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
- shouldLetterbox = true)
- setUpPortraitDisplay()
-
- controller.moveToDesktop(task, transitionSource = UNKNOWN)
- val wct = getLatestEnterDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
- }
-
- @Test
fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
val task = setUpFullscreenTask()
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index b1803e9..ae00c3e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -62,6 +62,7 @@
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.TestShellExecutor
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayInsetsController
import com.android.wm.shell.common.DisplayLayout
@@ -136,6 +137,7 @@
@Mock private lateinit var mockShellCommandHandler: ShellCommandHandler
@Mock private lateinit var mockWindowManager: IWindowManager
@Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
+ private val bgExecutor = TestShellExecutor()
private val transactionFactory = Supplier<SurfaceControl.Transaction> {
SurfaceControl.Transaction()
@@ -155,6 +157,7 @@
mockShellExecutor,
mockMainHandler,
mockMainChoreographer,
+ bgExecutor,
shellInit,
mockShellCommandHandler,
mockWindowManager,
@@ -208,6 +211,7 @@
task,
taskSurface,
mockMainHandler,
+ bgExecutor,
mockMainChoreographer,
mockSyncQueue,
mockRootTaskDisplayAreaOrganizer
@@ -232,6 +236,7 @@
task,
taskSurface,
mockMainHandler,
+ bgExecutor,
mockMainChoreographer,
mockSyncQueue,
mockRootTaskDisplayAreaOrganizer
@@ -247,6 +252,7 @@
task,
taskSurface,
mockMainHandler,
+ bgExecutor,
mockMainChoreographer,
mockSyncQueue,
mockRootTaskDisplayAreaOrganizer
@@ -344,7 +350,7 @@
onTaskChanging(task)
verify(mockDesktopModeWindowDecorFactory, never())
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
}
@Test
@@ -365,7 +371,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory)
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
} finally {
mockitoSession.finishMocking()
}
@@ -382,7 +388,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory, never())
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
}
@Test
@@ -399,7 +405,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory, never())
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
}
@Test
@@ -496,7 +502,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory, never())
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
} finally {
mockitoSession.finishMocking()
}
@@ -520,7 +526,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory)
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
} finally {
mockitoSession.finishMocking()
}
@@ -543,7 +549,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory)
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
} finally {
mockitoSession.finishMocking()
}
@@ -682,7 +688,7 @@
val decoration = mock(DesktopModeWindowDecoration::class.java)
whenever(
mockDesktopModeWindowDecorFactory.create(
- any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
).thenReturn(decoration)
decoration.mTaskInfo = task
whenever(decoration.isFocused).thenReturn(task.isFocused)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index d860609..2c19fdc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -82,7 +82,9 @@
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
+import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams;
@@ -165,6 +167,7 @@
private SurfaceControl.Transaction mMockTransaction;
private StaticMockitoSession mMockitoSession;
private TestableContext mTestableContext;
+ private ShellExecutor mBgExecutor = new TestShellExecutor();
/** Set up run before test class. */
@BeforeClass
@@ -657,7 +660,8 @@
MaximizeMenuFactory maximizeMenuFactory) {
final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext,
mMockDisplayController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl,
- mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer,
+ mMockHandler, mBgExecutor, mMockChoreographer, mMockSyncQueue,
+ mMockRootTaskDisplayAreaOrganizer,
SurfaceControl.Builder::new, mMockTransactionSupplier,
WindowContainerTransaction::new, SurfaceControl::new,
mMockSurfaceControlViewHostFactory, maximizeMenuFactory);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
index adda9a6..e548f8f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
@@ -130,7 +130,7 @@
@EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
fun testFullscreenMenuUsesSystemViewContainer() {
createTaskInfo(WINDOWING_MODE_FULLSCREEN, SPLIT_POSITION_UNDEFINED)
- val handleMenu = createAndShowHandleMenu()
+ val handleMenu = createAndShowHandleMenu(SPLIT_POSITION_UNDEFINED)
assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer)
// Verify menu is created at coordinates that, when added to WindowManager,
// show at the top-center of display.
@@ -142,7 +142,7 @@
@EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
fun testFreeformMenu_usesViewHostViewContainer() {
createTaskInfo(WINDOWING_MODE_FREEFORM, SPLIT_POSITION_UNDEFINED)
- handleMenu = createAndShowHandleMenu()
+ handleMenu = createAndShowHandleMenu(SPLIT_POSITION_UNDEFINED)
assertTrue(handleMenu.handleMenuViewContainer is AdditionalViewHostViewContainer)
// Verify menu is created near top-left of task.
val expected = Point(MENU_START_MARGIN, MENU_TOP_MARGIN)
@@ -153,7 +153,7 @@
@EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
fun testSplitLeftMenu_usesSystemViewContainer() {
createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_TOP_OR_LEFT)
- handleMenu = createAndShowHandleMenu()
+ handleMenu = createAndShowHandleMenu(SPLIT_POSITION_TOP_OR_LEFT)
assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer)
// Verify menu is created at coordinates that, when added to WindowManager,
// show at the top-center of split left task.
@@ -168,7 +168,7 @@
@EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
fun testSplitRightMenu_usesSystemViewContainer() {
createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_BOTTOM_OR_RIGHT)
- handleMenu = createAndShowHandleMenu()
+ handleMenu = createAndShowHandleMenu(SPLIT_POSITION_BOTTOM_OR_RIGHT)
assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer)
// Verify menu is created at coordinates that, when added to WindowManager,
// show at the top-center of split right task.
@@ -208,16 +208,30 @@
}
}
- private fun createAndShowHandleMenu(): HandleMenu {
+ private fun createAndShowHandleMenu(splitPosition: Int): HandleMenu {
val layoutId = if (mockDesktopWindowDecoration.mTaskInfo.isFreeform) {
R.layout.desktop_mode_app_header
} else {
R.layout.desktop_mode_app_handle
}
+ val captionX = when (mockDesktopWindowDecoration.mTaskInfo.windowingMode) {
+ WINDOWING_MODE_FULLSCREEN -> (DISPLAY_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2)
+ WINDOWING_MODE_FREEFORM -> 0
+ WINDOWING_MODE_MULTI_WINDOW -> {
+ if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
+ (SPLIT_LEFT_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2)
+ } else {
+ (SPLIT_RIGHT_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2)
+ }
+ }
+ else -> error("Invalid windowing mode")
+ }
val handleMenu = HandleMenu(mockDesktopWindowDecoration, layoutId,
onClickListener, onTouchListener, appIcon, appName, displayController,
- splitScreenController, true /* shouldShowWindowingPill */,
- true /* shouldShowBrowserPill */, 50 /* captionHeight */)
+ splitScreenController, shouldShowWindowingPill = true,
+ shouldShowBrowserPill = true, captionWidth = HANDLE_WIDTH, captionHeight = 50,
+ captionX = captionX
+ )
handleMenu.show()
return handleMenu
}
@@ -233,5 +247,6 @@
private const val MENU_START_MARGIN = 20
private const val MENU_PILL_ELEVATION = 2
private const val MENU_PILL_SPACING_MARGIN = 4
+ private const val HANDLE_WIDTH = 80
}
}
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index 999f40e5..1c5049e 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -23,6 +23,7 @@
import android.compat.annotation.EnabledSince;
import android.content.Context;
import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.VirtualDisplayFlag;
import android.hardware.display.VirtualDisplay;
import android.hardware.display.VirtualDisplayConfig;
import android.os.Build;
@@ -140,6 +141,7 @@
/**
* @hide
*/
+ @Nullable
public VirtualDisplay createVirtualDisplay(@NonNull String name,
int width, int height, int dpi, boolean isSecure, @Nullable Surface surface,
@Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) {
@@ -192,6 +194,11 @@
* <li>If attempting to create a new virtual display
* associated with this MediaProjection instance after it has
* been stopped by invoking {@link #stop()}.
+ * <li>If attempting to create a new virtual display
+ * associated with this MediaProjection instance after a
+ * {@link MediaProjection.Callback#onStop()} callback has been
+ * received due to the user or the system stopping the
+ * MediaProjection session.
* <li>If the target SDK is {@link
* android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up,
* and if this instance has already taken a recording through
@@ -208,12 +215,17 @@
* {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U}.
* Instead, recording doesn't begin until the user re-grants
* consent in the dialog.
+ * @return The created {@link VirtualDisplay}, or {@code null} if no {@link VirtualDisplay}
+ * could be created.
* @see VirtualDisplay
* @see VirtualDisplay.Callback
*/
+ @SuppressWarnings("RequiresPermission")
+ @Nullable
public VirtualDisplay createVirtualDisplay(@NonNull String name,
- int width, int height, int dpi, int flags, @Nullable Surface surface,
- @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) {
+ int width, int height, int dpi, @VirtualDisplayFlag int flags,
+ @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback,
+ @Nullable Handler handler) {
if (shouldMediaProjectionRequireCallback()) {
if (mCallbacks.isEmpty()) {
final IllegalStateException e = new IllegalStateException(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt
index c34fb38..c993855 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt
@@ -55,7 +55,7 @@
modifier
.pointerInput(isEnabled) {
if (isEnabled) {
- detectLongPressGesture { viewModel.onLongPress() }
+ detectLongPressGesture { viewModel.onLongPress(isA11yAction = false) }
}
}
.pointerInput(Unit) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
index d629eec..f8bd633 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
@@ -35,7 +35,7 @@
import com.android.systemui.res.R
import com.android.systemui.util.animation.MeasurementInput
-private object MediaCarousel {
+object MediaCarousel {
object Elements {
internal val Content =
ElementKey(debugName = "MediaCarouselContent", scenePicker = MediaScenePicker)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt
index 0398133..a22bc34 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt
@@ -25,7 +25,7 @@
/** [ElementScenePicker] implementation for the media carousel object. */
object MediaScenePicker : ElementScenePicker {
- private val shadeLockscreenFraction = 0.65f
+ const val SHADE_FRACTION = 0.66f
private val scenes =
setOf(
Scenes.Lockscreen,
@@ -44,7 +44,7 @@
return when {
// TODO: 352052894 - update with the actual scene picking
transition.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade) -> {
- if (transition.progress < shadeLockscreenFraction) {
+ if (transition.progress < SHADE_FRACTION) {
Scenes.Lockscreen
} else {
Scenes.Shade
@@ -53,7 +53,7 @@
// TODO: 345467290 - update with the actual scene picking
transition.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) -> {
- if (transition.progress < 1f - shadeLockscreenFraction) {
+ if (transition.progress < 1f - SHADE_FRACTION) {
Scenes.Shade
} else {
Scenes.Lockscreen
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
index df47cba..7d46c75 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
@@ -25,6 +25,8 @@
import com.android.compose.animation.scene.TransitionBuilder
import com.android.compose.animation.scene.UserActionDistance
import com.android.compose.animation.scene.UserActionDistanceScope
+import com.android.systemui.media.controls.ui.composable.MediaCarousel
+import com.android.systemui.media.controls.ui.composable.MediaScenePicker
import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.qs.ui.composable.QuickSettings
import com.android.systemui.scene.shared.model.Scenes
@@ -59,10 +61,13 @@
fade(QuickSettings.Elements.SplitShadeQuickSettings)
fade(QuickSettings.Elements.FooterActions)
}
- translate(
- QuickSettings.Elements.QuickQuickSettings,
- y = -ShadeHeader.Dimensions.CollapsedHeight * .66f
- )
+
+ val qsTranslation = ShadeHeader.Dimensions.CollapsedHeight * MediaScenePicker.SHADE_FRACTION
+ val qsExpansionDiff =
+ ShadeHeader.Dimensions.ExpandedHeight - ShadeHeader.Dimensions.CollapsedHeight
+
+ translate(QuickSettings.Elements.QuickQuickSettings, y = -qsTranslation)
+ translate(MediaCarousel.Elements.Content, y = -(qsExpansionDiff + qsTranslation))
translate(Notifications.Elements.NotificationScrim, Edge.Top, false)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
index 5cf4f16..7fd9ce2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
@@ -146,6 +146,14 @@
verify(mShadeViewController, never()).handleExternalTouch(any())
}
+ @Test
+ fun testCancelMotionEvent_popsTouchSession() {
+ swipe(Direction.DOWN)
+ val event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)
+ mInputListenerCaptor.lastValue.onInputEvent(event)
+ verify(mTouchSession).pop()
+ }
+
/**
* Simulates a swipe in the given direction and returns true if the touch was intercepted by the
* touch handler's gesture listener.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt
new file mode 100644
index 0000000..ee51e37
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 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.education.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.education.data.repository.contextualEducationRepository
+import com.android.systemui.education.data.repository.fakeEduClock
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shared.education.GestureType.BACK_GESTURE
+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 KeyboardTouchpadStatsInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val underTest = kosmos.keyboardTouchpadEduStatsInteractor
+
+ @Test
+ fun dataUpdatedOnIncrementSignalCount() =
+ testScope.runTest {
+ val model by
+ collectLastValue(
+ kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK_GESTURE)
+ )
+ val originalValue = model!!.signalCount
+ underTest.incrementSignalCount(BACK_GESTURE)
+ assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+ }
+
+ @Test
+ fun dataAddedOnUpdateShortcutTriggerTime() =
+ testScope.runTest {
+ val model by
+ collectLastValue(
+ kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK_GESTURE)
+ )
+ assertThat(model?.lastShortcutTriggeredTime).isNull()
+ underTest.updateShortcutTriggerTime(BACK_GESTURE)
+ assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant())
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
index 96b4b43..81db16b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
@@ -185,6 +185,21 @@
}
@Test
+ fun longPressed_isA11yAction_doesNotShowMenu_opensSettings() =
+ testScope.runTest {
+ createUnderTest(isOpenWppDirectlyEnabled = true)
+ val isMenuVisible by collectLastValue(underTest.isMenuVisible)
+ val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings)
+ val isA11yAction = true
+ runCurrent()
+
+ underTest.onLongPress(isA11yAction)
+
+ assertThat(isMenuVisible).isFalse()
+ assertThat(shouldOpenSettings).isTrue()
+ }
+
+ @Test
fun longPressed_closeDialogsBroadcastReceived_popupDismissed() =
testScope.runTest {
val isMenuVisible by collectLastValue(underTest.isMenuVisible)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
index 278c90a..fd2e335 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
@@ -159,8 +159,9 @@
)
assertThat(values[0]).isEqualTo(1f)
- // Should fade to zero between here
assertThat(values[1]).isEqualTo(0f)
+ // Should always finish with 1f to show HUNs
+ assertThat(values[2]).isEqualTo(1f)
}
@Test
@@ -177,7 +178,7 @@
testScope,
)
- assertThat(values.size).isEqualTo(2)
+ assertThat(values.size).isEqualTo(3)
// Shade stays open, and alpha should remain visible
values.forEach { assertThat(it).isEqualTo(1f) }
}
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 a120bdc..540a85a 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
@@ -175,12 +175,14 @@
transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone)
assertThat(isVisible).isFalse()
- kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+ kosmos.headsUpNotificationRepository.setNotifications(
buildNotificationRows(isPinned = true)
+ )
assertThat(isVisible).isTrue()
- kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+ kosmos.headsUpNotificationRepository.setNotifications(
buildNotificationRows(isPinned = false)
+ )
assertThat(isVisible).isFalse()
}
@@ -1699,8 +1701,8 @@
return transitionStateFlow
}
- private fun buildNotificationRows(isPinned: Boolean = false): Set<HeadsUpRowRepository> =
- setOf(
+ private fun buildNotificationRows(isPinned: Boolean = false): List<HeadsUpRowRepository> =
+ listOf(
fakeHeadsUpRowRepository(key = "0", isPinned = isPinned),
fakeHeadsUpRowRepository(key = "1", isPinned = isPinned),
fakeHeadsUpRowRepository(key = "2", isPinned = isPinned),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
new file mode 100644
index 0000000..8810ade
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
@@ -0,0 +1,510 @@
+/*
+ * 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(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import android.app.Notification
+import android.app.NotificationManager.IMPORTANCE_DEFAULT
+import android.app.NotificationManager.IMPORTANCE_LOW
+import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.shade.shadeTestUtil
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
+import com.android.systemui.statusbar.notification.collection.modifyEntry
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.google.common.truth.StringSubject
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(NotificationMinimalismPrototype.FLAG_NAME)
+class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
+
+ private val kosmos =
+ testKosmos().apply {
+ testDispatcher = UnconfinedTestDispatcher()
+ statusBarStateController =
+ mock<SysuiStatusBarStateController>().also { mock ->
+ doAnswer { statusBarState.ordinal }.whenever(mock).state
+ }
+ fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
+ }
+ private val notifPipeline: NotifPipeline = mock()
+ private var statusBarState: StatusBarState = StatusBarState.KEYGUARD
+
+ @Test
+ fun topUnseenSectioner() {
+ val solo = NotificationEntryBuilder().setTag("solo").build()
+ val child1 = NotificationEntryBuilder().setTag("child1").build()
+ val child2 = NotificationEntryBuilder().setTag("child2").build()
+ val parent = NotificationEntryBuilder().setTag("parent").build()
+ val group = GroupEntryBuilder().addChild(child1).addChild(child2).setSummary(parent).build()
+
+ runCoordinatorTest {
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = solo.key
+ assertThat(topUnseenSectioner.isInSection(solo)).isTrue()
+ assertThat(topUnseenSectioner.isInSection(child1)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(parent)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(group)).isFalse()
+
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child1.key
+ assertThat(topUnseenSectioner.isInSection(solo)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(child1)).isTrue()
+ assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(parent)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(group)).isTrue()
+
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = parent.key
+ assertThat(topUnseenSectioner.isInSection(solo)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(child1)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(parent)).isTrue()
+ assertThat(topUnseenSectioner.isInSection(group)).isTrue()
+
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = solo.key
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null
+ assertThat(topUnseenSectioner.isInSection(solo)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(child1)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(parent)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(group)).isFalse()
+ }
+ }
+
+ @Test
+ fun topOngoingSectioner() {
+ val solo = NotificationEntryBuilder().setTag("solo").build()
+ val child1 = NotificationEntryBuilder().setTag("child1").build()
+ val child2 = NotificationEntryBuilder().setTag("child2").build()
+ val parent = NotificationEntryBuilder().setTag("parent").build()
+ val group = GroupEntryBuilder().addChild(child1).addChild(child2).setSummary(parent).build()
+
+ runCoordinatorTest {
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = solo.key
+ assertThat(topOngoingSectioner.isInSection(solo)).isTrue()
+ assertThat(topOngoingSectioner.isInSection(child1)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(parent)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(group)).isFalse()
+
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key
+ assertThat(topOngoingSectioner.isInSection(solo)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(child1)).isTrue()
+ assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(parent)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(group)).isTrue()
+
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = parent.key
+ assertThat(topOngoingSectioner.isInSection(solo)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(child1)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(parent)).isTrue()
+ assertThat(topOngoingSectioner.isInSection(group)).isTrue()
+
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = solo.key
+ assertThat(topOngoingSectioner.isInSection(solo)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(child1)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(parent)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(group)).isFalse()
+ }
+ }
+
+ @Test
+ fun testPromoter() {
+ val child1 = NotificationEntryBuilder().setTag("child1").build()
+ val child2 = NotificationEntryBuilder().setTag("child2").build()
+ val child3 = NotificationEntryBuilder().setTag("child3").build()
+ val parent = NotificationEntryBuilder().setTag("parent").build()
+ GroupEntryBuilder()
+ .addChild(child1)
+ .addChild(child2)
+ .addChild(child3)
+ .setSummary(parent)
+ .build()
+
+ runCoordinatorTest {
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null
+ assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(child2)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null
+ assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue()
+ assertThat(promoter.shouldPromoteToTopLevel(child2)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
+ assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(child2))
+ .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+ assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
+ assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue()
+ assertThat(promoter.shouldPromoteToTopLevel(child2))
+ .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+ assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+ }
+ }
+
+ @Test
+ fun topOngoingIdentifier() {
+ val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build()
+ val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build()
+ val parent = defaultEntryBuilder().setTag("parent").setRank(3).build()
+ val child1 = defaultEntryBuilder().setTag("child1").setRank(4).build()
+ val child2 = defaultEntryBuilder().setTag("child2").setRank(5).build()
+ val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build()
+ val listEntryList = listOf(group, solo1, solo2)
+
+ runCoordinatorTest {
+ // TEST: base case - no entries in the list
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(emptyList())
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: none of these are unseen or ongoing yet, so don't pick them
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: when solo2 is the only one colorized, it gets picked up
+ solo2.setColorizedFgs(true)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(solo2.key)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: once solo1 is colorized, it takes priority for being ranked higher
+ solo1.setColorizedFgs(true)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(solo1.key)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: changing just the rank of solo1 causes it to pick up solo2 instead
+ solo1.modifyEntry { setRank(20) }
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(solo2.key)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: switching to SHADE disables the whole thing
+ statusBarState = StatusBarState.SHADE
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: switching back to KEYGUARD picks up the same entry again
+ statusBarState = StatusBarState.KEYGUARD
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(solo2.key)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: updating to not colorized revokes the top-ongoing status
+ solo2.setColorizedFgs(false)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(solo1.key)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: updating the importance to LOW revokes top-ongoing status
+ solo1.modifyEntry { setImportance(IMPORTANCE_LOW) }
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+ }
+ }
+
+ @Test
+ fun topUnseenIdentifier() {
+ val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build()
+ val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build()
+ val parent = defaultEntryBuilder().setTag("parent").setRank(4).build()
+ val child1 = defaultEntryBuilder().setTag("child1").setRank(5).build()
+ val child2 = defaultEntryBuilder().setTag("child2").setRank(6).build()
+ val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build()
+ val listEntryList = listOf(group, solo1, solo2)
+ val notificationEntryList = listOf(solo1, solo2, parent, child1, child2)
+
+ runCoordinatorTest {
+ // All entries are added (and now unseen)
+ notificationEntryList.forEach { collectionListener.onEntryAdded(it) }
+
+ // TEST: Filtered out entries are ignored
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(emptyList())
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: top-ranked unseen child is selected (not the summary)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(group))
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(child1.key)
+
+ // TEST: top-ranked entry is picked
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+ // TEST: if top-ranked unseen is colorized, fall back to #2 ranked unseen
+ solo1.setColorizedFgs(true)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(solo1.key)
+ assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+ // TEST: no more colorized entries
+ solo1.setColorizedFgs(false)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+ // TEST: if the rank of solo1 is reduced, solo2 will be preferred
+ solo1.modifyEntry { setRank(3) }
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+ // TEST: switching to SHADE state will disable the entire selector
+ statusBarState = StatusBarState.SHADE
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: switching back to KEYGUARD re-enables the selector
+ statusBarState = StatusBarState.KEYGUARD
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+ // TEST: QS Expansion does not mark entries as seen
+ setShadeAndQsExpansionThenWait(0f, 1f)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+ // TEST: Shade expansion does mark entries as seen
+ setShadeAndQsExpansionThenWait(1f, 0f)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: Entries updated while shade is expanded are NOT marked unseen
+ collectionListener.onEntryUpdated(solo1)
+ collectionListener.onEntryUpdated(solo2)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: Entries updated after shade is collapsed ARE marked unseen
+ setShadeAndQsExpansionThenWait(0f, 0f)
+ collectionListener.onEntryUpdated(solo1)
+ collectionListener.onEntryUpdated(solo2)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+ // TEST: low importance disqualifies the entry for top unseen
+ solo2.modifyEntry { setImportance(IMPORTANCE_LOW) }
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(solo1.key)
+ }
+ }
+
+ @Test
+ fun topUnseenIdentifier_headsUpMarksSeen() {
+ val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build()
+ val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build()
+ val listEntryList = listOf(solo1, solo2)
+ val notificationEntryList = listOf(solo1, solo2)
+
+ val hunRepo1 = solo1.fakeHeadsUpRowRepository()
+ val hunRepo2 = solo2.fakeHeadsUpRowRepository()
+
+ runCoordinatorTest {
+ // All entries are added (and now unseen)
+ notificationEntryList.forEach { collectionListener.onEntryAdded(it) }
+
+ // TEST: top-ranked entry is picked
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+ // TEST: heads up state and waiting isn't enough to be seen
+ kosmos.headsUpNotificationRepository.orderedHeadsUpRows.value =
+ listOf(hunRepo1, hunRepo2)
+ testScheduler.advanceTimeBy(1.seconds)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+ // TEST: even being pinned doesn't take effect immediately
+ hunRepo1.isPinned.value = true
+ testScheduler.advanceTimeBy(0.5.seconds)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+ // TEST: after being pinned a full second, solo1 is seen
+ testScheduler.advanceTimeBy(0.5.seconds)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+ // TEST: repeat; being heads up and pinned for 1 second triggers seen
+ kosmos.headsUpNotificationRepository.orderedHeadsUpRows.value = listOf(hunRepo2)
+ hunRepo1.isPinned.value = false
+ hunRepo2.isPinned.value = true
+ testScheduler.advanceTimeBy(1.seconds)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopUnseenKey().isEqualTo(null)
+ }
+ }
+
+ private fun NotificationEntry.fakeHeadsUpRowRepository() =
+ FakeHeadsUpRowRepository(key = key, elementKey = Any())
+
+ private fun KeyguardCoordinatorTestScope.setShadeAndQsExpansionThenWait(
+ shadeExpansion: Float,
+ qsExpansion: Float
+ ) {
+ kosmos.shadeTestUtil.setShadeAndQsExpansion(shadeExpansion, qsExpansion)
+ // The coordinator waits a fraction of a second for the shade expansion to stick.
+ testScheduler.advanceTimeBy(1.seconds)
+ }
+
+ private fun defaultEntryBuilder() = NotificationEntryBuilder().setImportance(IMPORTANCE_DEFAULT)
+
+ private fun runCoordinatorTest(testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit) {
+ kosmos.lockScreenMinimalismCoordinator.attach(notifPipeline)
+ kosmos.testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) {
+ KeyguardCoordinatorTestScope(
+ kosmos.lockScreenMinimalismCoordinator,
+ kosmos.testScope,
+ kosmos.fakeSettings,
+ )
+ .testBlock()
+ }
+ }
+
+ private inner class KeyguardCoordinatorTestScope(
+ private val coordinator: LockScreenMinimalismCoordinator,
+ private val scope: TestScope,
+ private val fakeSettings: FakeSettings,
+ ) : CoroutineScope by scope {
+ fun assertThatTopOngoingKey(): StringSubject {
+ return assertThat(
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value
+ )
+ }
+
+ fun assertThatTopUnseenKey(): StringSubject {
+ return assertThat(
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value
+ )
+ }
+
+ val testScheduler: TestCoroutineScheduler
+ get() = scope.testScheduler
+
+ val promoter: NotifPromoter
+ get() = coordinator.unseenNotifPromoter
+
+ val topUnseenSectioner: NotifSectioner
+ get() = coordinator.topUnseenSectioner
+
+ val topOngoingSectioner: NotifSectioner
+ get() = coordinator.topOngoingSectioner
+
+ val onBeforeTransformGroupsListener: OnBeforeTransformGroupsListener =
+ argumentCaptor { verify(notifPipeline).addOnBeforeTransformGroupsListener(capture()) }
+ .lastValue
+
+ val collectionListener: NotifCollectionListener =
+ argumentCaptor { verify(notifPipeline).addCollectionListener(capture()) }.lastValue
+
+ var showOnlyUnseenNotifsOnKeyguardSetting: Boolean
+ get() =
+ fakeSettings.getIntForUser(
+ Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+ UserHandle.USER_CURRENT,
+ ) == 1
+ set(value) {
+ fakeSettings.putIntForUser(
+ Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+ if (value) 1 else 2,
+ UserHandle.USER_CURRENT,
+ )
+ }
+ }
+
+ companion object {
+
+ private fun NotificationEntry.setColorizedFgs(colorized: Boolean) {
+ sbn.notification.setColorizedFgs(colorized)
+ }
+
+ private fun Notification.setColorizedFgs(colorized: Boolean) {
+ extras.putBoolean(Notification.EXTRA_COLORIZED, colorized)
+ flags =
+ if (colorized) {
+ flags or Notification.FLAG_FOREGROUND_SERVICE
+ } else {
+ flags and Notification.FLAG_FOREGROUND_SERVICE.inv()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
index 8b4265f..14134cc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
@@ -33,7 +33,6 @@
import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository
import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index f8e6337..f96cf10 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -43,7 +43,6 @@
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
import com.android.systemui.statusbar.policy.fakeConfigurationController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 71cd95f..6f09931 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -1096,7 +1096,8 @@
)
)
runCurrent()
- assertThat(alpha).isEqualTo(0f)
+ // Resets to 1f after communal scene is hidden
+ assertThat(alpha).isEqualTo(1f)
}
@Test
@@ -1151,7 +1152,7 @@
)
)
runCurrent()
- assertThat(alpha).isEqualTo(0f)
+ assertThat(alpha).isEqualTo(1f)
}
@Test
@@ -1208,7 +1209,8 @@
)
)
runCurrent()
- assertThat(alpha).isEqualTo(0f)
+ // Resets to 1f after communal scene is hidden
+ assertThat(alpha).isEqualTo(1f)
}
@Test
@@ -1263,7 +1265,7 @@
)
)
runCurrent()
- assertThat(alpha).isEqualTo(0f)
+ assertThat(alpha).isEqualTo(1f)
}
private suspend fun TestScope.showLockscreen() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
index b643968..c3c5a48 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
@@ -50,6 +50,7 @@
statusBarStateController = statusBarStateController,
mainExecutor = mainExecutor,
legacyActivityStarter = { legacyActivityStarterInternal },
+ activityStarterInternal = { activityStarterInternal },
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
index 10a2f64..d82b9db 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
@@ -231,7 +231,6 @@
// extra activity options to set on pending intent
val activityOptions = mock(ActivityOptions::class.java)
activityOptions.splashScreenStyle = SPLASH_SCREEN_STYLE_SOLID_COLOR
- activityOptions.isPendingIntentBackgroundActivityLaunchAllowedByPermission = false
val bundleCaptor = argumentCaptor<Bundle>()
startPendingIntentMaybeDismissingKeyguard(
@@ -255,7 +254,8 @@
bundleCaptor.capture()
)
val options = ActivityOptions.fromBundle(bundleCaptor.firstValue)
- assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission).isFalse()
+ assertThat(options.getPendingIntentBackgroundActivityStartMode())
+ .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS)
assertThat(options.splashScreenStyle).isEqualTo(SPLASH_SCREEN_STYLE_SOLID_COLOR)
}
diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml
index 763930d..07a40c8 100644
--- a/packages/SystemUI/res/values/flags.xml
+++ b/packages/SystemUI/res/values/flags.xml
@@ -33,9 +33,4 @@
<!-- Whether the user switcher chip shows in the status bar. When true, the multi user
avatar will no longer show on the lockscreen -->
<bool name="flag_user_switcher_chip">false</bool>
-
- <!-- Whether the battery icon is allowed to display a shield when battery life is being
- protected. -->
- <bool name="flag_battery_shield_icon">false</bool>
-
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 8322b6c..68c83c7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3677,4 +3677,31 @@
<string name="home_controls_dream_description">Quickly access your home controls as a screensaver</string>
<!-- Label for volume undo action [CHAR LIMIT=NONE] -->
<string name="volume_undo_action">Undo</string>
+
+ <!-- Keyboard touchpad contextual education strings-->
+ <!-- Education toast text for Back [CHAR_LIMIT=100] -->
+ <string name="back_edu_toast_content">To go back, swipe left or right with three fingers on the touchpad</string>
+ <!-- Education toast text for Home [CHAR_LIMIT=100] -->
+ <string name="home_edu_toast_content">To go home, swipe up with three fingers on the touchpad</string>
+ <!-- Education toast text for Overview [CHAR_LIMIT=100] -->
+ <string name="overview_edu_toast_content">To view recent apps, swipe up and hold with three fingers on the touchpad</string>
+ <!-- Education toast text for All Apps [CHAR_LIMIT=100] -->
+ <string name="all_apps_edu_toast_content">To view all your apps, press the action key on your keyboard</string>
+
+ <!-- Education notification title for Back [CHAR_LIMIT=100] -->
+ <string name="back_edu_notification_title">Use your touchpad to go back</string>
+ <!-- Education notification text for Back [CHAR_LIMIT=100] -->
+ <string name="back_edu_notification_content">Swipe left or right using three fingers. Tap to learn more gestures.</string>
+ <!-- Education notification title for Home [CHAR_LIMIT=100] -->
+ <string name="home_edu_notification_title">Use your touchpad to go home</string>
+ <!-- Education notification text for Home [CHAR_LIMIT=100] -->
+ <string name="home_edu_notification_content">Swipe up using three fingers. Tap to learn more gestures.</string>
+ <!-- Education notification title for Overview [CHAR_LIMIT=100] -->
+ <string name="overview_edu_notification_title">Use your touchpad to view recent apps</string>
+ <!-- Education notification text for Overview [CHAR_LIMIT=100] -->
+ <string name="overview_edu_notification_content">Swipe up and hold using three fingers. Tap to learn more gestures.</string>
+ <!-- Education notification title for All Apps [CHAR_LIMIT=100] -->
+ <string name="all_apps_edu_notification_title">Use your keyboard to view all apps</string>
+ <!-- Education notification text for All Apps [CHAR_LIMIT=100] -->
+ <string name="all_apps_edu_notification_content">Press the action key at any time. Tap to learn more gestures.</string>
</resources>
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java
index f1fb45c..baca959 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java
@@ -79,7 +79,8 @@
if (mCapture != null && mCapture) {
sendTouchEvent((MotionEvent) ev);
}
- if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
+ if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP
+ || ((MotionEvent) ev).getAction() == MotionEvent.ACTION_CANCEL) {
session.pop();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
index 4035e95..efa55e9 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
@@ -128,8 +128,13 @@
completer.set(predecessor);
}
- if (mActiveTouchSessions.isEmpty() && mStopMonitoringPending) {
- stopMonitoring(false);
+ if (mActiveTouchSessions.isEmpty()) {
+ if (mStopMonitoringPending) {
+ stopMonitoring(false);
+ } else {
+ // restart monitoring to reset any destructive state on the input session
+ startMonitoring();
+ }
}
});
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index 5c53234..e634726 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -88,7 +88,6 @@
private boolean mPowerSaveEnabled;
private boolean mIsBatteryDefender;
private boolean mIsIncompatibleCharging;
- private boolean mDisplayShieldEnabled;
// Error state where we know nothing about the current battery state
private boolean mBatteryStateUnknown;
// Lazily-loaded since this is expected to be a rare-if-ever state
@@ -270,7 +269,7 @@
int resId = 0;
if (mPowerSaveEnabled) {
resId = R.drawable.battery_unified_attr_powersave;
- } else if (mIsBatteryDefender && mDisplayShieldEnabled) {
+ } else if (mIsBatteryDefender) {
resId = R.drawable.battery_unified_attr_defend;
} else if (isCharging) {
resId = R.drawable.battery_unified_attr_charging;
@@ -288,7 +287,7 @@
private ColorProfile getCurrentColorProfile() {
return getColorProfile(
mPowerSaveEnabled,
- mIsBatteryDefender && mDisplayShieldEnabled,
+ mIsBatteryDefender,
mPluggedIn,
mLevel <= 20);
}
@@ -410,10 +409,6 @@
mBatteryEstimateFetcher = fetcher;
}
- void setDisplayShieldEnabled(boolean displayShieldEnabled) {
- mDisplayShieldEnabled = displayShieldEnabled;
- }
-
void updatePercentText() {
if (!newStatusBarIcons()) {
updatePercentTextLegacy();
@@ -659,7 +654,7 @@
float mainBatteryWidth =
res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width) * iconScaleFactor;
- boolean displayShield = mDisplayShieldEnabled && mIsBatteryDefender;
+ boolean displayShield = mIsBatteryDefender;
float fullBatteryIconHeight =
BatterySpecs.getFullBatteryHeight(mainBatteryHeight, displayShield);
float fullBatteryIconWidth =
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
index 4f13e6f..9a30c21 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
@@ -34,7 +34,6 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
-import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.StatusBarLocation;
import com.android.systemui.statusbar.phone.ui.StatusBarIconController;
@@ -153,8 +152,6 @@
mBatteryController = batteryController;
mView.setBatteryEstimateFetcher(mBatteryController::getEstimatedTimeRemainingString);
- mView.setDisplayShieldEnabled(
- getContext().getResources().getBoolean(R.bool.flag_battery_shield_icon));
mSlotBattery = getResources().getString(com.android.internal.R.string.status_bar_battery);
mSettingObserver = new SettingObserver(mMainHandler);
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
index 85e2bdb..b6ace81 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
@@ -19,10 +19,14 @@
import android.annotation.SuppressLint
import android.content.Context
+import android.os.Bundle
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
+import android.view.accessibility.AccessibilityNodeInfo
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import com.android.systemui.shade.TouchLogger
import kotlin.math.pow
import kotlin.math.sqrt
@@ -44,6 +48,10 @@
attrs,
) {
+ init {
+ setupAccessibilityDelegate()
+ }
+
constructor(
context: Context,
attrs: AttributeSet?,
@@ -55,6 +63,7 @@
view: View,
x: Int,
y: Int,
+ isA11yAction: Boolean = false,
)
/** Notifies that the gesture was too short for a long press, it is actually a click. */
@@ -63,6 +72,8 @@
var listener: Listener? = null
+ var accessibilityHintLongPressAction: AccessibilityAction? = null
+
private val interactionHandler: LongPressHandlingViewInteractionHandler by lazy {
LongPressHandlingViewInteractionHandler(
postDelayed = { block, timeoutMs ->
@@ -107,6 +118,51 @@
override fun onTouchEvent(event: MotionEvent?): Boolean {
return interactionHandler.onTouchEvent(event?.toModel())
}
+
+ private fun setupAccessibilityDelegate() {
+ accessibilityDelegate =
+ object : AccessibilityDelegate() {
+ override fun onInitializeAccessibilityNodeInfo(
+ v: View,
+ info: AccessibilityNodeInfo
+ ) {
+ super.onInitializeAccessibilityNodeInfo(v, info)
+ if (
+ interactionHandler.isLongPressHandlingEnabled &&
+ accessibilityHintLongPressAction != null
+ ) {
+ info.addAction(accessibilityHintLongPressAction)
+ }
+ }
+
+ override fun performAccessibilityAction(
+ host: View,
+ action: Int,
+ args: Bundle?
+ ): Boolean {
+ return if (
+ interactionHandler.isLongPressHandlingEnabled &&
+ action == AccessibilityNodeInfoCompat.ACTION_LONG_CLICK
+ ) {
+ val longPressHandlingView = host as? LongPressHandlingView
+ if (longPressHandlingView != null) {
+ // the coordinates are not available as it is an a11y long press
+ listener?.onLongPressDetected(
+ view = longPressHandlingView,
+ x = 0,
+ y = 0,
+ isA11yAction = true,
+ )
+ true
+ } else {
+ false
+ }
+ } else {
+ super.performAccessibilityAction(host, action, args)
+ }
+ }
+ }
+ }
}
private fun MotionEvent.toModel(): LongPressHandlingViewInteractionHandler.MotionEventModel {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
index 7f8103e..6864f4e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
@@ -18,7 +18,7 @@
import android.app.Activity
import android.app.ActivityOptions
-import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
import android.app.Dialog
import android.app.PendingIntent
import android.content.ComponentName
@@ -93,8 +93,8 @@
0 /* enterResId */,
0 /* exitResId */
).apply {
- pendingIntentBackgroundActivityStartMode = MODE_BACKGROUND_ACTIVITY_START_ALLOWED
- isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
+ pendingIntentBackgroundActivityStartMode =
+ MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
taskAlwaysOnTop = true
}
diff --git a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
index 53b9261..0e2e2e6 100644
--- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
@@ -16,12 +16,21 @@
package com.android.systemui.education.dagger
+import com.android.systemui.CoreStartable
+import com.android.systemui.Flags
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.education.data.repository.ContextualEducationRepository
import com.android.systemui.education.data.repository.ContextualEducationRepositoryImpl
+import com.android.systemui.education.domain.interactor.ContextualEducationInteractor
+import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor
+import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractorImpl
+import com.android.systemui.shared.education.GestureType
import dagger.Binds
+import dagger.Lazy
import dagger.Module
import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
import java.time.Clock
import javax.inject.Qualifier
import kotlinx.coroutines.CoroutineDispatcher
@@ -53,5 +62,41 @@
fun provideEduClock(): Clock {
return Clock.systemUTC()
}
+
+ @Provides
+ @IntoMap
+ @ClassKey(ContextualEducationInteractor::class)
+ fun provideContextualEducationInteractor(
+ implLazy: Lazy<ContextualEducationInteractor>
+ ): CoreStartable {
+ return if (Flags.keyboardTouchpadContextualEducation()) {
+ implLazy.get()
+ } else {
+ // No-op implementation when the flag is disabled.
+ return NoOpCoreStartable
+ }
+ }
+
+ @Provides
+ fun provideKeyboardTouchpadEduStatsInteractor(
+ implLazy: Lazy<KeyboardTouchpadEduStatsInteractorImpl>
+ ): KeyboardTouchpadEduStatsInteractor {
+ return if (Flags.keyboardTouchpadContextualEducation()) {
+ implLazy.get()
+ } else {
+ // No-op implementation when the flag is disabled.
+ return NoOpKeyboardTouchpadEduStatsInteractor
+ }
+ }
+ }
+
+ private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor {
+ override fun incrementSignalCount(gestureType: GestureType) {}
+
+ override fun updateShortcutTriggerTime(gestureType: GestureType) {}
+ }
+
+ private object NoOpCoreStartable : CoreStartable {
+ override fun start() {}
}
}
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
new file mode 100644
index 0000000..e2aa911
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 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.education.domain.interactor
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.education.data.repository.ContextualEducationRepository
+import com.android.systemui.shared.education.GestureType
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+/**
+ * Allows updating education data (e.g. signal count, shortcut time) for different gesture types.
+ * Change user education repository when user is changed.
+ */
+@SysUISingleton
+class ContextualEducationInteractor
+@Inject
+constructor(
+ @Background private val backgroundScope: CoroutineScope,
+ private val selectedUserInteractor: SelectedUserInteractor,
+ private val repository: ContextualEducationRepository,
+) : CoreStartable {
+
+ override fun start() {
+ backgroundScope.launch {
+ selectedUserInteractor.selectedUser.collectLatest { repository.setUser(it) }
+ }
+ }
+
+ suspend fun incrementSignalCount(gestureType: GestureType) =
+ repository.incrementSignalCount(gestureType)
+
+ suspend fun updateShortcutTriggerTime(gestureType: GestureType) =
+ repository.updateShortcutTriggerTime(gestureType)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt
new file mode 100644
index 0000000..643e571
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 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.education.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.shared.education.GestureType
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Encapsulates the update functions of KeyboardTouchpadEduStatsInteractor. This encapsulation is
+ * for having a different implementation of interactor when the feature flag is off.
+ */
+interface KeyboardTouchpadEduStatsInteractor {
+ fun incrementSignalCount(gestureType: GestureType)
+
+ fun updateShortcutTriggerTime(gestureType: GestureType)
+}
+
+/** Allow update to education data related to keyboard/touchpad. */
+@SysUISingleton
+class KeyboardTouchpadEduStatsInteractorImpl
+@Inject
+constructor(
+ @Background private val backgroundScope: CoroutineScope,
+ private val contextualEducationInteractor: ContextualEducationInteractor
+) : KeyboardTouchpadEduStatsInteractor {
+
+ override fun incrementSignalCount(gestureType: GestureType) {
+ // Todo: check if keyboard/touchpad is connected before update
+ backgroundScope.launch { contextualEducationInteractor.incrementSignalCount(gestureType) }
+ }
+
+ override fun updateShortcutTriggerTime(gestureType: GestureType) {
+ backgroundScope.launch {
+ contextualEducationInteractor.updateShortcutTriggerTime(gestureType)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 1ba274f..0e06117 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -37,6 +37,8 @@
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
import javax.inject.Inject
@@ -55,6 +57,7 @@
FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token
NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
+ NotificationMinimalismPrototype.token dependsOn NotificationsHeadsUpRefactor.token
// SceneContainer dependencies
SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index 893835a..59ec87a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.domain.interactor
import android.animation.ValueAnimator
+import android.util.Log
import com.android.app.animation.Interpolators
import com.android.app.tracing.coroutines.launch
import com.android.systemui.dagger.SysUISingleton
@@ -184,11 +185,7 @@
.filterRelevantKeyguardStateAnd { isOccluded -> isOccluded }
.collect {
if (!maybeHandleInsecurePowerGesture()) {
- startTransitionTo(
- toState = KeyguardState.OCCLUDED,
- modeOnCanceled = TransitionModeOnCanceled.RESET,
- ownerReason = "isOccluded = true",
- )
+ Log.i(TAG, "Ignoring change to isOccluded to prevent errant AOD->OCCLUDED")
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index ec03a6d..046e79c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -188,7 +188,7 @@
* Whether the system is dreaming. [isDreaming] will be always be true when [isDozing] is true,
* but not vice-versa.
*/
- val isDreaming: Flow<Boolean> = repository.isDreaming
+ val isDreaming: StateFlow<Boolean> = repository.isDreaming
/** Whether the system is dreaming with an overlay active */
val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay
@@ -205,7 +205,8 @@
trySendWithFailureLogging(
cameraLaunchSourceIntToModel(source),
TAG,
- "updated onCameraLaunchGestureDetected")
+ "updated onCameraLaunchGestureDetected"
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
index 7a06d2f..beb54c7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
@@ -129,13 +129,16 @@
}
}
- /** Notifies that the user has long-pressed on the lock screen. */
- fun onLongPress() {
+ /** Notifies that the user has long-pressed on the lock screen.
+ *
+ * @param isA11yAction: Whether the action was performed as an a11y action
+ */
+ fun onLongPress(isA11yAction: Boolean = false) {
if (!isLongPressHandlingEnabled.value) {
return
}
- if (featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP)) {
+ if (isA11yAction || featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP)) {
showSettings()
} else {
showMenu()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index 76f7749..1b9788f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -74,8 +74,8 @@
val bgView = view.bgView
longPressHandlingView.listener =
object : LongPressHandlingView.Listener {
- override fun onLongPressDetected(view: View, x: Int, y: Int) {
- if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
+ override fun onLongPressDetected(view: View, x: Int, y: Int, isA11yAction: Boolean) {
+ if (!isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
return
}
vibratorHelper.performHapticFeedback(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
index 057b4f9..b387855 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
@@ -18,12 +18,15 @@
package com.android.systemui.keyguard.ui.binder
import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.tracing.coroutines.launch
import com.android.systemui.common.ui.view.LongPressHandlingView
import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
import com.android.systemui.plugins.FalsingManager
object KeyguardLongPressViewBinder {
@@ -43,14 +46,19 @@
onSingleTap: () -> Unit,
falsingManager: FalsingManager,
) {
+ view.accessibilityHintLongPressAction =
+ AccessibilityNodeInfo.AccessibilityAction(
+ AccessibilityNodeInfoCompat.ACTION_LONG_CLICK,
+ view.resources.getString(R.string.lock_screen_settings)
+ )
view.listener =
object : LongPressHandlingView.Listener {
- override fun onLongPressDetected(view: View, x: Int, y: Int) {
- if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
+ override fun onLongPressDetected(view: View, x: Int, y: Int, isA11yAction: Boolean) {
+ if (!isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
return
}
- viewModel.onLongPress()
+ viewModel.onLongPress(isA11yAction)
}
override fun onSingleTapDetected(view: View) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
index bb4fb79..e9db1d2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
@@ -83,6 +83,7 @@
MathUtils.lerp(startAlpha, 0f, it)
}
},
+ onFinish = { 1f },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
index f1cbf25..1d2edc6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
@@ -33,9 +33,12 @@
/** Whether the long-press handling feature should be enabled. */
val isLongPressHandlingEnabled: Flow<Boolean> = interactor.isLongPressHandlingEnabled
- /** Notifies that the user has long-pressed on the lock screen. */
- fun onLongPress() {
- interactor.onLongPress()
+ /** Notifies that the user has long-pressed on the lock screen.
+ *
+ * @param isA11yAction: Whether the action was performed as an a11y action
+ */
+ fun onLongPress(isA11yAction: Boolean) {
+ interactor.onLongPress(isA11yAction)
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
index 46ba5d1..8811908 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
@@ -80,6 +80,7 @@
1f - it
}
},
+ onFinish = { 1f },
)
/** Bouncer container alpha */
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 51447cc..72f37fc 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
@@ -187,7 +187,6 @@
applicationScope.launch {
// TODO(b/296114544): Combine with some global hun state to make it visible!
deviceProvisioningInteractor.isDeviceProvisioned
- .distinctUntilChanged()
.flatMapLatest { isAllowedToBeVisible ->
if (isAllowedToBeVisible) {
combine(
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt
index 4f27b9e..b3d5c9e 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt
@@ -99,7 +99,7 @@
original,
updates.component,
updates.owner,
- type.displayId
+ type.displayId,
)
}
return updated
@@ -120,6 +120,7 @@
return replaceWithScreenshot(
original = original,
componentName = topMainRootTask?.topActivity ?: defaultComponent,
+ taskId = topMainRootTask?.taskId,
owner = defaultOwner,
displayId = original.displayId
)
@@ -144,11 +145,12 @@
)
}
- suspend fun replaceWithScreenshot(
+ private suspend fun replaceWithScreenshot(
original: ScreenshotData,
componentName: ComponentName?,
owner: UserHandle?,
displayId: Int,
+ taskId: Int? = null,
): ScreenshotData {
Log.i(TAG, "Capturing screenshot: $componentName / $owner")
val screenshot = captureDisplay(displayId)
@@ -157,7 +159,8 @@
bitmap = screenshot,
userHandle = owner,
topComponent = componentName,
- screenBounds = Rect(0, 0, screenshot?.width ?: 0, screenshot?.height ?: 0)
+ screenBounds = Rect(0, 0, screenshot?.width ?: 0, screenshot?.height ?: 0),
+ taskId = taskId ?: -1,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index d870fe6..d090aea 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -56,13 +56,12 @@
import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
-import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import com.android.systemui.util.kotlin.collectFlow
import java.util.function.Consumer
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
/**
@@ -165,7 +164,14 @@
*
* Based on [ShadeInteractor.isAnyFullyExpanded] and [ShadeInteractor.isUserInteracting].
*/
- private var shadeShowing = false
+ private var shadeShowingAndConsumingTouches = false
+
+ /**
+ * True if the shade ever fully expands and the user isn't interacting with it (aka finger on
+ * screen dragging). In this case, the shade should handle all touch events until it has fully
+ * collapsed.
+ */
+ private var userNotInteractiveAtShadeFullyExpanded = false
/**
* True if the device is dreaming, in which case we shouldn't do anything for top/bottom swipes
@@ -317,9 +323,25 @@
)
collectFlow(
containerView,
- allOf(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)),
- {
- shadeShowing = it
+ combine(
+ shadeInteractor.isAnyFullyExpanded,
+ shadeInteractor.isUserInteracting,
+ shadeInteractor.isShadeFullyCollapsed,
+ ::Triple
+ ),
+ { (isFullyExpanded, isUserInteracting, isShadeFullyCollapsed) ->
+ val expandedAndNotInteractive = isFullyExpanded && !isUserInteracting
+
+ // If we ever are fully expanded and not interacting, capture this state as we
+ // should not handle touches until we fully collapse again
+ userNotInteractiveAtShadeFullyExpanded =
+ !isShadeFullyCollapsed &&
+ (userNotInteractiveAtShadeFullyExpanded || expandedAndNotInteractive)
+
+ // If the shade reaches full expansion without interaction, then we should allow it
+ // to consume touches rather than handling it here until it disappears.
+ shadeShowingAndConsumingTouches =
+ userNotInteractiveAtShadeFullyExpanded || expandedAndNotInteractive
updateTouchHandlingState()
}
)
@@ -337,7 +359,8 @@
* Also clears gesture exclusion zones when the hub is occluded or gone.
*/
private fun updateTouchHandlingState() {
- val shouldInterceptGestures = hubShowing && !(shadeShowing || anyBouncerShowing)
+ val shouldInterceptGestures =
+ hubShowing && !(shadeShowingAndConsumingTouches || anyBouncerShowing)
if (shouldInterceptGestures) {
lifecycleRegistry.currentState = Lifecycle.State.RESUMED
} else {
@@ -395,11 +418,12 @@
private fun handleTouchEventOnCommunalView(view: View, ev: MotionEvent): Boolean {
val isDown = ev.actionMasked == MotionEvent.ACTION_DOWN
val isUp = ev.actionMasked == MotionEvent.ACTION_UP
+ val isMove = ev.actionMasked == MotionEvent.ACTION_MOVE
val isCancel = ev.actionMasked == MotionEvent.ACTION_CANCEL
- val hubOccluded = anyBouncerShowing || shadeShowing
+ val hubOccluded = anyBouncerShowing || shadeShowingAndConsumingTouches
- if (isDown && !hubOccluded) {
+ if ((isDown || isMove) && !hubOccluded) {
isTrackingHubTouch = true
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt
index b2140f7..130b117 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt
@@ -18,6 +18,7 @@
import android.content.Context
import android.content.res.ColorStateList
+import android.view.ContextThemeWrapper
import androidx.annotation.ColorInt
import com.android.settingslib.Utils
import com.android.systemui.res.R
@@ -41,8 +42,11 @@
/** The chip should have a red background with white text. */
data object Red : ColorsModel {
- override fun background(context: Context): ColorStateList =
- ColorStateList.valueOf(context.getColor(R.color.GM2_red_600))
+ override fun background(context: Context): ColorStateList {
+ val themedContext =
+ ContextThemeWrapper(context, com.android.internal.R.style.Theme_DeviceDefault_Light)
+ return Utils.getColorAttr(themedContext, com.android.internal.R.attr.materialColorError)
+ }
override fun text(context: Context) = context.getColor(android.R.color.white)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
index 3dcaff3..b342722 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
@@ -52,8 +52,11 @@
}
fun getNotificationBuckets(): IntArray {
- if (PriorityPeopleSection.isEnabled || NotificationMinimalismPrototype.V2.isEnabled
- || NotificationClassificationFlag.isEnabled) {
+ if (
+ PriorityPeopleSection.isEnabled ||
+ NotificationMinimalismPrototype.isEnabled ||
+ NotificationClassificationFlag.isEnabled
+ ) {
// We don't need this list to be adaptive, it can be the superset of all features.
return PriorityBucket.getAllInOrder()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
new file mode 100644
index 0000000..a6605f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
@@ -0,0 +1,295 @@
+/*
+ * 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.statusbar.notification.collection.coordinator
+
+import android.annotation.SuppressLint
+import android.app.NotificationManager
+import android.os.UserHandle
+import android.provider.Settings
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.collection.GroupEntry
+import com.android.systemui.statusbar.notification.collection.ListEntry
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING
+import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+
+/**
+ * If the setting is enabled, this will track seen notifications and ensure that they only show in
+ * the shelf on the lockscreen.
+ *
+ * This class is a replacement of the [OriginalUnseenKeyguardCoordinator].
+ */
+@CoordinatorScope
+@SuppressLint("SharedFlowCreation")
+class LockScreenMinimalismCoordinator
+@Inject
+constructor(
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ private val dumpManager: DumpManager,
+ private val headsUpInteractor: HeadsUpNotificationInteractor,
+ private val logger: LockScreenMinimalismCoordinatorLogger,
+ @Application private val scope: CoroutineScope,
+ private val secureSettings: SecureSettings,
+ private val seenNotificationsInteractor: SeenNotificationsInteractor,
+ private val statusBarStateController: StatusBarStateController,
+ private val shadeInteractor: ShadeInteractor,
+) : Coordinator, Dumpable {
+
+ private val unseenNotifications = mutableSetOf<NotificationEntry>()
+ private var isShadeVisible = false
+ private var unseenFilterEnabled = false
+
+ override fun attach(pipeline: NotifPipeline) {
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) {
+ return
+ }
+ pipeline.addPromoter(unseenNotifPromoter)
+ pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs)
+ pipeline.addCollectionListener(collectionListener)
+ scope.launch { trackUnseenFilterSettingChanges() }
+ dumpManager.registerDumpable(this)
+ }
+
+ private suspend fun trackSeenNotifications() {
+ coroutineScope {
+ launch { clearUnseenNotificationsWhenShadeIsExpanded() }
+ launch { markHeadsUpNotificationsAsSeen() }
+ }
+ }
+
+ private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() {
+ shadeInteractor.isShadeFullyExpanded.collectLatest { isExpanded ->
+ // Give keyguard events time to propagate, in case this expansion is part of the
+ // keyguard transition and not the user expanding the shade
+ delay(SHADE_VISIBLE_SEEN_TIMEOUT)
+ isShadeVisible = isExpanded
+ if (isExpanded) {
+ logger.logShadeVisible(unseenNotifications.size)
+ unseenNotifications.clear()
+ // no need to invalidateList; filtering is inactive while shade is open
+ } else {
+ logger.logShadeHidden()
+ }
+ }
+ }
+
+ private suspend fun markHeadsUpNotificationsAsSeen() {
+ headsUpInteractor.topHeadsUpRowIfPinned
+ .map { it?.let { headsUpInteractor.notificationKey(it) } }
+ .collectLatest { key ->
+ if (key == null) {
+ logger.logTopHeadsUpRow(key = null, wasUnseenWhenPinned = false)
+ } else {
+ val wasUnseenWhenPinned = unseenNotifications.any { it.key == key }
+ logger.logTopHeadsUpRow(key, wasUnseenWhenPinned)
+ if (wasUnseenWhenPinned) {
+ delay(HEADS_UP_SEEN_TIMEOUT)
+ val wasUnseenAfterDelay = unseenNotifications.removeIf { it.key == key }
+ logger.logHunHasBeenSeen(key, wasUnseenAfterDelay)
+ // no need to invalidateList; nothing should change until after heads up
+ }
+ }
+ }
+ }
+
+ private fun unseenFeatureEnabled(): Flow<Boolean> {
+ // TODO(b/330387368): create LOCK_SCREEN_NOTIFICATION_MINIMALISM setting to use here?
+ // Or should we actually just repurpose using the existing setting?
+ if (NotificationMinimalismPrototype.isEnabled) {
+ return flowOf(true)
+ }
+ return secureSettings
+ // emit whenever the setting has changed
+ .observerFlow(
+ UserHandle.USER_ALL,
+ Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+ )
+ // perform a query immediately
+ .onStart { emit(Unit) }
+ // for each change, lookup the new value
+ .map {
+ secureSettings.getIntForUser(
+ name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+ def = 0,
+ userHandle = UserHandle.USER_CURRENT,
+ ) == 1
+ }
+ // don't emit anything if nothing has changed
+ .distinctUntilChanged()
+ // perform lookups on the bg thread pool
+ .flowOn(bgDispatcher)
+ // only track the most recent emission, if events are happening faster than they can be
+ // consumed
+ .conflate()
+ }
+
+ private suspend fun trackUnseenFilterSettingChanges() {
+ unseenFeatureEnabled().collectLatest { isSettingEnabled ->
+ // update local field and invalidate if necessary
+ if (isSettingEnabled != unseenFilterEnabled) {
+ unseenFilterEnabled = isSettingEnabled
+ unseenNotifPromoter.invalidateList("unseen setting changed")
+ }
+ // if the setting is enabled, then start tracking and filtering unseen notifications
+ logger.logTrackingUnseen(isSettingEnabled)
+ if (isSettingEnabled) {
+ trackSeenNotifications()
+ }
+ }
+ }
+
+ private val collectionListener =
+ object : NotifCollectionListener {
+ override fun onEntryAdded(entry: NotificationEntry) {
+ if (!isShadeVisible) {
+ logger.logUnseenAdded(entry.key)
+ unseenNotifications.add(entry)
+ }
+ }
+
+ override fun onEntryUpdated(entry: NotificationEntry) {
+ if (!isShadeVisible) {
+ logger.logUnseenUpdated(entry.key)
+ unseenNotifications.add(entry)
+ }
+ }
+
+ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+ if (unseenNotifications.remove(entry)) {
+ logger.logUnseenRemoved(entry.key)
+ }
+ }
+ }
+
+ private fun pickOutTopUnseenNotifs(list: List<ListEntry>) {
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+ // Only ever elevate a top unseen notification on keyguard, not even locked shade
+ if (statusBarStateController.state != StatusBarState.KEYGUARD) {
+ seenNotificationsInteractor.setTopOngoingNotification(null)
+ seenNotificationsInteractor.setTopUnseenNotification(null)
+ return
+ }
+ // On keyguard pick the top-ranked unseen or ongoing notification to elevate
+ val nonSummaryEntries: Sequence<NotificationEntry> =
+ list
+ .asSequence()
+ .flatMap {
+ when (it) {
+ is NotificationEntry -> listOfNotNull(it)
+ is GroupEntry -> it.children
+ else -> error("unhandled type of $it")
+ }
+ }
+ .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT }
+ seenNotificationsInteractor.setTopOngoingNotification(
+ nonSummaryEntries
+ .filter { ColorizedFgsCoordinator.isRichOngoing(it) }
+ .minByOrNull { it.ranking.rank }
+ )
+ seenNotificationsInteractor.setTopUnseenNotification(
+ nonSummaryEntries
+ .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications }
+ .minByOrNull { it.ranking.rank }
+ )
+ }
+
+ @VisibleForTesting
+ val unseenNotifPromoter =
+ object : NotifPromoter(TAG) {
+ override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
+ when {
+ NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode() -> false
+ seenNotificationsInteractor.isTopOngoingNotification(child) -> true
+ !NotificationMinimalismPrototype.ungroupTopUnseen -> false
+ else -> seenNotificationsInteractor.isTopUnseenNotification(child)
+ }
+ }
+
+ val topOngoingSectioner =
+ object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) {
+ override fun isInSection(entry: ListEntry): Boolean {
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+ return entry.anyEntry { notificationEntry ->
+ seenNotificationsInteractor.isTopOngoingNotification(notificationEntry)
+ }
+ }
+ }
+
+ val topUnseenSectioner =
+ object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) {
+ override fun isInSection(entry: ListEntry): Boolean {
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+ return entry.anyEntry { notificationEntry ->
+ seenNotificationsInteractor.isTopUnseenNotification(notificationEntry)
+ }
+ }
+ }
+
+ private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) =
+ when {
+ predicate(representativeEntry) -> true
+ this !is GroupEntry -> false
+ else -> children.any(predicate)
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) =
+ with(pw.asIndenting()) {
+ seenNotificationsInteractor.dump(this)
+ printCollection("unseen notifications", unseenNotifications) { println(it.key) }
+ }
+
+ companion object {
+ private const val TAG = "LockScreenMinimalismCoordinator"
+ private val SHADE_VISIBLE_SEEN_TIMEOUT = 0.25.seconds
+ private val HEADS_UP_SEEN_TIMEOUT = 0.75.seconds
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
new file mode 100644
index 0000000..e44a77c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.statusbar.notification.collection.coordinator
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.UnseenNotificationLog
+import javax.inject.Inject
+
+private const val TAG = "LockScreenMinimalismCoordinator"
+
+class LockScreenMinimalismCoordinatorLogger
+@Inject
+constructor(
+ @UnseenNotificationLog private val buffer: LogBuffer,
+) {
+
+ fun logTrackingUnseen(trackingUnseen: Boolean) =
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ messageInitializer = { bool1 = trackingUnseen },
+ messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." },
+ )
+
+ fun logShadeVisible(numUnseen: Int) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ messageInitializer = { int1 = numUnseen },
+ messagePrinter = { "Shade expanded. Notifications marked as seen: $int1" }
+ )
+ }
+
+ fun logShadeHidden() {
+ buffer.log(TAG, LogLevel.DEBUG, "Shade no longer expanded.")
+ }
+
+ fun logUnseenAdded(key: String) =
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ messageInitializer = { str1 = key },
+ messagePrinter = { "Unseen notif added: $str1" },
+ )
+
+ fun logUnseenUpdated(key: String) =
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ messageInitializer = { str1 = key },
+ messagePrinter = { "Unseen notif updated: $str1" },
+ )
+
+ fun logUnseenRemoved(key: String) =
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ messageInitializer = { str1 = key },
+ messagePrinter = { "Unseen notif removed: $str1" },
+ )
+
+ fun logHunHasBeenSeen(key: String, wasUnseen: Boolean) =
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ messageInitializer = {
+ str1 = key
+ bool1 = wasUnseen
+ },
+ messagePrinter = { "Heads up notif has been seen: $str1 wasUnseen=$bool1" },
+ )
+
+ fun logTopHeadsUpRow(key: String?, wasUnseenWhenPinned: Boolean) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ messageInitializer = {
+ str1 = key
+ bool1 = wasUnseenWhenPinned
+ },
+ messagePrinter = { "New notif is top heads up: $str1 wasUnseen=$bool1" },
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index 99327d1..73ce48b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -47,6 +47,7 @@
hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator,
keyguardCoordinator: KeyguardCoordinator,
unseenKeyguardCoordinator: OriginalUnseenKeyguardCoordinator,
+ lockScreenMinimalismCoordinator: LockScreenMinimalismCoordinator,
rankingCoordinator: RankingCoordinator,
colorizedFgsCoordinator: ColorizedFgsCoordinator,
deviceProvisionedCoordinator: DeviceProvisionedCoordinator,
@@ -87,7 +88,11 @@
mCoordinators.add(hideLocallyDismissedNotifsCoordinator)
mCoordinators.add(hideNotifsForOtherUsersCoordinator)
mCoordinators.add(keyguardCoordinator)
- mCoordinators.add(unseenKeyguardCoordinator)
+ if (NotificationMinimalismPrototype.isEnabled) {
+ mCoordinators.add(lockScreenMinimalismCoordinator)
+ } else {
+ mCoordinators.add(unseenKeyguardCoordinator)
+ }
mCoordinators.add(rankingCoordinator)
mCoordinators.add(colorizedFgsCoordinator)
mCoordinators.add(deviceProvisionedCoordinator)
@@ -120,12 +125,12 @@
}
// Manually add Ordered Sections
- if (NotificationMinimalismPrototype.V2.isEnabled) {
- mOrderedSections.add(unseenKeyguardCoordinator.topOngoingSectioner) // Top Ongoing
+ if (NotificationMinimalismPrototype.isEnabled) {
+ mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing
}
mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp
- if (NotificationMinimalismPrototype.V2.isEnabled) {
- mOrderedSections.add(unseenKeyguardCoordinator.topUnseenSectioner) // Top Unseen
+ if (NotificationMinimalismPrototype.isEnabled) {
+ mOrderedSections.add(lockScreenMinimalismCoordinator.topUnseenSectioner) // Top Unseen
}
mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService
if (PriorityPeopleSection.isEnabled) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
index 5dd1663..5b25b11 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar.notification.collection.coordinator
import android.annotation.SuppressLint
-import android.app.NotificationManager
import android.os.UserHandle
import android.provider.Settings
import androidx.annotation.VisibleForTesting
@@ -30,21 +29,14 @@
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.expansionChanges
-import com.android.systemui.statusbar.notification.collection.GroupEntry
-import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
-import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING
-import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.headsUpEvents
import com.android.systemui.util.asIndenting
@@ -73,9 +65,12 @@
import kotlinx.coroutines.yield
/**
- * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section
- * headers on the lockscreen. If enabled, it will also track and hide seen notifications on the
- * lockscreen.
+ * If the setting is enabled, this will track and hide seen notifications on the lockscreen.
+ *
+ * This is the "original" unseen keyguard coordinator because this is the logic originally developed
+ * for large screen devices where showing "seen" notifications on the lock screen was distracting.
+ * Moreover, this file was created during a project that will replace this logic, so the
+ * [LockScreenMinimalismCoordinator] is the expected replacement of this file.
*/
@CoordinatorScope
@SuppressLint("SharedFlowCreation")
@@ -100,10 +95,7 @@
private var unseenFilterEnabled = false
override fun attach(pipeline: NotifPipeline) {
- if (NotificationMinimalismPrototype.V2.isEnabled) {
- pipeline.addPromoter(unseenNotifPromoter)
- pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs)
- }
+ NotificationMinimalismPrototype.assertInLegacyMode()
pipeline.addFinalizeFilter(unseenNotifFilter)
pipeline.addCollectionListener(collectionListener)
scope.launch { trackUnseenFilterSettingChanges() }
@@ -112,6 +104,7 @@
private suspend fun trackSeenNotifications() {
// Whether or not keyguard is visible (or occluded).
+ @Suppress("DEPRECATION")
val isKeyguardPresentFlow: Flow<Boolean> =
keyguardTransitionInteractor
.transitionValue(
@@ -265,11 +258,9 @@
}
private fun unseenFeatureEnabled(): Flow<Boolean> {
- if (
- NotificationMinimalismPrototype.V1.isEnabled ||
- NotificationMinimalismPrototype.V2.isEnabled
- ) {
- return flowOf(true)
+ if (NotificationMinimalismPrototype.isEnabled) {
+ // TODO(b/330387368): should this really just be turned off? If so, hide the setting.
+ return flowOf(false)
}
return secureSettings
// emit whenever the setting has changed
@@ -340,110 +331,18 @@
}
}
- private fun pickOutTopUnseenNotifs(list: List<ListEntry>) {
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
- // Only ever elevate a top unseen notification on keyguard, not even locked shade
- if (statusBarStateController.state != StatusBarState.KEYGUARD) {
- seenNotificationsInteractor.setTopOngoingNotification(null)
- seenNotificationsInteractor.setTopUnseenNotification(null)
- return
- }
- // On keyguard pick the top-ranked unseen or ongoing notification to elevate
- val nonSummaryEntries: Sequence<NotificationEntry> =
- list
- .asSequence()
- .flatMap {
- when (it) {
- is NotificationEntry -> listOfNotNull(it)
- is GroupEntry -> it.children
- else -> error("unhandled type of $it")
- }
- }
- .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT }
- seenNotificationsInteractor.setTopOngoingNotification(
- nonSummaryEntries
- .filter { ColorizedFgsCoordinator.isRichOngoing(it) }
- .minByOrNull { it.ranking.rank }
- )
- seenNotificationsInteractor.setTopUnseenNotification(
- nonSummaryEntries
- .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications }
- .minByOrNull { it.ranking.rank }
- )
- }
-
- @VisibleForTesting
- val unseenNotifPromoter =
- object : NotifPromoter("$TAG-unseen") {
- override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
- else if (!NotificationMinimalismPrototype.V2.ungroupTopUnseen) false
- else
- seenNotificationsInteractor.isTopOngoingNotification(child) ||
- seenNotificationsInteractor.isTopUnseenNotification(child)
- }
-
- val topOngoingSectioner =
- object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) {
- override fun isInSection(entry: ListEntry): Boolean {
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false
- return entry.anyEntry { notificationEntry ->
- seenNotificationsInteractor.isTopOngoingNotification(notificationEntry)
- }
- }
- }
-
- val topUnseenSectioner =
- object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) {
- override fun isInSection(entry: ListEntry): Boolean {
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false
- return entry.anyEntry { notificationEntry ->
- seenNotificationsInteractor.isTopUnseenNotification(notificationEntry)
- }
- }
- }
-
- private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) =
- when {
- predicate(representativeEntry) -> true
- this !is GroupEntry -> false
- else -> children.any(predicate)
- }
-
@VisibleForTesting
val unseenNotifFilter =
- object : NotifFilter("$TAG-unseen") {
+ object : NotifFilter(TAG) {
var hasFilteredAnyNotifs = false
- /**
- * Encapsulates a definition of "being on the keyguard". Note that these two definitions
- * are wildly different: [StatusBarState.KEYGUARD] is when on the lock screen and does
- * not include shade or occluded states, whereas [KeyguardRepository.isKeyguardShowing]
- * is any state where the keyguard has not been dismissed, including locked shade and
- * occluded lock screen.
- *
- * Returning false for locked shade and occluded states means that this filter will
- * allow seen notifications to appear in the locked shade.
- */
- private fun isOnKeyguard(): Boolean =
- if (NotificationMinimalismPrototype.V2.isEnabled) {
- false // disable this feature under this prototype
- } else if (
- NotificationMinimalismPrototype.V1.isEnabled &&
- NotificationMinimalismPrototype.V1.showOnLockedShade
- ) {
- statusBarStateController.state == StatusBarState.KEYGUARD
- } else {
- keyguardRepository.isKeyguardShowing()
- }
-
override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
when {
// Don't apply filter if the setting is disabled
!unseenFilterEnabled -> false
// Don't apply filter if the keyguard isn't currently showing
- !isOnKeyguard() -> false
+ !keyguardRepository.isKeyguardShowing() -> false
// Don't apply the filter if the notification is unseen
unseenNotifications.contains(entry) -> false
// Don't apply the filter to (non-promoted) group summaries
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
index caa6c17..71c98b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
@@ -150,8 +150,9 @@
if (entry == null) {
return false;
}
- boolean isTopUnseen = NotificationMinimalismPrototype.V2.isEnabled()
- && mSeenNotificationsInteractor.isTopUnseenNotification(entry);
+ boolean isTopUnseen = NotificationMinimalismPrototype.isEnabled()
+ && (mSeenNotificationsInteractor.isTopUnseenNotification(entry)
+ || mSeenNotificationsInteractor.isTopOngoingNotification(entry));
if (isTopUnseen || mHeadsUpManager.isHeadsUpEntry(entry.getKey())) {
return !mVisibilityLocationProvider.isInVisibleLocation(entry);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
index bf44b9f..24b75d4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
@@ -30,6 +30,7 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -44,8 +45,17 @@
private val shadeInteractor: ShadeInteractor,
) {
+ /** The top-ranked heads up row, regardless of pinned state */
val topHeadsUpRow: Flow<HeadsUpRowKey?> = headsUpRepository.topHeadsUpRow
+ /** The top-ranked heads up row, if that row is pinned */
+ val topHeadsUpRowIfPinned: Flow<HeadsUpRowKey?> =
+ headsUpRepository.topHeadsUpRow
+ .flatMapLatest { repository ->
+ repository?.isPinned?.map { pinned -> repository.takeIf { pinned } } ?: flowOf(null)
+ }
+ .distinctUntilChanged()
+
/** Set of currently pinned top-level heads up rows to be displayed. */
val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy {
if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
@@ -89,10 +99,10 @@
flowOf(false)
} else {
combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) {
- hasPinnedRows,
- animatingAway ->
- hasPinnedRows || animatingAway
- }
+ hasPinnedRows,
+ animatingAway ->
+ hasPinnedRows || animatingAway
+ }
}
}
@@ -127,6 +137,9 @@
fun elementKeyFor(key: HeadsUpRowKey) = (key as HeadsUpRowRepository).elementKey
+ /** Returns the Notification Key (the standard string) of this row. */
+ fun notificationKey(key: HeadsUpRowKey): String = (key as HeadsUpRowRepository).key
+
fun setHeadsUpAnimatingAway(animatingAway: Boolean) {
headsUpRepository.setHeadsUpAnimatingAway(animatingAway)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
index 85c66bd..948a3c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
@@ -16,10 +16,12 @@
package com.android.systemui.statusbar.notification.domain.interactor
+import android.util.IndentingPrintWriter
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.util.printSection
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow
@@ -41,24 +43,42 @@
/** Set the entry that is identified as the top ongoing notification. */
fun setTopOngoingNotification(entry: NotificationEntry?) {
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
notificationListRepository.topOngoingNotificationKey.value = entry?.key
}
/** Determine if the given notification is the top ongoing notification. */
fun isTopOngoingNotification(entry: NotificationEntry?): Boolean =
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
else
entry != null && notificationListRepository.topOngoingNotificationKey.value == entry.key
/** Set the entry that is identified as the top unseen notification. */
fun setTopUnseenNotification(entry: NotificationEntry?) {
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
notificationListRepository.topUnseenNotificationKey.value = entry?.key
}
/** Determine if the given notification is the top unseen notification. */
fun isTopUnseenNotification(entry: NotificationEntry?): Boolean =
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
else entry != null && notificationListRepository.topUnseenNotificationKey.value == entry.key
+
+ fun dump(pw: IndentingPrintWriter) =
+ with(pw) {
+ printSection("SeenNotificationsInteractor") {
+ print(
+ "hasFilteredOutSeenNotifications",
+ notificationListRepository.hasFilteredOutSeenNotifications.value
+ )
+ print(
+ "topOngoingNotificationKey",
+ notificationListRepository.topOngoingNotificationKey.value
+ )
+ print(
+ "topUnseenNotificationKey",
+ notificationListRepository.topUnseenNotificationKey.value
+ )
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
index bf37036..06f3db5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
@@ -24,102 +24,43 @@
/** Helper for reading or using the minimalism prototype flag state. */
@Suppress("NOTHING_TO_INLINE")
object NotificationMinimalismPrototype {
+ const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
- val version: Int by lazy {
- SystemProperties.getInt("persist.notification_minimalism_prototype.version", 2)
- }
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
- object V1 {
- /** The aconfig flag name */
- const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
+ /** Is the heads-up cycling animation enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.notificationMinimalismPrototype()
- /** A token used for dependency declaration */
- val token: FlagToken
- get() = FlagToken(FLAG_NAME, isEnabled)
+ /**
+ * The prototype will (by default) use a promoter to ensure that the top unseen notification is
+ * not grouped, but this property read allows that behavior to be disabled.
+ */
+ val ungroupTopUnseen: Boolean
+ get() =
+ if (isUnexpectedlyInLegacyMode()) false
+ else
+ SystemProperties.getBoolean(
+ "persist.notification_minimalism_prototype.ungroup_top_unseen",
+ false
+ )
- /** Is the heads-up cycling animation enabled */
- @JvmStatic
- inline val isEnabled
- get() = Flags.notificationMinimalismPrototype() && version == 1
+ /**
+ * 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)
- /**
- * the prototype will now show seen notifications on the locked shade by default, but this
- * property read allows that to be quickly disabled for testing
- */
- val showOnLockedShade: Boolean
- get() =
- if (isUnexpectedlyInLegacyMode()) false
- else
- SystemProperties.getBoolean(
- "persist.notification_minimalism_prototype.show_on_locked_shade",
- true
- )
-
- /** gets the configurable max number of notifications */
- val maxNotifs: Int
- get() =
- if (isUnexpectedlyInLegacyMode()) -1
- else
- SystemProperties.getInt(
- "persist.notification_minimalism_prototype.lock_screen_max_notifs",
- 1
- )
-
- /**
- * 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)
- }
- object V2 {
- const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
-
- /** A token used for dependency declaration */
- val token: FlagToken
- get() = FlagToken(FLAG_NAME, isEnabled)
-
- /** Is the heads-up cycling animation enabled */
- @JvmStatic
- inline val isEnabled
- get() = Flags.notificationMinimalismPrototype() && version == 2
-
- /**
- * The prototype will (by default) use a promoter to ensure that the top unseen notification
- * is not grouped, but this property read allows that behavior to be disabled.
- */
- val ungroupTopUnseen: Boolean
- get() =
- if (isUnexpectedlyInLegacyMode()) false
- else
- SystemProperties.getBoolean(
- "persist.notification_minimalism_prototype.ungroup_top_unseen",
- true
- )
-
- /**
- * 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)
- }
+ /**
+ * 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/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index 391bc43..06222fd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -74,7 +74,7 @@
/** Whether we allow keyguard to show less important notifications above the shelf. */
private val limitLockScreenToOneImportant
- get() = NotificationMinimalismPrototype.V2.isEnabled
+ get() = NotificationMinimalismPrototype.isEnabled
/** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */
private var dividerHeight by notNull<Float>()
@@ -405,16 +405,8 @@
fun updateResources() {
maxKeyguardNotifications =
- infiniteIfNegative(
- if (NotificationMinimalismPrototype.V1.isEnabled) {
- NotificationMinimalismPrototype.V1.maxNotifs
- } else {
- resources.getInteger(R.integer.keyguard_max_notification_count)
- }
- )
- maxNotificationsExcludesMedia =
- NotificationMinimalismPrototype.V1.isEnabled ||
- NotificationMinimalismPrototype.V2.isEnabled
+ infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count))
+ maxNotificationsExcludesMedia = NotificationMinimalismPrototype.isEnabled
dividerHeight =
max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index 97266c5..86c7c6b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -24,6 +24,7 @@
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.ActivityStarter.OnDismissAction
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.util.concurrency.DelayableExecutor
import dagger.Lazy
@@ -36,10 +37,16 @@
constructor(
private val statusBarStateController: SysuiStatusBarStateController,
@Main private val mainExecutor: DelayableExecutor,
+ activityStarterInternal: Lazy<ActivityStarterInternalImpl>,
legacyActivityStarter: Lazy<LegacyActivityStarterInternalImpl>
) : ActivityStarter {
- private val activityStarterInternal: ActivityStarterInternal = legacyActivityStarter.get()
+ private val activityStarterInternal: ActivityStarterInternal =
+ if (SceneContainerFlag.isEnabled) {
+ activityStarterInternal.get()
+ } else {
+ legacyActivityStarter.get()
+ }
override fun startPendingIntentDismissingKeyguard(intent: PendingIntent) {
activityStarterInternal.startPendingIntentDismissingKeyguard(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
index ae98e1d..107bf1e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
@@ -16,23 +16,93 @@
package com.android.systemui.statusbar.phone
+import android.app.ActivityManager
+import android.app.ActivityOptions
+import android.app.ActivityTaskManager
import android.app.PendingIntent
+import android.app.TaskStackBuilder
+import android.content.Context
import android.content.Intent
+import android.content.res.Resources
import android.os.Bundle
+import android.os.RemoteException
import android.os.UserHandle
+import android.provider.Settings
+import android.util.Log
+import android.view.RemoteAnimationAdapter
import android.view.View
+import android.view.WindowManager
+import com.android.systemui.ActivityIntentHelper
+import com.android.systemui.Flags
import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.DelegateTransitionAnimatorController
+import com.android.systemui.assist.AssistManager
+import com.android.systemui.camera.CameraIntents
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.DisplayId
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.keyguard.KeyguardViewMediator
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.ShadeController
+import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.kotlin.getOrNull
+import dagger.Lazy
+import java.util.Optional
import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
/**
- * Encapsulates the activity logic for activity starter when flexiglass is enabled.
+ * Encapsulates the activity logic for activity starter when the SceneContainerFlag is enabled.
*
* TODO: b/308819693
*/
+@ExperimentalCoroutinesApi
@SysUISingleton
-class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInternal {
+class ActivityStarterInternalImpl
+@Inject
+constructor(
+ private val statusBarKeyguardViewManagerLazy: Lazy<StatusBarKeyguardViewManager>,
+ private val keyguardInteractor: KeyguardInteractor,
+ private val centralSurfacesOptLazy: Lazy<Optional<CentralSurfaces>>,
+ private val context: Context,
+ @Main private val resources: Resources,
+ private val selectedUserInteractor: SelectedUserInteractor,
+ private val deviceEntryInteractor: DeviceEntryInteractor,
+ private val activityTransitionAnimator: ActivityTransitionAnimator,
+ @DisplayId private val displayId: Int,
+ private val deviceProvisioningInteractor: DeviceProvisioningInteractor,
+ private val activityIntentHelper: ActivityIntentHelper,
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ private val assistManagerLazy: Lazy<AssistManager>,
+ @Main private val mainExecutor: DelayableExecutor,
+ private val shadeControllerLazy: Lazy<ShadeController>,
+ private val communalSceneInteractor: CommunalSceneInteractor,
+ private val statusBarWindowController: StatusBarWindowController,
+ private val keyguardViewMediatorLazy: Lazy<KeyguardViewMediator>,
+ private val shadeAnimationInteractor: ShadeAnimationInteractor,
+ private val notifShadeWindowControllerLazy: Lazy<NotificationShadeWindowController>,
+ private val commandQueue: CommandQueue,
+ private val lockScreenUserManager: NotificationLockscreenUserManager,
+) : ActivityStarterInternal {
+ private val centralSurfaces: CentralSurfaces?
+ get() = centralSurfacesOptLazy.get().getOrNull()
+
override fun startPendingIntentDismissingKeyguard(
intent: PendingIntent,
dismissShade: Boolean,
@@ -45,7 +115,119 @@
extraOptions: Bundle?,
customMessage: String?,
) {
- TODO("Not yet implemented b/308819693")
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+ val animationController =
+ if (associatedView is ExpandableNotificationRow) {
+ centralSurfaces?.getAnimatorControllerFromNotification(associatedView)
+ } else animationController
+
+ val willLaunchResolverActivity =
+ intent.isActivity &&
+ activityIntentHelper.wouldPendingLaunchResolverActivity(
+ intent,
+ lockScreenUserManager.currentUserId,
+ )
+
+ val actuallyShowOverLockscreen =
+ showOverLockscreen &&
+ intent.isActivity &&
+ (skipLockscreenChecks ||
+ activityIntentHelper.wouldPendingShowOverLockscreen(
+ intent,
+ lockScreenUserManager.currentUserId
+ ))
+
+ val animate =
+ !willLaunchResolverActivity &&
+ animationController != null &&
+ shouldAnimateLaunch(intent.isActivity, actuallyShowOverLockscreen)
+
+ // We wrap animationCallback with a StatusBarLaunchAnimatorController so
+ // that the shade is collapsed after the animation (or when it is cancelled,
+ // aborted, etc).
+ val statusBarController =
+ wrapAnimationControllerForShadeOrStatusBar(
+ animationController = animationController,
+ dismissShade = dismissShade,
+ isLaunchForActivity = intent.isActivity,
+ )
+ val controller =
+ if (actuallyShowOverLockscreen) {
+ wrapAnimationControllerForLockscreen(dismissShade, statusBarController)
+ } else {
+ statusBarController
+ }
+
+ // If we animate, don't collapse the shade and defer the keyguard dismiss (in case we
+ // run the animation on the keyguard). The animation will take care of (instantly)
+ // collapsing the shade and hiding the keyguard once it is done.
+ val collapse = dismissShade && !animate
+ val runnable = Runnable {
+ try {
+ activityTransitionAnimator.startPendingIntentWithAnimation(
+ controller,
+ animate,
+ intent.creatorPackage,
+ actuallyShowOverLockscreen,
+ object : ActivityTransitionAnimator.PendingIntentStarter {
+ override fun startPendingIntent(
+ animationAdapter: RemoteAnimationAdapter?
+ ): Int {
+ val options =
+ ActivityOptions(
+ CentralSurfaces.getActivityOptions(displayId, animationAdapter)
+ .apply { extraOptions?.let { putAll(it) } }
+ )
+ // TODO b/221255671: restrict this to only be set for notifications
+ options.isEligibleForLegacyPermissionPrompt = true
+ options.setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+ )
+ return intent.sendAndReturnResult(
+ context,
+ 0,
+ fillInIntent,
+ null,
+ null,
+ null,
+ options.toBundle()
+ )
+ }
+ },
+ )
+ } catch (e: PendingIntent.CanceledException) {
+ // the stack trace isn't very helpful here.
+ // Just log the exception message.
+ Log.w(TAG, "Sending intent failed: $e")
+ if (!collapse) {
+ // executeRunnableDismissingKeyguard did not collapse for us already.
+ shadeControllerLazy.get().collapseOnMainThread()
+ }
+ // TODO: Dismiss Keyguard.
+ }
+ if (intent.isActivity) {
+ assistManagerLazy.get().hideAssist()
+ // This activity could have started while the device is dreaming, in which case
+ // the dream would occlude the activity. In order to show the newly started
+ // activity, we wake from the dream.
+ centralSurfaces?.awakenDreams()
+ }
+ intentSentUiThreadCallback?.let { mainExecutor.execute(it) }
+ }
+
+ if (!actuallyShowOverLockscreen) {
+ mainExecutor.execute {
+ executeRunnableDismissingKeyguard(
+ runnable = runnable,
+ afterKeyguardGone = willLaunchResolverActivity,
+ dismissShade = collapse,
+ willAnimateOnKeyguard = animate,
+ customMessage = customMessage,
+ )
+ }
+ } else {
+ mainExecutor.execute(runnable)
+ }
}
override fun startActivityDismissingKeyguard(
@@ -59,7 +241,116 @@
disallowEnterPictureInPictureWhileLaunching: Boolean,
userHandle: UserHandle?
) {
- TODO("Not yet implemented b/308819693")
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+ val userHandle: UserHandle = userHandle ?: getActivityUserHandle(intent)
+
+ if (onlyProvisioned && !deviceProvisioningInteractor.isDeviceProvisioned()) return
+
+ val willLaunchResolverActivity: Boolean =
+ activityIntentHelper.wouldLaunchResolverActivity(
+ intent,
+ selectedUserInteractor.getSelectedUserId(),
+ )
+
+ val animate =
+ animationController != null &&
+ !willLaunchResolverActivity &&
+ shouldAnimateLaunch(isActivityIntent = true)
+ val animController =
+ wrapAnimationControllerForShadeOrStatusBar(
+ animationController = animationController,
+ dismissShade = dismissShade,
+ isLaunchForActivity = true,
+ )
+
+ // If we animate, we will dismiss the shade only once the animation is done. This is
+ // taken care of by the StatusBarLaunchAnimationController.
+ val dismissShadeDirectly = dismissShade && animController == null
+
+ val runnable = Runnable {
+ assistManagerLazy.get().hideAssist()
+ intent.flags =
+ if (intent.flags and Intent.FLAG_ACTIVITY_REORDER_TO_FRONT != 0) {
+ Intent.FLAG_ACTIVITY_NEW_TASK
+ } else {
+ Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ }
+ intent.addFlags(flags)
+ val result = intArrayOf(ActivityManager.START_CANCELED)
+ activityTransitionAnimator.startIntentWithAnimation(
+ animController,
+ animate,
+ intent.getPackage()
+ ) { adapter: RemoteAnimationAdapter? ->
+ val options =
+ ActivityOptions(CentralSurfaces.getActivityOptions(displayId, adapter))
+
+ // We know that the intent of the caller is to dismiss the keyguard and
+ // this runnable is called right after the keyguard is solved, so we tell
+ // WM that we should dismiss it to avoid flickers when opening an activity
+ // that can also be shown over the keyguard.
+ options.setDismissKeyguardIfInsecure()
+ options.setDisallowEnterPictureInPictureWhileLaunching(
+ disallowEnterPictureInPictureWhileLaunching
+ )
+ if (CameraIntents.isInsecureCameraIntent(intent)) {
+ // Normally an activity will set it's requested rotation
+ // animation on its window. However when launching an activity
+ // causes the orientation to change this is too late. In these cases
+ // the default animation is used. This doesn't look good for
+ // the camera (as it rotates the camera contents out of sync
+ // with physical reality). So, we ask the WindowManager to
+ // force the cross fade animation if an orientation change
+ // happens to occur during the launch.
+ options.rotationAnimationHint =
+ WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS
+ }
+ if (Settings.Panel.ACTION_VOLUME == intent.action) {
+ // Settings Panel is implemented as activity(not a dialog), so
+ // underlying app is paused and may enter picture-in-picture mode
+ // as a result.
+ // So we need to disable picture-in-picture mode here
+ // if it is volume panel.
+ options.setDisallowEnterPictureInPictureWhileLaunching(true)
+ }
+ try {
+ result[0] =
+ ActivityTaskManager.getService()
+ .startActivityAsUser(
+ null,
+ context.basePackageName,
+ context.attributionTag,
+ intent,
+ intent.resolveTypeIfNeeded(context.contentResolver),
+ null,
+ null,
+ 0,
+ Intent.FLAG_ACTIVITY_NEW_TASK,
+ null,
+ options.toBundle(),
+ userHandle.identifier,
+ )
+ } catch (e: RemoteException) {
+ Log.w(TAG, "Unable to start activity", e)
+ }
+ result[0]
+ }
+ callback?.onActivityStarted(result[0])
+ }
+ val cancelRunnable = Runnable {
+ callback?.onActivityStarted(ActivityManager.START_CANCELED)
+ }
+ // Do not deferKeyguard when occluded because, when keyguard is occluded,
+ // we do not launch the activity until keyguard is done.
+ executeRunnableDismissingKeyguard(
+ runnable,
+ cancelRunnable,
+ dismissShadeDirectly,
+ willLaunchResolverActivity,
+ deferred = !isKeyguardOccluded(),
+ animate,
+ customMessage,
+ )
}
override fun startActivity(
@@ -69,7 +360,64 @@
showOverLockscreenWhenLocked: Boolean,
userHandle: UserHandle?
) {
- TODO("Not yet implemented b/308819693")
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+ val userHandle = userHandle ?: getActivityUserHandle(intent)
+ // Make sure that we dismiss the keyguard if it is directly dismissible or when we don't
+ // want to show the activity above it.
+ if (deviceEntryInteractor.isUnlocked.value || !showOverLockscreenWhenLocked) {
+ startActivityDismissingKeyguard(
+ intent = intent,
+ onlyProvisioned = false,
+ dismissShade = dismissShade,
+ disallowEnterPictureInPictureWhileLaunching = false,
+ callback = null,
+ flags = 0,
+ animationController = animationController,
+ userHandle = userHandle,
+ )
+ return
+ }
+
+ val animate =
+ animationController != null &&
+ shouldAnimateLaunch(
+ isActivityIntent = true,
+ showOverLockscreen = showOverLockscreenWhenLocked
+ )
+
+ var controller: ActivityTransitionAnimator.Controller? = null
+ if (animate) {
+ // Wrap the animation controller to dismiss the shade and set
+ // mIsLaunchingActivityOverLockscreen during the animation.
+ val delegate =
+ wrapAnimationControllerForShadeOrStatusBar(
+ animationController = animationController,
+ dismissShade = dismissShade,
+ isLaunchForActivity = true,
+ )
+ controller = wrapAnimationControllerForLockscreen(dismissShade, delegate)
+ } else if (dismissShade) {
+ // The animation will take care of dismissing the shade at the end of the animation.
+ // If we don't animate, collapse it directly.
+ shadeControllerLazy.get().cancelExpansionAndCollapseShade()
+ }
+
+ // We should exit the dream to prevent the activity from starting below the
+ // dream.
+ if (keyguardInteractor.isDreaming.value) {
+ centralSurfaces?.awakenDreams()
+ }
+
+ activityTransitionAnimator.startIntentWithAnimation(
+ controller,
+ animate,
+ intent.getPackage(),
+ showOverLockscreenWhenLocked
+ ) { adapter: RemoteAnimationAdapter? ->
+ TaskStackBuilder.create(context)
+ .addNextIntent(intent)
+ .startActivities(CentralSurfaces.getActivityOptions(displayId, adapter), userHandle)
+ }
}
override fun dismissKeyguardThenExecute(
@@ -78,7 +426,23 @@
afterKeyguardGone: Boolean,
customMessage: String?
) {
- TODO("Not yet implemented b/308819693")
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+ Log.i(TAG, "Invoking dismissKeyguardThenExecute, afterKeyguardGone: $afterKeyguardGone")
+
+ // TODO b/308819693: startWakeAndUnlock animation when pulsing
+
+ if (isKeyguardShowing()) {
+ statusBarKeyguardViewManagerLazy
+ .get()
+ .dismissWithAction(action, cancel, afterKeyguardGone, customMessage)
+ } else {
+ // If the keyguard isn't showing but the device is dreaming, we should exit the
+ // dream.
+ if (keyguardInteractor.isDreaming.value) {
+ centralSurfaces?.awakenDreams()
+ }
+ action.onDismiss()
+ }
}
override fun executeRunnableDismissingKeyguard(
@@ -90,10 +454,195 @@
willAnimateOnKeyguard: Boolean,
customMessage: String?
) {
- TODO("Not yet implemented b/308819693")
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+ val onDismissAction: ActivityStarter.OnDismissAction =
+ object : ActivityStarter.OnDismissAction {
+ override fun onDismiss(): Boolean {
+ if (runnable != null) {
+ if (isKeyguardOccluded()) {
+ statusBarKeyguardViewManagerLazy
+ .get()
+ .addAfterKeyguardGoneRunnable(runnable)
+ } else {
+ mainExecutor.execute(runnable)
+ }
+ }
+ if (dismissShade) {
+ shadeControllerLazy.get().collapseShadeForActivityStart()
+ }
+ if (Flags.communalHub()) {
+ communalSceneInteractor.changeSceneForActivityStartOnDismissKeyguard()
+ }
+ return deferred
+ }
+
+ override fun willRunAnimationOnKeyguard(): Boolean {
+ if (Flags.communalHub() && communalSceneInteractor.isIdleOnCommunal.value) {
+ // Override to false when launching activity over the hub that requires auth
+ return false
+ }
+ return willAnimateOnKeyguard
+ }
+ }
+ dismissKeyguardThenExecute(
+ onDismissAction,
+ cancelAction,
+ afterKeyguardGone,
+ customMessage,
+ )
}
override fun shouldAnimateLaunch(isActivityIntent: Boolean): Boolean {
- TODO("Not yet implemented b/308819693")
+ return shouldAnimateLaunch(isActivityIntent, false)
+ }
+
+ /**
+ * Whether we should animate an activity launch.
+ *
+ * Note: This method must be called *before* dismissing the keyguard.
+ */
+ private fun shouldAnimateLaunch(
+ isActivityIntent: Boolean,
+ showOverLockscreen: Boolean,
+ ): Boolean {
+ // TODO(b/294418322): always support launch animations when occluded.
+ val ignoreOcclusion = showOverLockscreen && Flags.mediaLockscreenLaunchAnimation()
+ if (isKeyguardOccluded() && !ignoreOcclusion) {
+ return false
+ }
+
+ // Always animate if we are not showing the keyguard or if we animate over the lockscreen
+ // (without unlocking it).
+ if (showOverLockscreen || !isKeyguardShowing()) {
+ return true
+ }
+
+ // We don't animate non-activity launches as they can break the animation.
+ // TODO(b/184121838): Support non activity launches on the lockscreen.
+ return isActivityIntent
+ }
+
+ /** Retrieves the current user handle to start the Activity. */
+ private fun getActivityUserHandle(intent: Intent): UserHandle {
+ val packages: Array<String> = resources.getStringArray(R.array.system_ui_packages)
+ for (pkg in packages) {
+ val componentName = intent.component ?: break
+ if (pkg == componentName.packageName) {
+ return UserHandle(UserHandle.myUserId())
+ }
+ }
+ return UserHandle(selectedUserInteractor.getSelectedUserId())
+ }
+
+ private fun isKeyguardShowing(): Boolean {
+ return !deviceEntryInteractor.isDeviceEntered.value
+ }
+
+ private fun isKeyguardOccluded(): Boolean {
+ return keyguardTransitionInteractor.getCurrentState() == KeyguardState.OCCLUDED
+ }
+
+ /**
+ * Return a [ActivityTransitionAnimator.Controller] wrapping `animationController` so that:
+ * - if it launches in the notification shade window and `dismissShade` is true, then the shade
+ * will be instantly dismissed at the end of the animation.
+ * - if it launches in status bar window, it will make the status bar window match the device
+ * size during the animation (that way, the animation won't be clipped by the status bar
+ * size).
+ *
+ * @param animationController the controller that is wrapped and will drive the main animation.
+ * @param dismissShade whether the notification shade will be dismissed at the end of the
+ * animation. This is ignored if `animationController` is not animating in the shade window.
+ * @param isLaunchForActivity whether the launch is for an activity.
+ */
+ private fun wrapAnimationControllerForShadeOrStatusBar(
+ animationController: ActivityTransitionAnimator.Controller?,
+ dismissShade: Boolean,
+ isLaunchForActivity: Boolean,
+ ): ActivityTransitionAnimator.Controller? {
+ if (animationController == null) {
+ return null
+ }
+ val rootView = animationController.transitionContainer.rootView
+ val controllerFromStatusBar: Optional<ActivityTransitionAnimator.Controller> =
+ statusBarWindowController.wrapAnimationControllerIfInStatusBar(
+ rootView,
+ animationController
+ )
+ if (controllerFromStatusBar.isPresent) {
+ return controllerFromStatusBar.get()
+ }
+
+ centralSurfaces?.let {
+ // If the view is not in the status bar, then we are animating a view in the shade.
+ // We have to make sure that we collapse it when the animation ends or is cancelled.
+ if (dismissShade) {
+ return StatusBarTransitionAnimatorController(
+ animationController,
+ shadeAnimationInteractor,
+ shadeControllerLazy.get(),
+ notifShadeWindowControllerLazy.get(),
+ commandQueue,
+ displayId,
+ isLaunchForActivity
+ )
+ }
+ }
+
+ return animationController
+ }
+
+ /**
+ * Wraps an animation controller so that if an activity would be launched on top of the
+ * lockscreen, the correct flags are set for it to be occluded.
+ */
+ private fun wrapAnimationControllerForLockscreen(
+ dismissShade: Boolean,
+ animationController: ActivityTransitionAnimator.Controller?
+ ): ActivityTransitionAnimator.Controller? {
+ return animationController?.let {
+ object : DelegateTransitionAnimatorController(it) {
+ override fun onIntentStarted(willAnimate: Boolean) {
+ delegate.onIntentStarted(willAnimate)
+ if (willAnimate) {
+ centralSurfaces?.setIsLaunchingActivityOverLockscreen(true, dismissShade)
+ }
+ }
+
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
+ super.onTransitionAnimationStart(isExpandingFullyAbove)
+ if (Flags.communalHub()) {
+ communalSceneInteractor.snapToScene(
+ CommunalScenes.Blank,
+ ActivityTransitionAnimator.TIMINGS.totalDuration
+ )
+ }
+ }
+
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+ // Set mIsLaunchingActivityOverLockscreen to false before actually
+ // finishing the animation so that we can assume that
+ // mIsLaunchingActivityOverLockscreen being true means that we will
+ // collapse the shade (or at least run the post collapse runnables)
+ // later on.
+ centralSurfaces?.setIsLaunchingActivityOverLockscreen(false, false)
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
+ }
+
+ override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) {
+ // Set mIsLaunchingActivityOverLockscreen to false before actually
+ // finishing the animation so that we can assume that
+ // mIsLaunchingActivityOverLockscreen being true means that we will
+ // collapse the shade (or at least run the // post collapse
+ // runnables) later on.
+ centralSurfaces?.setIsLaunchingActivityOverLockscreen(false, false)
+ delegate.onTransitionAnimationCancelled(newKeyguardOccludedState)
+ }
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "ActivityStarterInternalImpl"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
index 4c97854..16bd7f8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
@@ -110,7 +110,7 @@
chipView.setOnClickListener(chipModel.onClickListener)
// Accessibility
- setChipAccessibility(chipModel, chipView)
+ setChipAccessibility(chipModel, chipView, chipBackgroundView)
// Colors
val textColor = chipModel.colors.text(chipContext)
@@ -213,7 +213,11 @@
this.setPaddingRelative(/* start= */ 0, paddingTop, paddingEnd, paddingBottom)
}
- private fun setChipAccessibility(chipModel: OngoingActivityChipModel.Shown, chipView: View) {
+ private fun setChipAccessibility(
+ chipModel: OngoingActivityChipModel.Shown,
+ chipView: View,
+ chipBackgroundView: View,
+ ) {
when (chipModel) {
is OngoingActivityChipModel.Shown.Countdown -> {
// Set as assertive so talkback will announce the countdown
@@ -224,6 +228,16 @@
chipView.accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_NONE
}
}
+ // Clickable chips need to be a minimum size for accessibility purposes, but let
+ // non-clickable chips be smaller.
+ if (chipModel.onClickListener != null) {
+ chipBackgroundView.minimumWidth =
+ chipBackgroundView.context.resources.getDimensionPixelSize(
+ R.dimen.min_clickable_item_size
+ )
+ } else {
+ chipBackgroundView.minimumWidth = 0
+ }
}
private fun animateLightsOutView(view: View, visible: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
index 645a361..6aaa948 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
@@ -382,7 +382,8 @@
"\nPREVIOUS: [$previousHunKey]" +
"\nNEXT LIST: $nextListStr" +
"\nNEXT MAP: $nextMapStr" +
- "\nDROPPED: $dropSetStr"
+ "\nDROPPED: $dropSetStr" +
+ "\nENABLED: $enableAtRuntime"
}
private fun logState(reason: String) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
index 4838554..07bbca7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
@@ -31,6 +31,13 @@
* @see android.provider.Settings.Global.DEVICE_PROVISIONED
*/
val isDeviceProvisioned: Flow<Boolean>
+
+ /**
+ * Whether this device has been provisioned.
+ *
+ * @see android.provider.Settings.Global.DEVICE_PROVISIONED
+ */
+ fun isDeviceProvisioned(): Boolean
}
@Module
@@ -48,11 +55,15 @@
val listener =
object : DeviceProvisionedController.DeviceProvisionedListener {
override fun onDeviceProvisionedChanged() {
- trySend(deviceProvisionedController.isDeviceProvisioned)
+ trySend(isDeviceProvisioned())
}
}
deviceProvisionedController.addCallback(listener)
- trySend(deviceProvisionedController.isDeviceProvisioned)
+ trySend(isDeviceProvisioned())
awaitClose { deviceProvisionedController.removeCallback(listener) }
}
+
+ override fun isDeviceProvisioned(): Boolean {
+ return deviceProvisionedController.isDeviceProvisioned
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
index 66ed092..ace4ce0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
@@ -26,7 +26,7 @@
class DeviceProvisioningInteractor
@Inject
constructor(
- repository: DeviceProvisioningRepository,
+ private val repository: DeviceProvisioningRepository,
) {
/**
* Whether this device has been provisioned.
@@ -34,4 +34,8 @@
* @see android.provider.Settings.Global.DEVICE_PROVISIONED
*/
val isDeviceProvisioned: Flow<Boolean> = repository.isDeviceProvisioned
+
+ fun isDeviceProvisioned(): Boolean {
+ return repository.isDeviceProvisioned()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
index 639b53b..5600b87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
@@ -710,6 +710,35 @@
environment.verifyLifecycleObserversUnregistered();
}
+ @Test
+ public void testLastSessionPop_createsNewInputSession() {
+ final TouchHandler touchHandler = createTouchHandler();
+
+ final TouchHandler.TouchSession.Callback callback =
+ Mockito.mock(TouchHandler.TouchSession.Callback.class);
+
+ final Environment environment = new Environment(Stream.of(touchHandler)
+ .collect(Collectors.toCollection(HashSet::new)), mKosmos);
+
+ final InputEvent initialEvent = Mockito.mock(InputEvent.class);
+ environment.publishInputEvent(initialEvent);
+
+ final TouchHandler.TouchSession session = captureSession(touchHandler);
+ session.registerCallback(callback);
+
+ // Clear invocations on input session and factory.
+ clearInvocations(environment.mInputFactory);
+ clearInvocations(environment.mInputSession);
+
+ // Pop only active touch session.
+ session.pop();
+ environment.executeAll();
+
+ // Verify that input session disposed and new session requested from factory.
+ verify(environment.mInputSession).dispose();
+ verify(environment.mInputFactory).create(any(), any(), any(), anyBoolean());
+ }
+
private GestureDetector.OnGestureListener registerGestureListener(TouchHandler handler) {
final GestureDetector.OnGestureListener gestureListener = Mockito.mock(
GestureDetector.OnGestureListener.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
index d940fc9..7a4bbfe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
@@ -36,7 +36,6 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.flags.FakeFeatureFlags;
-import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.StatusBarLocation;
import com.android.systemui.statusbar.policy.BatteryController;
@@ -77,9 +76,6 @@
when(mBatteryMeterView.getContext()).thenReturn(mContext);
when(mBatteryMeterView.getResources()).thenReturn(mContext.getResources());
-
- mContext.getOrCreateTestableResources().addOverride(
- R.bool.flag_battery_shield_icon, false);
}
@Test
@@ -136,26 +132,6 @@
verify(mTunerService, never()).addTunable(any(), any());
}
- @Test
- public void shieldFlagDisabled_viewNotified() {
- mContext.getOrCreateTestableResources().addOverride(
- R.bool.flag_battery_shield_icon, false);
-
- initController();
-
- verify(mBatteryMeterView).setDisplayShieldEnabled(false);
- }
-
- @Test
- public void shieldFlagEnabled_viewNotified() {
- mContext.getOrCreateTestableResources().addOverride(
- R.bool.flag_battery_shield_icon, true);
-
- initController();
-
- verify(mBatteryMeterView).setDisplayShieldEnabled(true);
- }
-
private void initController() {
mController = new BatteryMeterViewController(
mBatteryMeterView,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
index 2bd0976..2aa33a17 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
@@ -22,9 +22,9 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.flags.Flags.FLAG_NEW_STATUS_BAR_ICONS
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.battery.BatteryMeterView.BatteryEstimateFetcher
+import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.BatteryController.EstimateFetchCompletion
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -67,9 +67,8 @@
fun contentDescription_unknown() {
mBatteryMeterView.onBatteryUnknownStateChanged(true)
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
- context.getString(R.string.accessibility_battery_unknown)
- )
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(context.getString(R.string.accessibility_battery_unknown))
}
@Test
@@ -80,11 +79,10 @@
mBatteryMeterView.updatePercentText()
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
- context.getString(
- R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
- )
- )
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(
+ context.getString(R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE)
+ )
}
@Test
@@ -96,13 +94,14 @@
mBatteryMeterView.updatePercentText()
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(
context.getString(
- R.string.accessibility_battery_level_charging_paused_with_estimate,
- 17,
- ESTIMATE,
+ R.string.accessibility_battery_level_charging_paused_with_estimate,
+ 17,
+ ESTIMATE,
)
- )
+ )
}
@Test
@@ -110,27 +109,24 @@
mBatteryMeterView.onBatteryLevelChanged(90, false)
mBatteryMeterView.onIsBatteryDefenderChanged(true)
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
- context.getString(R.string.accessibility_battery_level_charging_paused, 90)
- )
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(context.getString(R.string.accessibility_battery_level_charging_paused, 90))
}
@Test
fun contentDescription_charging() {
mBatteryMeterView.onBatteryLevelChanged(45, true)
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
- context.getString(R.string.accessibility_battery_level_charging, 45)
- )
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(context.getString(R.string.accessibility_battery_level_charging, 45))
}
@Test
fun contentDescription_notCharging() {
mBatteryMeterView.onBatteryLevelChanged(45, false)
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
- context.getString(R.string.accessibility_battery_level, 45)
- )
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(context.getString(R.string.accessibility_battery_level, 45))
}
@Test
@@ -138,9 +134,8 @@
mBatteryMeterView.onBatteryLevelChanged(45, true)
mBatteryMeterView.onIsIncompatibleChargingChanged(true)
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
- context.getString(R.string.accessibility_battery_level, 45)
- )
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(context.getString(R.string.accessibility_battery_level, 45))
}
@Test
@@ -152,19 +147,17 @@
mBatteryMeterView.updatePercentText()
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
- context.getString(
- R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
- )
- )
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(
+ context.getString(R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE)
+ )
// Update the show mode from estimate to percent
mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
assertThat(mBatteryMeterView.batteryPercentViewText).isEqualTo("15%")
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
- context.getString(R.string.accessibility_battery_level, 15)
- )
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(context.getString(R.string.accessibility_battery_level, 15))
}
@Test
@@ -176,19 +169,17 @@
mBatteryMeterView.updatePercentText()
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
- context.getString(
- R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(
+ context.getString(R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE)
)
- )
// Update the show mode from estimate to percent
mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
assertThat(mBatteryMeterView.batteryPercentView).isNull()
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
- context.getString(R.string.accessibility_battery_level, 15)
- )
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(context.getString(R.string.accessibility_battery_level, 15))
assertThat(mBatteryMeterView.unifiedBatteryState.showPercent).isTrue()
}
@@ -225,49 +216,47 @@
// BatteryDefender
mBatteryMeterView.onBatteryLevelChanged(90, false)
mBatteryMeterView.onIsBatteryDefenderChanged(true)
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
- context.getString(R.string.accessibility_battery_level_charging_paused, 90)
- )
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(context.getString(R.string.accessibility_battery_level_charging_paused, 90))
// BatteryDefender & estimate
mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
mBatteryMeterView.updatePercentText()
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(
context.getString(
- R.string.accessibility_battery_level_charging_paused_with_estimate,
- 90,
- ESTIMATE,
+ R.string.accessibility_battery_level_charging_paused_with_estimate,
+ 90,
+ ESTIMATE,
)
- )
+ )
// Just estimate
mBatteryMeterView.onIsBatteryDefenderChanged(false)
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(
context.getString(
- R.string.accessibility_battery_level_with_estimate,
- 90,
- ESTIMATE,
+ R.string.accessibility_battery_level_with_estimate,
+ 90,
+ ESTIMATE,
)
- )
+ )
// Just percent
mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
- context.getString(R.string.accessibility_battery_level, 90)
- )
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(context.getString(R.string.accessibility_battery_level, 90))
// Charging
mBatteryMeterView.onBatteryLevelChanged(90, true)
- assertThat(mBatteryMeterView.contentDescription).isEqualTo(
- context.getString(R.string.accessibility_battery_level_charging, 90)
- )
+ assertThat(mBatteryMeterView.contentDescription)
+ .isEqualTo(context.getString(R.string.accessibility_battery_level_charging, 90))
}
@Test
@DisableFlags(FLAG_NEW_STATUS_BAR_ICONS)
fun isBatteryDefenderChanged_true_drawableGetsTrue_flagOff() {
- mBatteryMeterView.setDisplayShieldEnabled(true)
val drawable = getBatteryDrawable()
mBatteryMeterView.onIsBatteryDefenderChanged(true)
@@ -278,8 +267,6 @@
@Test
@EnableFlags(FLAG_NEW_STATUS_BAR_ICONS)
fun isBatteryDefenderChanged_true_drawableGetsTrue_flagOn() {
- mBatteryMeterView.setDisplayShieldEnabled(true)
-
mBatteryMeterView.onIsBatteryDefenderChanged(true)
assertThat(mBatteryMeterView.unifiedBatteryState.attribution).isNotNull()
@@ -288,7 +275,6 @@
@Test
@DisableFlags(FLAG_NEW_STATUS_BAR_ICONS)
fun isBatteryDefenderChanged_false_drawableGetsFalse_flagOff() {
- mBatteryMeterView.setDisplayShieldEnabled(true)
val drawable = getBatteryDrawable()
// Start as true
@@ -303,8 +289,6 @@
@Test
@EnableFlags(FLAG_NEW_STATUS_BAR_ICONS)
fun isBatteryDefenderChanged_false_drawableGetsFalse_flagOn() {
- mBatteryMeterView.setDisplayShieldEnabled(true)
-
// Start as true
mBatteryMeterView.onIsBatteryDefenderChanged(true)
@@ -316,27 +300,6 @@
@Test
@DisableFlags(FLAG_NEW_STATUS_BAR_ICONS)
- fun isBatteryDefenderChanged_true_featureflagOff_drawableGetsFalse_flagOff() {
- mBatteryMeterView.setDisplayShieldEnabled(false)
- val drawable = getBatteryDrawable()
-
- mBatteryMeterView.onIsBatteryDefenderChanged(true)
-
- assertThat(drawable.displayShield).isFalse()
- }
-
- @Test
- @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS)
- fun isBatteryDefenderChanged_true_featureflagOff_drawableGetsFalse_flagOn() {
- mBatteryMeterView.setDisplayShieldEnabled(false)
-
- mBatteryMeterView.onIsBatteryDefenderChanged(true)
-
- assertThat(mBatteryMeterView.unifiedBatteryState.attribution).isNull()
- }
-
- @Test
- @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS)
fun isIncompatibleChargingChanged_true_drawableGetsChargingFalse_flagOff() {
mBatteryMeterView.onBatteryLevelChanged(45, true)
val drawable = getBatteryDrawable()
@@ -381,8 +344,8 @@
}
private fun getBatteryDrawable(): AccessorizedBatteryDrawable {
- return (mBatteryMeterView.getChildAt(0) as ImageView)
- .drawable as AccessorizedBatteryDrawable
+ return (mBatteryMeterView.getChildAt(0) as ImageView).drawable
+ as AccessorizedBatteryDrawable
}
private class Fetcher : BatteryEstimateFetcher {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index fc3b35d..a3959d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -1574,30 +1574,6 @@
}
@Test
- @BrokenWithSceneContainer(339465026)
- fun aodToOccluded() =
- testScope.runTest {
- // GIVEN a prior transition has run to AOD
- runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.AOD)
- runCurrent()
-
- // WHEN the keyguard is occluded
- keyguardRepository.setKeyguardOccluded(true)
- runCurrent()
-
- // THEN a transition to OCCLUDED should occur
- assertThat(transitionRepository)
- .startedTransition(
- ownerName = "FromAodTransitionInteractor(isOccluded = true)",
- from = KeyguardState.AOD,
- to = KeyguardState.OCCLUDED,
- animatorAssertion = { it.isNotNull() },
- )
-
- coroutineContext.cancelChildren()
- }
-
- @Test
@DisableSceneContainer
fun aodToPrimaryBouncer() =
testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt
index 3b0e194..bab9bbb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt
@@ -16,13 +16,13 @@
package com.android.systemui.screenshot.policy
import android.content.ComponentName
-import androidx.test.ext.junit.runners.AndroidJUnit4
import android.graphics.Insets
import android.graphics.Rect
import android.os.UserHandle
import android.view.Display.DEFAULT_DISPLAY
import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
+import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.systemui.screenshot.ImageCapture
import com.android.systemui.screenshot.ScreenshotData
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.FILES
@@ -40,20 +40,23 @@
@RunWith(AndroidJUnit4::class)
class PolicyRequestProcessorTest {
- val imageCapture = object : ImageCapture {
- override fun captureDisplay(displayId: Int, crop: Rect?) = null
- override suspend fun captureTask(taskId: Int) = null
- }
+ val imageCapture =
+ object : ImageCapture {
+ override fun captureDisplay(displayId: Int, crop: Rect?) = null
+
+ override suspend fun captureTask(taskId: Int) = null
+ }
/** Tests behavior when no policies are applied */
@Test
fun testProcess_defaultOwner_whenNoPolicyApplied() {
val fullScreenWork = DisplayContentRepository {
- singleFullScreen(TaskSpec(taskId = 1001, name = FILES, userId = WORK))
+ singleFullScreen(TaskSpec(taskId = TASK_ID, name = FILES, userId = WORK))
}
val request =
- ScreenshotData(TAKE_SCREENSHOT_FULLSCREEN,
+ ScreenshotData(
+ TAKE_SCREENSHOT_FULLSCREEN,
SCREENSHOT_KEY_CHORD,
null,
topComponent = null,
@@ -61,24 +64,34 @@
taskId = -1,
insets = Insets.NONE,
bitmap = null,
- displayId = DEFAULT_DISPLAY)
+ displayId = DEFAULT_DISPLAY
+ )
/* Create a policy request processor with no capture policies */
val requestProcessor =
- PolicyRequestProcessor(Dispatchers.Unconfined,
+ PolicyRequestProcessor(
+ Dispatchers.Unconfined,
imageCapture,
policies = emptyList(),
defaultOwner = UserHandle.of(PERSONAL),
defaultComponent = ComponentName("default", "Component"),
- displayTasks = fullScreenWork)
+ displayTasks = fullScreenWork
+ )
val result = runBlocking { requestProcessor.process(request) }
- assertWithMessage(
- "With no policy, the screenshot should be assigned to the default user"
- ).that(result.userHandle).isEqualTo(UserHandle.of(PERSONAL))
+ assertWithMessage("With no policy, the screenshot should be assigned to the default user")
+ .that(result.userHandle)
+ .isEqualTo(UserHandle.of(PERSONAL))
- assertWithMessage("The topComponent of the screenshot").that(result.topComponent)
- .isEqualTo(ComponentName.unflattenFromString(FILES))
+ assertWithMessage("The topComponent of the screenshot")
+ .that(result.topComponent)
+ .isEqualTo(ComponentName.unflattenFromString(FILES))
+
+ assertWithMessage("Task ID").that(result.taskId).isEqualTo(TASK_ID)
+ }
+
+ companion object {
+ const val TASK_ID = 1001
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index 169511f..86c9ab7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -312,6 +312,59 @@
}
@Test
+ fun lifecycle_doesNotResumeOnUserInteractivityOnceExpanded() =
+ with(kosmos) {
+ testScope.runTest {
+ // Communal is open.
+ goToScene(CommunalScenes.Communal)
+
+ // Shade shows up.
+ shadeTestUtil.setShadeExpansion(1.0f)
+ testableLooper.processAllMessages()
+ underTest.onTouchEvent(DOWN_EVENT)
+ testableLooper.processAllMessages()
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+ // Shade starts collapsing.
+ shadeTestUtil.setShadeExpansion(.5f)
+ testableLooper.processAllMessages()
+ underTest.onTouchEvent(DOWN_EVENT)
+ testableLooper.processAllMessages()
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+ // Shade fully collpase, and then expand should with touch interaction should now
+ // be resumed.
+ shadeTestUtil.setShadeExpansion(0f)
+ testableLooper.processAllMessages()
+ shadeTestUtil.setShadeExpansion(.5f)
+ testableLooper.processAllMessages()
+ underTest.onTouchEvent(DOWN_EVENT)
+ testableLooper.processAllMessages()
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ }
+ }
+
+ @Test
+ fun touchHandling_moveEventProcessedAfterCancel() =
+ with(kosmos) {
+ testScope.runTest {
+ // Communal is open.
+ goToScene(CommunalScenes.Communal)
+
+ // Shade shows up.
+ shadeTestUtil.setQsExpansion(0.5f)
+ testableLooper.processAllMessages()
+ assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
+ assertThat(underTest.onTouchEvent(CANCEL_EVENT)).isTrue()
+ assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse()
+ assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue()
+ }
+ }
+
+ @Test
fun editMode_communalAvailable() =
with(kosmos) {
testScope.runTest {
@@ -488,5 +541,35 @@
CONTAINER_HEIGHT.toFloat() / 2,
0
)
+
+ private val CANCEL_EVENT =
+ MotionEvent.obtain(
+ 0L,
+ 0L,
+ MotionEvent.ACTION_CANCEL,
+ CONTAINER_WIDTH.toFloat() / 2,
+ CONTAINER_HEIGHT.toFloat() / 2,
+ 0
+ )
+
+ private val MOVE_EVENT =
+ MotionEvent.obtain(
+ 0L,
+ 0L,
+ MotionEvent.ACTION_MOVE,
+ CONTAINER_WIDTH.toFloat() / 2,
+ CONTAINER_HEIGHT.toFloat() / 2,
+ 0
+ )
+
+ private val UP_EVENT =
+ MotionEvent.obtain(
+ 0L,
+ 0L,
+ MotionEvent.ACTION_UP,
+ CONTAINER_WIDTH.toFloat() / 2,
+ CONTAINER_HEIGHT.toFloat() / 2,
+ 0
+ )
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 8a6b68f..a5c4bcd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -675,8 +675,14 @@
mMainHandler = new Handler(Looper.getMainLooper());
+ LongPressHandlingView longPressHandlingView = mock(LongPressHandlingView.class);
when(mView.requireViewById(R.id.keyguard_long_press))
- .thenReturn(mock(LongPressHandlingView.class));
+ .thenReturn(longPressHandlingView);
+
+ Resources longPressHandlingViewRes = mock(Resources.class);
+ when(longPressHandlingView.getResources()).thenReturn(longPressHandlingViewRes);
+ when(longPressHandlingViewRes.getString(anyInt())).thenReturn("");
+
mHeadsUpNotificationInteractor =
new HeadsUpNotificationInteractor(mFakeHeadsUpNotificationRepository,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
index 52af907..64eadb7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
@@ -36,7 +36,6 @@
import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
index acb005f..0407fc1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
@@ -42,7 +42,7 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
// this class has no testable logic with either of these flags enabled
-@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.V2.FLAG_NAME)
+@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.FLAG_NAME)
class NotificationSectionsFeatureManagerTest : SysuiTestCase() {
lateinit var manager: NotificationSectionsFeatureManager
private val proxyFake = DeviceConfigProxyFake()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index cc2ef53..12cfdcf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -35,7 +35,6 @@
import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.batteryController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt
index f40282f..a8271fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt
@@ -21,10 +21,9 @@
import android.view.MotionEvent
import android.view.MotionEvent.CLASSIFICATION_NONE
import android.view.MotionEvent.TOOL_TYPE_FINGER
-import java.lang.reflect.Method
-import org.mockito.kotlin.doNothing
+import org.mockito.AdditionalAnswers
+import org.mockito.Mockito.mock
import org.mockito.kotlin.doReturn
-import org.mockito.kotlin.spy
import org.mockito.kotlin.whenever
fun motionEvent(
@@ -40,31 +39,18 @@
val event =
MotionEvent.obtain(/* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0)
event.source = source
- val spy =
- spy<MotionEvent>(event) {
- on { getToolType(0) } doReturn toolType
- on { getPointerCount() } doReturn pointerCount
- axisValues.forEach { (key, value) -> on { getAxisValue(key) } doReturn value }
- on { getClassification() } doReturn classification
- }
- ensureFinalizeIsNotCalledTwice(spy)
- return spy
-}
-
-private fun ensureFinalizeIsNotCalledTwice(spy: MotionEvent) {
- // Spy in mockito will create copy of the spied object, copying all its field etc. Here it means
- // we create copy of MotionEvent and its mNativePtr, so we have two separate objects of type
- // MotionEvents with the same mNativePtr. That breaks because MotionEvent has custom finalize()
- // method which goes to native code and tries to delete the reference from mNativePtr. It works
- // first time but second time reference is already deleted and it breaks. That's why we have to
- // avoid calling finalize twice
- doNothing().whenever(spy).finalizeUsingReflection()
-}
-
-private fun MotionEvent.finalizeUsingReflection() {
- val finalizeMethod: Method = MotionEvent::class.java.getDeclaredMethod("finalize")
- finalizeMethod.isAccessible = true
- finalizeMethod.invoke(this)
+ // we need to use mock with delegation instead of spy because:
+ // 1. Spy will try to deallocate the same memory again when finalize() is called as it keep the
+ // same memory pointer to native MotionEvent
+ // 2. Even after workaround for issue above there still remains problem with destructor of
+ // native event trying to free the same chunk of native memory. I'm not sure why it happens but
+ // mock seems to fix the issue and because it delegates all calls seems safer overall
+ val delegate = mock(MotionEvent::class.java, AdditionalAnswers.delegatesTo<MotionEvent>(event))
+ doReturn(toolType).whenever(delegate).getToolType(0)
+ doReturn(pointerCount).whenever(delegate).pointerCount
+ doReturn(classification).whenever(delegate).classification
+ axisValues.forEach { (key, value) -> doReturn(value).whenever(delegate).getAxisValue(key) }
+ return delegate
}
fun touchpadEvent(
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 f73f43d..edf4bcc 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
@@ -17,7 +17,10 @@
package com.android.systemui.education.data.repository
import com.android.systemui.kosmos.Kosmos
+import java.time.Clock
import java.time.Instant
var Kosmos.contextualEducationRepository: ContextualEducationRepository by
- Kosmos.Fixture { FakeContextualEducationRepository(FakeEduClock(Instant.MIN)) }
+ Kosmos.Fixture { FakeContextualEducationRepository(fakeEduClock) }
+
+var Kosmos.fakeEduClock: Clock by Kosmos.Fixture { FakeEduClock(Instant.MIN) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt
new file mode 100644
index 0000000..5b2dc2b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 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.education.domain.interactor
+
+import com.android.systemui.education.data.repository.contextualEducationRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+
+val Kosmos.contextualEducationInteractor by
+ Kosmos.Fixture {
+ ContextualEducationInteractor(
+ backgroundScope = testScope.backgroundScope,
+ repository = contextualEducationRepository,
+ selectedUserInteractor = selectedUserInteractor
+ )
+ }
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
new file mode 100644
index 0000000..8f84e04
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 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.education.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+
+var Kosmos.keyboardTouchpadEduStatsInteractor by
+ Kosmos.Fixture {
+ KeyboardTouchpadEduStatsInteractorImpl(
+ backgroundScope = testScope.backgroundScope,
+ contextualEducationInteractor = contextualEducationInteractor
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
index da956ec..8b4de2b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
@@ -22,13 +22,12 @@
* The [modifier] function will be passed an instance of a NotificationEntryBuilder. Any
* modifications made to the builder will be applied to the [entry].
*/
-inline fun modifyEntry(
- entry: NotificationEntry,
+inline fun NotificationEntry.modifyEntry(
crossinline modifier: NotificationEntryBuilder.() -> Unit
) {
- val builder = NotificationEntryBuilder(entry)
+ val builder = NotificationEntryBuilder(this)
modifier(builder)
- builder.apply(entry)
+ builder.apply(this)
}
fun getAttachState(entry: ListEntry): ListAttachState {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt
new file mode 100644
index 0000000..77d97bb
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
+import com.android.systemui.util.settings.fakeSettings
+
+var Kosmos.lockScreenMinimalismCoordinator by
+ Kosmos.Fixture {
+ LockScreenMinimalismCoordinator(
+ bgDispatcher = testDispatcher,
+ dumpManager = dumpManager,
+ headsUpInteractor = headsUpNotificationInteractor,
+ logger = lockScreenMinimalismCoordinatorLogger,
+ scope = testScope.backgroundScope,
+ secureSettings = fakeSettings,
+ seenNotificationsInteractor = seenNotificationsInteractor,
+ statusBarStateController = statusBarStateController,
+ shadeInteractor = shadeInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt
new file mode 100644
index 0000000..77aeb44
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+
+val Kosmos.lockScreenMinimalismCoordinatorLogger by
+ Kosmos.Fixture { LockScreenMinimalismCoordinatorLogger(logcatLogBuffer()) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
index 492e87b..7e8f1a9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
@@ -22,14 +22,19 @@
import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
val Kosmos.headsUpNotificationRepository by Fixture { FakeHeadsUpNotificationRepository() }
class FakeHeadsUpNotificationRepository : HeadsUpRepository {
override val isHeadsUpAnimatingAway: MutableStateFlow<Boolean> = MutableStateFlow(false)
- override val topHeadsUpRow: Flow<HeadsUpRowRepository?> = MutableStateFlow(null)
- override val activeHeadsUpRows: MutableStateFlow<Set<HeadsUpRowRepository>> =
- MutableStateFlow(emptySet())
+
+ val orderedHeadsUpRows = MutableStateFlow(emptyList<HeadsUpRowRepository>())
+ override val topHeadsUpRow: Flow<HeadsUpRowRepository?> =
+ orderedHeadsUpRows.map { it.firstOrNull() }.distinctUntilChanged()
+ override val activeHeadsUpRows: Flow<Set<HeadsUpRowRepository>> =
+ orderedHeadsUpRows.map { it.toSet() }.distinctUntilChanged()
override fun setHeadsUpAnimatingAway(animatingAway: Boolean) {
isHeadsUpAnimatingAway.value = animatingAway
@@ -38,4 +43,12 @@
override fun snooze() {
// do nothing
}
+
+ fun setNotifications(notifications: List<HeadsUpRowRepository>) {
+ this.orderedHeadsUpRows.value = notifications.toList()
+ }
+
+ fun setNotifications(vararg notifications: HeadsUpRowRepository) {
+ this.orderedHeadsUpRows.value = notifications.toList()
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt
deleted file mode 100644
index 9be7dfe..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.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.statusbar.notification.stack.data.repository
-
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
-
-fun FakeHeadsUpNotificationRepository.setNotifications(notifications: List<HeadsUpRowRepository>) {
- setNotifications(*notifications.toTypedArray())
-}
-
-fun FakeHeadsUpNotificationRepository.setNotifications(vararg notifications: HeadsUpRowRepository) {
- this.activeHeadsUpRows.value = notifications.toSet()
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
index 9247e88..e3176f1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
@@ -26,9 +26,14 @@
class FakeDeviceProvisioningRepository @Inject constructor() : DeviceProvisioningRepository {
private val _isDeviceProvisioned = MutableStateFlow(true)
override val isDeviceProvisioned: Flow<Boolean> = _isDeviceProvisioned
+
fun setDeviceProvisioned(isProvisioned: Boolean) {
_isDeviceProvisioned.value = isProvisioned
}
+
+ override fun isDeviceProvisioned(): Boolean {
+ return _isDeviceProvisioned.value
+ }
}
@Module
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 7a055d1..76b4263 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -1192,6 +1192,18 @@
*/
public float getHdrBrightnessFromSdr(float brightness, float maxDesiredHdrSdrRatio) {
Spline sdrToHdrSpline = mHbmData != null ? mHbmData.sdrToHdrRatioSpline : null;
+ return getHdrBrightnessFromSdr(brightness, maxDesiredHdrSdrRatio, sdrToHdrSpline);
+ }
+
+ /**
+ * Calculate the HDR brightness for the specified SDR brightenss, restricted by the
+ * maxDesiredHdrSdrRatio (the ratio between the HDR luminance and SDR luminance) and specific
+ * sdrToHdrSpline
+ *
+ * @return the HDR brightness or BRIGHTNESS_INVALID when no mapping exists.
+ */
+ public float getHdrBrightnessFromSdr(float brightness, float maxDesiredHdrSdrRatio,
+ @Nullable Spline sdrToHdrSpline) {
if (sdrToHdrSpline == null) {
return PowerManager.BRIGHTNESS_INVALID;
}
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 6992580..1177be2 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -505,14 +505,15 @@
mClock = mInjector.getClock();
mLogicalDisplay = logicalDisplay;
mDisplayId = mLogicalDisplay.getDisplayIdLocked();
+ mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
+ IBinder displayToken = mDisplayDevice.getDisplayTokenLocked();
+ DisplayDeviceInfo displayDeviceInfo = mDisplayDevice.getDisplayDeviceInfoLocked();
mSensorManager = sensorManager;
mHandler = new DisplayControllerHandler(handler.getLooper());
- mDisplayDeviceConfig = logicalDisplay.getPrimaryDisplayDeviceLocked()
- .getDisplayDeviceConfig();
+ mDisplayDeviceConfig = mDisplayDevice.getDisplayDeviceConfig();
mIsEnabled = logicalDisplay.isEnabledLocked();
mIsInTransition = logicalDisplay.isInTransitionLocked();
- mIsDisplayInternal = logicalDisplay.getPrimaryDisplayDeviceLocked()
- .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL;
+ mIsDisplayInternal = displayDeviceInfo.type == Display.TYPE_INTERNAL;
mWakelockController = mInjector.getWakelockController(mDisplayId, callbacks);
mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController(
mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(),
@@ -521,7 +522,7 @@
mTag = TAG + "[" + mDisplayId + "]";
mThermalBrightnessThrottlingDataId =
logicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId;
- mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
+
mUniqueDisplayId = mDisplayDevice.getUniqueId();
mDisplayStatsId = mUniqueDisplayId.hashCode();
mPhysicalDisplayName = mDisplayDevice.getNameLocked();
@@ -569,8 +570,7 @@
mBrightnessRangeController = mInjector.getBrightnessRangeController(hbmController,
modeChangeCallback, mDisplayDeviceConfig, mHandler, flags,
- mDisplayDevice.getDisplayTokenLocked(),
- mDisplayDevice.getDisplayDeviceInfoLocked());
+ displayToken, displayDeviceInfo);
mDisplayBrightnessController =
new DisplayBrightnessController(context, null,
@@ -584,8 +584,8 @@
mUniqueDisplayId,
mThermalBrightnessThrottlingDataId,
logicalDisplay.getPowerThrottlingDataIdLocked(),
- mDisplayDeviceConfig,
- mDisplayId), mContext, flags, mSensorManager);
+ mDisplayDeviceConfig, displayDeviceInfo.width, displayDeviceInfo.height,
+ displayToken, mDisplayId), mContext, flags, mSensorManager);
// Seed the cached brightness
saveBrightnessInfo(getScreenBrightnessSetting());
mAutomaticBrightnessStrategy =
@@ -893,7 +893,7 @@
mBrightnessClamperController.onDisplayChanged(
new BrightnessClamperController.DisplayDeviceData(uniqueId,
thermalBrightnessThrottlingDataId, powerThrottlingDataId,
- config, mDisplayId));
+ config, info.width, info.height, token, mDisplayId));
if (changed) {
updatePowerState();
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 88d2c00..afab743 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -28,13 +28,16 @@
import android.hardware.display.DisplayManagerInternal;
import android.os.Handler;
import android.os.HandlerExecutor;
+import android.os.IBinder;
import android.os.PowerManager;
import android.provider.DeviceConfig;
import android.provider.DeviceConfigInterface;
import android.util.IndentingPrintWriter;
import android.util.Slog;
+import android.util.Spline;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.display.BrightnessSynchronizer;
import com.android.server.display.DisplayBrightnessState;
import com.android.server.display.DisplayDeviceConfig;
import com.android.server.display.DisplayDeviceConfig.PowerThrottlingConfigData;
@@ -65,6 +68,11 @@
private final List<BrightnessClamper<? super DisplayDeviceData>> mClampers;
private final List<BrightnessStateModifier> mModifiers;
+
+ private final List<DisplayDeviceDataListener> mDisplayDeviceDataListeners = new ArrayList<>();
+ private final List<StatefulModifier> mStatefulModifiers = new ArrayList<>();
+ private ModifiersAggregatedState mModifiersAggregatedState = new ModifiersAggregatedState();
+
private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener;
private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX;
@@ -110,7 +118,16 @@
mClampers = injector.getClampers(handler, clamperChangeListenerInternal, data, flags,
context);
mModifiers = injector.getModifiers(flags, context, handler, clamperChangeListener,
- data.mDisplayDeviceConfig);
+ data);
+
+ mModifiers.forEach(m -> {
+ if (m instanceof DisplayDeviceDataListener l) {
+ mDisplayDeviceDataListeners.add(l);
+ }
+ if (m instanceof StatefulModifier s) {
+ mStatefulModifiers.add(s);
+ }
+ });
mOnPropertiesChangedListener =
properties -> mClampers.forEach(BrightnessClamper::onDeviceConfigChanged);
mLightSensorController.configure(data.getAmbientLightSensor(), data.getDisplayId());
@@ -123,6 +140,7 @@
public void onDisplayChanged(DisplayDeviceData data) {
mLightSensorController.configure(data.getAmbientLightSensor(), data.getDisplayId());
mClampers.forEach(clamper -> clamper.onDisplayChanged(data));
+ mDisplayDeviceDataListeners.forEach(l -> l.onDisplayChanged(data));
adjustLightSensorSubscription();
}
@@ -234,14 +252,27 @@
customAnimationRate = minClamper.getCustomAnimationRate();
}
+ ModifiersAggregatedState newAggregatedState = new ModifiersAggregatedState();
+ mStatefulModifiers.forEach((clamper) -> clamper.applyStateChange(newAggregatedState));
+
if (mBrightnessCap != brightnessCap
|| mClamperType != clamperType
- || mCustomAnimationRate != customAnimationRate) {
+ || mCustomAnimationRate != customAnimationRate
+ || needToNotifyExternalListener(mModifiersAggregatedState, newAggregatedState)) {
mBrightnessCap = brightnessCap;
mClamperType = clamperType;
mCustomAnimationRate = customAnimationRate;
mClamperChangeListenerExternal.onChanged();
}
+ mModifiersAggregatedState = newAggregatedState;
+ }
+
+ private boolean needToNotifyExternalListener(ModifiersAggregatedState state1,
+ ModifiersAggregatedState state2) {
+ return !BrightnessSynchronizer.floatEquals(state1.mMaxDesiredHdrRatio,
+ state2.mMaxDesiredHdrRatio)
+ || state1.mSdrHdrRatioSpline != state2.mSdrHdrRatioSpline
+ || state1.mHdrHbmEnabled != state2.mHdrHbmEnabled;
}
private void start() {
@@ -295,17 +326,16 @@
List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context,
Handler handler, ClamperChangeListener listener,
- DisplayDeviceConfig displayDeviceConfig) {
+ DisplayDeviceData data) {
List<BrightnessStateModifier> modifiers = new ArrayList<>();
modifiers.add(new DisplayDimModifier(context));
modifiers.add(new BrightnessLowPowerModeModifier());
- if (flags.isEvenDimmerEnabled() && displayDeviceConfig != null
- && displayDeviceConfig.isEvenDimmerAvailable()) {
+ if (flags.isEvenDimmerEnabled() && data.mDisplayDeviceConfig.isEvenDimmerAvailable()) {
modifiers.add(new BrightnessLowLuxModifier(handler, listener, context,
- displayDeviceConfig));
+ data.mDisplayDeviceConfig));
}
if (flags.useNewHdrBrightnessModifier()) {
- modifiers.add(new HdrBrightnessModifier());
+ modifiers.add(new HdrBrightnessModifier(handler, listener, data));
}
return modifiers;
}
@@ -319,7 +349,14 @@
}
/**
- * Config Data for clampers
+ * Modifier should implement this interface in order to receive display change updates
+ */
+ interface DisplayDeviceDataListener {
+ void onDisplayChanged(DisplayDeviceData displayData);
+ }
+
+ /**
+ * Config Data for clampers/modifiers
*/
public static class DisplayDeviceData implements BrightnessThermalClamper.ThermalData,
BrightnessPowerClamper.PowerData,
@@ -331,23 +368,34 @@
@NonNull
private final String mPowerThrottlingDataId;
@NonNull
- private final DisplayDeviceConfig mDisplayDeviceConfig;
+ final DisplayDeviceConfig mDisplayDeviceConfig;
- private final int mDisplayId;
+ final int mWidth;
+
+ final int mHeight;
+
+ final IBinder mDisplayToken;
+
+ final int mDisplayId;
public DisplayDeviceData(@NonNull String uniqueDisplayId,
@NonNull String thermalThrottlingDataId,
@NonNull String powerThrottlingDataId,
@NonNull DisplayDeviceConfig displayDeviceConfig,
+ int width,
+ int height,
+ IBinder displayToken,
int displayId) {
mUniqueDisplayId = uniqueDisplayId;
mThermalThrottlingDataId = thermalThrottlingDataId;
mPowerThrottlingDataId = powerThrottlingDataId;
mDisplayDeviceConfig = displayDeviceConfig;
+ mWidth = width;
+ mHeight = height;
+ mDisplayToken = displayToken;
mDisplayId = displayId;
}
-
@NonNull
@Override
public String getUniqueDisplayId() {
@@ -406,4 +454,24 @@
return mDisplayId;
}
}
+
+ /**
+ * Stateful modifier should implement this interface and modify aggregatedState.
+ * AggregatedState is used by Controller to determine if updatePowerState call is needed
+ * to correctly adjust brightness
+ */
+ interface StatefulModifier {
+ void applyStateChange(ModifiersAggregatedState aggregatedState);
+ }
+
+ /**
+ * StatefulModifiers contribute to AggregatedState, that is used to decide if brightness
+ * adjustement is needed
+ */
+ public static class ModifiersAggregatedState {
+ float mMaxDesiredHdrRatio = HdrBrightnessModifier.DEFAULT_MAX_HDR_SDR_RATIO;
+ @Nullable
+ Spline mSdrHdrRatioSpline = null;
+ boolean mHdrHbmEnabled = false;
+ }
}
diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java
index a829866..2ee70fd 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java
@@ -16,17 +16,85 @@
package com.android.server.display.brightness.clamper;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.hardware.display.DisplayManagerInternal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.view.SurfaceControlHdrLayerInfoListener;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.display.BrightnessSynchronizer;
import com.android.server.display.DisplayBrightnessState;
+import com.android.server.display.DisplayDeviceConfig;
+import com.android.server.display.config.HdrBrightnessData;
import java.io.PrintWriter;
-public class HdrBrightnessModifier implements BrightnessStateModifier {
+public class HdrBrightnessModifier implements BrightnessStateModifier,
+ BrightnessClamperController.DisplayDeviceDataListener,
+ BrightnessClamperController.StatefulModifier {
+
+ static final float DEFAULT_MAX_HDR_SDR_RATIO = 1.0f;
+ private static final float DEFAULT_HDR_LAYER_SIZE = -1.0f;
+
+ private final SurfaceControlHdrLayerInfoListener mHdrListener =
+ new SurfaceControlHdrLayerInfoListener() {
+ @Override
+ public void onHdrInfoChanged(IBinder displayToken, int numberOfHdrLayers, int maxW,
+ int maxH, int flags, float maxDesiredHdrSdrRatio) {
+ boolean hdrLayerPresent = numberOfHdrLayers > 0;
+ mHandler.post(() -> HdrBrightnessModifier.this.onHdrInfoChanged(
+ hdrLayerPresent ? (float) (maxW * maxH) : DEFAULT_HDR_LAYER_SIZE,
+ hdrLayerPresent ? maxDesiredHdrSdrRatio : DEFAULT_MAX_HDR_SDR_RATIO));
+ }
+ };
+
+ private final Handler mHandler;
+ private final BrightnessClamperController.ClamperChangeListener mClamperChangeListener;
+ private final Injector mInjector;
+
+ private IBinder mRegisteredDisplayToken;
+
+ private float mScreenSize;
+ private float mHdrLayerSize = DEFAULT_HDR_LAYER_SIZE;
+ private HdrBrightnessData mHdrBrightnessData;
+ private DisplayDeviceConfig mDisplayDeviceConfig;
+ private float mMaxDesiredHdrRatio = DEFAULT_MAX_HDR_SDR_RATIO;
+ private Mode mMode = Mode.NO_HDR;
+
+ HdrBrightnessModifier(Handler handler,
+ BrightnessClamperController.ClamperChangeListener clamperChangeListener,
+ BrightnessClamperController.DisplayDeviceData displayData) {
+ this(handler, clamperChangeListener, new Injector(), displayData);
+ }
+
+ @VisibleForTesting
+ HdrBrightnessModifier(Handler handler,
+ BrightnessClamperController.ClamperChangeListener clamperChangeListener,
+ Injector injector,
+ BrightnessClamperController.DisplayDeviceData displayData) {
+ mHandler = handler;
+ mClamperChangeListener = clamperChangeListener;
+ mInjector = injector;
+ onDisplayChanged(displayData);
+ }
+
+ // Called in DisplayControllerHandler
@Override
public void apply(DisplayManagerInternal.DisplayPowerRequest request,
DisplayBrightnessState.Builder stateBuilder) {
- // noop
+ if (mHdrBrightnessData == null) { // no hdr data
+ return;
+ }
+ if (mMode == Mode.NO_HDR) {
+ return;
+ }
+
+ float hdrBrightness = mDisplayDeviceConfig.getHdrBrightnessFromSdr(
+ stateBuilder.getBrightness(), mMaxDesiredHdrRatio,
+ mHdrBrightnessData.sdrToHdrRatioSpline);
+ stateBuilder.setHdrBrightness(hdrBrightness);
}
@Override
@@ -34,11 +102,13 @@
// noop
}
+ // Called in DisplayControllerHandler
@Override
public void stop() {
- // noop
+ unregisterHdrListener();
}
+
@Override
public boolean shouldListenToLightSensor() {
return false;
@@ -48,4 +118,117 @@
public void setAmbientLux(float lux) {
// noop
}
+
+ @Override
+ public void onDisplayChanged(BrightnessClamperController.DisplayDeviceData displayData) {
+ mHandler.post(() -> onDisplayChanged(displayData.mDisplayToken, displayData.mWidth,
+ displayData.mHeight, displayData.mDisplayDeviceConfig));
+ }
+
+ // Called in DisplayControllerHandler
+ @Override
+ public void applyStateChange(
+ BrightnessClamperController.ModifiersAggregatedState aggregatedState) {
+ if (mMode != Mode.NO_HDR) {
+ aggregatedState.mMaxDesiredHdrRatio = mMaxDesiredHdrRatio;
+ aggregatedState.mSdrHdrRatioSpline = mHdrBrightnessData.sdrToHdrRatioSpline;
+ aggregatedState.mHdrHbmEnabled = (mMode == Mode.HBM_HDR);
+ }
+ }
+
+ // Called in DisplayControllerHandler
+ private void onDisplayChanged(IBinder displayToken, int width, int height,
+ DisplayDeviceConfig config) {
+ mDisplayDeviceConfig = config;
+ mScreenSize = (float) width * height;
+ HdrBrightnessData data = config.getHdrBrightnessData();
+ if (data == null) {
+ unregisterHdrListener();
+ } else {
+ registerHdrListener(displayToken);
+ }
+ recalculate(data, mMaxDesiredHdrRatio);
+ }
+
+ // Called in DisplayControllerHandler
+ private void recalculate(@Nullable HdrBrightnessData data, float maxDesiredHdrRatio) {
+ Mode newMode = recalculateMode(data);
+ // if HDR mode changed, notify changed
+ boolean needToNotifyChange = mMode != newMode;
+ // If HDR mode is active, we need to check if other HDR params are changed
+ if (mMode != HdrBrightnessModifier.Mode.NO_HDR) {
+ if (!BrightnessSynchronizer.floatEquals(mMaxDesiredHdrRatio, maxDesiredHdrRatio)
+ || data != mHdrBrightnessData) {
+ needToNotifyChange = true;
+ }
+ }
+
+ mMode = newMode;
+ mHdrBrightnessData = data;
+ mMaxDesiredHdrRatio = maxDesiredHdrRatio;
+
+ if (needToNotifyChange) {
+ mClamperChangeListener.onChanged();
+ }
+ }
+
+ // Called in DisplayControllerHandler
+ private Mode recalculateMode(@Nullable HdrBrightnessData data) {
+ // no config
+ if (data == null) {
+ return Mode.NO_HDR;
+ }
+ // HDR layer < minHdr % for Nbm
+ if (mHdrLayerSize < mScreenSize * data.minimumHdrPercentOfScreenForNbm) {
+ return Mode.NO_HDR;
+ }
+ // HDR layer < minHdr % for Hbm, and HDR layer >= that minHdr % for Nbm
+ if (mHdrLayerSize < mScreenSize * data.minimumHdrPercentOfScreenForHbm) {
+ return Mode.NBM_HDR;
+ }
+ // HDR layer > that minHdr % for Hbm
+ return Mode.HBM_HDR;
+ }
+
+ // Called in DisplayControllerHandler
+ private void onHdrInfoChanged(float hdrLayerSize, float maxDesiredHdrSdrRatio) {
+ mHdrLayerSize = hdrLayerSize;
+ recalculate(mHdrBrightnessData, maxDesiredHdrSdrRatio);
+ }
+
+ // Called in DisplayControllerHandler
+ private void registerHdrListener(IBinder displayToken) {
+ if (mRegisteredDisplayToken == displayToken) {
+ return;
+ }
+ unregisterHdrListener();
+ if (displayToken != null) {
+ mInjector.registerHdrListener(mHdrListener, displayToken);
+ mRegisteredDisplayToken = displayToken;
+ }
+ }
+
+ // Called in DisplayControllerHandler
+ private void unregisterHdrListener() {
+ if (mRegisteredDisplayToken != null) {
+ mInjector.unregisterHdrListener(mHdrListener, mRegisteredDisplayToken);
+ mRegisteredDisplayToken = null;
+ mHdrLayerSize = DEFAULT_HDR_LAYER_SIZE;
+ }
+ }
+
+ private enum Mode {
+ NO_HDR, NBM_HDR, HBM_HDR
+ }
+
+ @SuppressLint("MissingPermission")
+ static class Injector {
+ void registerHdrListener(SurfaceControlHdrLayerInfoListener listener, IBinder token) {
+ listener.register(token);
+ }
+
+ void unregisterHdrListener(SurfaceControlHdrLayerInfoListener listener, IBinder token) {
+ listener.unregister(token);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index ecbbd46..5d10780 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1099,14 +1099,9 @@
final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext,
userId, AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO);
InputMethodSettingsRepository.put(userId, newSettings);
- if (!mConcurrentMultiUserModeEnabled) {
- // We need to rebuild IMEs.
- postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId);
- updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId);
- } else {
- // TODO(b/352758479): Stop relying on initializeVisibleBackgroundUserLocked()
- initializeVisibleBackgroundUserLocked(userId);
- }
+ // We need to rebuild IMEs.
+ postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId);
+ updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId);
}
}
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java b/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java
index 17f2fcc..bb35b37 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java
@@ -340,6 +340,11 @@
getOutPrintWriter().println("Profile uses unified challenge");
return false;
}
+ if (mOld.isEmpty()) {
+ getOutPrintWriter().println(
+ "User has a lock credential, but old credential was not provided");
+ return false;
+ }
try {
final boolean result = mLockPatternUtils.checkCredential(getOldCredential(),
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
index d9f11b1..05d4c82 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
@@ -226,6 +226,14 @@
: getDefaultMinAspectRatio();
}
+ float getDefaultMinAspectRatioForUnresizableAppsFromConfig() {
+ return mAppCompatConfiguration.getDefaultMinAspectRatioForUnresizableApps();
+ }
+
+ boolean isSplitScreenAspectRatioForUnresizableAppsEnabled() {
+ return mAppCompatConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled();
+ }
+
private float getDisplaySizeMinAspectRatio() {
final DisplayArea displayArea = mActivityRecord.getDisplayArea();
if (displayArea == null) {
@@ -278,7 +286,7 @@
return getSplitScreenAspectRatio();
}
- private float getDefaultMinAspectRatio() {
+ float getDefaultMinAspectRatio() {
if (mActivityRecord.getDisplayArea() == null
|| !mAppCompatConfiguration
.getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()) {
diff --git a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
index f9f5058..3ecdff6 100644
--- a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
+++ b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
@@ -16,18 +16,33 @@
package com.android.server.wm;
+import static android.content.pm.ActivityInfo.isFixedOrientationLandscape;
+import static android.content.pm.ActivityInfo.isFixedOrientationPortrait;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+
+import static com.android.server.wm.AppCompatConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW;
+import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+import static com.android.server.wm.AppCompatUtils.computeAspectRatio;
import static com.android.server.wm.LaunchParamsUtil.applyLayoutGravity;
import static com.android.server.wm.LaunchParamsUtil.calculateLayoutBounds;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityOptions;
+import android.app.AppCompatTaskInfo;
+import android.app.TaskInfo;
import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.SystemProperties;
import android.util.Size;
import android.view.Gravity;
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
+
import java.util.function.Consumer;
/**
@@ -38,6 +53,8 @@
public static final float DESKTOP_MODE_INITIAL_BOUNDS_SCALE = SystemProperties
.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f;
+ public static final int DESKTOP_MODE_LANDSCAPE_APP_PADDING = SystemProperties
+ .getInt("persist.wm.debug.desktop_mode_landscape_app_padding", 25);
/**
* Updates launch bounds for an activity with respect to its activity options, window layout,
@@ -48,12 +65,8 @@
@NonNull Rect outBounds, @NonNull Consumer<String> logger) {
// Use stable frame instead of raw frame to avoid launching freeform windows on top of
// stable insets, which usually are system widgets such as sysbar & navbar.
- final TaskDisplayArea displayArea = task.getDisplayArea();
- final Rect screenBounds = displayArea.getBounds();
final Rect stableBounds = new Rect();
- displayArea.getStableRect(stableBounds);
- final int desiredWidth = (int) (stableBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
- final int desiredHeight = (int) (stableBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+ task.getDisplayArea().getStableRect(stableBounds);
if (options != null && options.getLaunchBounds() != null) {
outBounds.set(options.getLaunchBounds());
@@ -63,37 +76,282 @@
final int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
if (layout.hasSpecifiedSize()) {
calculateLayoutBounds(stableBounds, layout, outBounds,
- new Size(desiredWidth, desiredHeight));
+ calculateIdealSize(stableBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE));
applyLayoutGravity(verticalGravity, horizontalGravity, outBounds,
stableBounds);
logger.accept("layout specifies sizes, inheriting size and applying gravity");
} else if (verticalGravity > 0 || horizontalGravity > 0) {
- calculateAndCentreInitialBounds(outBounds, screenBounds);
+ outBounds.set(calculateInitialBounds(task, activity, stableBounds));
applyLayoutGravity(verticalGravity, horizontalGravity, outBounds,
stableBounds);
logger.accept("layout specifies gravity, applying desired bounds and gravity");
}
} else {
- calculateAndCentreInitialBounds(outBounds, screenBounds);
+ outBounds.set(calculateInitialBounds(task, activity, stableBounds));
logger.accept("layout not specified, applying desired bounds");
}
}
/**
- * Calculates the initial height and width of a task in desktop mode and centers it within the
- * window bounds.
+ * Calculates the initial bounds required for an application to fill a scale of the display
+ * bounds without any letterboxing. This is done by taking into account the applications
+ * fullscreen size, aspect ratio, orientation and resizability to calculate an area this is
+ * compatible with the applications previous configuration.
*/
- private static void calculateAndCentreInitialBounds(@NonNull Rect outBounds,
+ private static @NonNull Rect calculateInitialBounds(@NonNull Task task,
+ @NonNull ActivityRecord activity, @NonNull Rect stableBounds
+ ) {
+ final TaskInfo taskInfo = task.getTaskInfo();
+ // Display bounds not taking into account insets.
+ final TaskDisplayArea displayArea = task.getDisplayArea();
+ final Rect screenBounds = displayArea.getBounds();
+ final Size idealSize = calculateIdealSize(screenBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+ if (!Flags.enableWindowingDynamicInitialBounds()) {
+ return centerInScreen(idealSize, screenBounds);
+ }
+ // TODO(b/353457301): Replace with app compat aspect ratio method when refactoring complete.
+ float appAspectRatio = calculateAspectRatio(task, activity);
+ final float tdaWidth = stableBounds.width();
+ final float tdaHeight = stableBounds.height();
+ final int activityOrientation = activity.getOverrideOrientation();
+ final Size initialSize = switch (taskInfo.configuration.orientation) {
+ case ORIENTATION_LANDSCAPE -> {
+ // Device in landscape orientation.
+ if (appAspectRatio == 0) {
+ appAspectRatio = 1;
+ }
+ if (taskInfo.isResizeable) {
+ if (isFixedOrientationPortrait(activityOrientation)) {
+ // For portrait resizeable activities, respect apps fullscreen width but
+ // apply ideal size height.
+ yield new Size((int) ((tdaHeight / appAspectRatio) + 0.5f),
+ idealSize.getHeight());
+ }
+ // For landscape resizeable activities, simply apply ideal size.
+ yield idealSize;
+ }
+ // If activity is unresizeable, regardless of orientation, calculate maximum size
+ // (within the ideal size) maintaining original aspect ratio.
+ yield maximizeSizeGivenAspectRatio(
+ activity.getOverrideOrientation(), idealSize, appAspectRatio);
+ }
+ case ORIENTATION_PORTRAIT -> {
+ // Device in portrait orientation.
+ final int customPortraitWidthForLandscapeApp = screenBounds.width()
+ - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2);
+ if (taskInfo.isResizeable) {
+ if (isFixedOrientationLandscape(activityOrientation)) {
+ if (appAspectRatio == 0) {
+ appAspectRatio = tdaWidth / (tdaWidth - 1);
+ }
+ // For landscape resizeable activities, respect apps fullscreen height and
+ // apply custom app width.
+ yield new Size(customPortraitWidthForLandscapeApp,
+ (int) ((tdaWidth / appAspectRatio) + 0.5f));
+ }
+ // For portrait resizeable activities, simply apply ideal size.
+ yield idealSize;
+ }
+ if (appAspectRatio == 0) {
+ appAspectRatio = 1;
+ }
+ if (isFixedOrientationLandscape(activityOrientation)) {
+ // For landscape unresizeable activities, apply custom app width to ideal size
+ // and calculate maximum size with this area while maintaining original aspect
+ // ratio.
+ yield maximizeSizeGivenAspectRatio(activityOrientation,
+ new Size(customPortraitWidthForLandscapeApp, idealSize.getHeight()),
+ appAspectRatio);
+ }
+ // For portrait unresizeable activities, calculate maximum size (within the ideal
+ // size) maintaining original aspect ratio.
+ yield maximizeSizeGivenAspectRatio(activityOrientation, idealSize, appAspectRatio);
+ }
+ default -> idealSize;
+ };
+ return centerInScreen(initialSize, screenBounds);
+ }
+
+ /**
+ * Calculates the largest size that can fit in a given area while maintaining a specific aspect
+ * ratio.
+ */
+ private static @NonNull Size maximizeSizeGivenAspectRatio(
+ @ActivityInfo.ScreenOrientation int orientation,
+ @NonNull Size targetArea,
+ float aspectRatio
+ ) {
+ final int targetHeight = targetArea.getHeight();
+ final int targetWidth = targetArea.getWidth();
+ final int finalHeight;
+ final int finalWidth;
+ if (isFixedOrientationPortrait(orientation)) {
+ // Portrait activity.
+ // Calculate required width given ideal height and aspect ratio.
+ int tempWidth = (int) (targetHeight / aspectRatio);
+ if (tempWidth <= targetWidth) {
+ // If the calculated width does not exceed the ideal width, overall size is within
+ // ideal size and can be applied.
+ finalHeight = targetHeight;
+ finalWidth = tempWidth;
+ } else {
+ // Applying target height cause overall size to exceed ideal size when maintain
+ // aspect ratio. Instead apply ideal width and calculate required height to respect
+ // aspect ratio.
+ finalWidth = targetWidth;
+ finalHeight = (int) (finalWidth * aspectRatio);
+ }
+ } else {
+ // Landscape activity.
+ // Calculate required width given ideal height and aspect ratio.
+ int tempWidth = (int) (targetHeight * aspectRatio);
+ if (tempWidth <= targetWidth) {
+ // If the calculated width does not exceed the ideal width, overall size is within
+ // ideal size and can be applied.
+ finalHeight = targetHeight;
+ finalWidth = tempWidth;
+ } else {
+ // Applying target height cause overall size to exceed ideal size when maintain
+ // aspect ratio. Instead apply ideal width and calculate required height to respect
+ // aspect ratio.
+ finalWidth = targetWidth;
+ finalHeight = (int) (finalWidth / aspectRatio);
+ }
+ }
+ return new Size(finalWidth, finalHeight);
+ }
+
+ /**
+ * Calculates the aspect ratio of an activity from its fullscreen bounds.
+ */
+ @VisibleForTesting
+ static float calculateAspectRatio(@NonNull Task task, @NonNull ActivityRecord activity) {
+ final TaskInfo taskInfo = task.getTaskInfo();
+ final float fullscreenWidth = task.getDisplayArea().getBounds().width();
+ final float fullscreenHeight = task.getDisplayArea().getBounds().height();
+ final float maxAspectRatio = activity.getMaxAspectRatio();
+ final float minAspectRatio = activity.getMinAspectRatio();
+ float desiredAspectRatio = 0;
+ if (taskInfo.isRunning) {
+ final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo;
+ if (appCompatTaskInfo.topActivityBoundsLetterboxed) {
+ desiredAspectRatio = (float) Math.max(
+ appCompatTaskInfo.topActivityLetterboxWidth,
+ appCompatTaskInfo.topActivityLetterboxHeight)
+ / Math.min(appCompatTaskInfo.topActivityLetterboxWidth,
+ appCompatTaskInfo.topActivityLetterboxHeight);
+ } else {
+ desiredAspectRatio = Math.max(fullscreenHeight, fullscreenWidth)
+ / Math.min(fullscreenHeight, fullscreenWidth);
+ }
+ } else {
+ final float letterboxAspectRatioOverride =
+ getFixedOrientationLetterboxAspectRatio(activity, task);
+ if (!task.mDisplayContent.getIgnoreOrientationRequest()) {
+ desiredAspectRatio = DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW;
+ } else if (letterboxAspectRatioOverride
+ > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) {
+ desiredAspectRatio = letterboxAspectRatioOverride;
+ }
+ }
+ // If the activity matches display orientation, the display aspect ratio should be used
+ if (activityMatchesDisplayOrientation(
+ taskInfo.configuration.orientation,
+ activity.getOverrideOrientation())) {
+ desiredAspectRatio = Math.max(fullscreenWidth, fullscreenHeight)
+ / Math.min(fullscreenWidth, fullscreenHeight);
+ }
+ if (maxAspectRatio >= 1 && desiredAspectRatio > maxAspectRatio) {
+ desiredAspectRatio = maxAspectRatio;
+ } else if (minAspectRatio >= 1 && desiredAspectRatio < minAspectRatio) {
+ desiredAspectRatio = minAspectRatio;
+ }
+ return desiredAspectRatio;
+ }
+
+ private static boolean activityMatchesDisplayOrientation(
+ @Configuration.Orientation int deviceOrientation,
+ @ActivityInfo.ScreenOrientation int activityOrientation) {
+ if (deviceOrientation == ORIENTATION_PORTRAIT) {
+ return isFixedOrientationPortrait(activityOrientation);
+ }
+ return isFixedOrientationLandscape(activityOrientation);
+ }
+
+ /**
+ * Calculates the desired initial bounds for applications in desktop windowing. This is done as
+ * a scale of the screen bounds.
+ */
+ private static @NonNull Size calculateIdealSize(@NonNull Rect screenBounds, float scale) {
+ final int width = (int) (screenBounds.width() * scale);
+ final int height = (int) (screenBounds.height() * scale);
+ return new Size(width, height);
+ }
+
+ /**
+ * Adjusts bounds to be positioned in the middle of the screen.
+ */
+ private static @NonNull Rect centerInScreen(@NonNull Size desiredSize,
@NonNull Rect screenBounds) {
- // TODO(b/319819547): Account for app constraints so apps do not become letterboxed
- // The desired dimensions that a fully resizable window should take when initially entering
- // desktop mode. Calculated as a percentage of the available display area as defined by the
- // DESKTOP_MODE_INITIAL_BOUNDS_SCALE.
- final int desiredWidth = (int) (screenBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
- final int desiredHeight = (int) (screenBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
- outBounds.right = desiredWidth;
- outBounds.bottom = desiredHeight;
- outBounds.offset(screenBounds.centerX() - outBounds.centerX(),
- screenBounds.centerY() - outBounds.centerY());
+ // TODO(b/325240051): Position apps with bottom heavy offset
+ final int heightOffset = (screenBounds.height() - desiredSize.getHeight()) / 2;
+ final int widthOffset = (screenBounds.width() - desiredSize.getWidth()) / 2;
+ final Rect resultBounds = new Rect(0, 0,
+ desiredSize.getWidth(), desiredSize.getHeight());
+ resultBounds.offset(screenBounds.left + widthOffset, screenBounds.top + heightOffset);
+ return resultBounds;
+ }
+
+ private static float getFixedOrientationLetterboxAspectRatio(@NonNull ActivityRecord activity,
+ @NonNull Task task) {
+ return activity.shouldCreateCompatDisplayInsets()
+ ? getDefaultMinAspectRatioForUnresizableApps(activity, task)
+ : activity.mAppCompatController.getAppCompatAspectRatioOverrides()
+ .getDefaultMinAspectRatio();
+ }
+
+ private static float getDefaultMinAspectRatioForUnresizableApps(
+ @NonNull ActivityRecord activity,
+ @NonNull Task task) {
+ final AppCompatAspectRatioOverrides appCompatAspectRatioOverrides =
+ activity.mAppCompatController.getAppCompatAspectRatioOverrides();
+ if (appCompatAspectRatioOverrides.isSplitScreenAspectRatioForUnresizableAppsEnabled()) {
+ // Default letterbox aspect ratio for unresizable apps.
+ return getSplitScreenAspectRatio(activity, task);
+ }
+
+ if (appCompatAspectRatioOverrides.getDefaultMinAspectRatioForUnresizableAppsFromConfig()
+ > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) {
+ return appCompatAspectRatioOverrides
+ .getDefaultMinAspectRatioForUnresizableAppsFromConfig();
+ }
+
+ return appCompatAspectRatioOverrides.getDefaultMinAspectRatio();
+ }
+
+ /**
+ * Calculates the aspect ratio of the available display area when an app enters split-screen on
+ * a given device, taking into account any dividers and insets.
+ */
+ private static float getSplitScreenAspectRatio(@NonNull ActivityRecord activity,
+ @NonNull Task task) {
+ final int dividerWindowWidth =
+ activity.mWmService.mContext.getResources().getDimensionPixelSize(
+ R.dimen.docked_stack_divider_thickness);
+ final int dividerInsets =
+ activity.mWmService.mContext.getResources().getDimensionPixelSize(
+ R.dimen.docked_stack_divider_insets);
+ final int dividerSize = dividerWindowWidth - dividerInsets * 2;
+ final Rect bounds = new Rect(0, 0,
+ task.mDisplayContent.getDisplayInfo().appWidth,
+ task.mDisplayContent.getDisplayInfo().appHeight);
+ if (bounds.width() >= bounds.height()) {
+ bounds.inset(/* dx */ dividerSize / 2, /* dy */ 0);
+ bounds.right = bounds.centerX();
+ } else {
+ bounds.inset(/* dx */ 0, /* dy */ dividerSize / 2);
+ bounds.bottom = bounds.centerY();
+ }
+ return computeAspectRatio(bounds);
}
}
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index aacd3c6..548addb 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -25,7 +25,6 @@
import android.app.ActivityOptions;
import android.content.Context;
import android.content.pm.ActivityInfo;
-import android.os.SystemProperties;
import android.util.Slog;
import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
@@ -38,19 +37,9 @@
TAG_WITH_CLASS_NAME ? "DesktopModeLaunchParamsModifier" : TAG_ATM;
private static final boolean DEBUG = false;
- public static final float DESKTOP_MODE_INITIAL_BOUNDS_SCALE =
- SystemProperties
- .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f;
-
- /**
- * Flag to indicate whether to restrict desktop mode to supported devices.
- */
- private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean(
- "persist.wm.debug.desktop_mode_enforce_device_restrictions", true);
-
private StringBuilder mLogBuilder;
- private final Context mContext;
+ @NonNull private final Context mContext;
DesktopModeLaunchParamsModifier(@NonNull Context context) {
mContext = context;
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
index e982153..e04716e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
@@ -24,6 +24,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -41,8 +42,8 @@
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.server.display.DisplayBrightnessState;
-import com.android.server.display.DisplayDeviceConfig;
import com.android.server.display.brightness.BrightnessReason;
+import com.android.server.display.brightness.clamper.BrightnessClamperController.ModifiersAggregatedState;
import com.android.server.display.config.SensorData;
import com.android.server.display.feature.DeviceConfigParameterProvider;
import com.android.server.display.feature.DisplayManagerFlags;
@@ -89,6 +90,10 @@
@Mock
private BrightnessModifier mMockModifier;
@Mock
+ private TestStatefulModifier mMockStatefulModifier;
+ @Mock
+ private TestDisplayListenerModifier mMockDisplayListenerModifier;
+ @Mock
private DisplayManagerInternal.DisplayPowerRequest mMockRequest;
@Mock
@@ -99,7 +104,8 @@
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mTestInjector = new TestInjector(List.of(mMockClamper), List.of(mMockModifier));
+ mTestInjector = new TestInjector(List.of(mMockClamper),
+ List.of(mMockModifier, mMockStatefulModifier, mMockDisplayListenerModifier));
when(mMockDisplayDeviceData.getDisplayId()).thenReturn(DISPLAY_ID);
when(mMockDisplayDeviceData.getAmbientLightSensor()).thenReturn(mMockSensorData);
@@ -168,6 +174,13 @@
}
@Test
+ public void testOnDisplayChanged_DelegatesToDisplayListeners() {
+ mClamperController.onDisplayChanged(mMockDisplayDeviceData);
+
+ verify(mMockDisplayListenerModifier).onDisplayChanged(mMockDisplayDeviceData);
+ }
+
+ @Test
public void testOnDisplayChanged_doesNotRestartLightSensor() {
mClamperController.onDisplayChanged(mMockDisplayDeviceData);
@@ -189,6 +202,8 @@
mClamperController.clamp(mMockRequest, initialBrightness, initialSlowChange, STATE_ON);
verify(mMockModifier).apply(eq(mMockRequest), any());
+ verify(mMockDisplayListenerModifier).apply(eq(mMockRequest), any());
+ verify(mMockStatefulModifier).apply(eq(mMockRequest), any());
}
@Test
@@ -326,11 +341,40 @@
verify(mMockClamper).stop();
}
+ @Test
+ public void test_doesNotNotifyExternalListener_aggregatedStateNotChanged() {
+ mTestInjector.mCapturedChangeListener.onChanged();
+ mTestHandler.flush();
+
+ verify(mMockExternalListener, never()).onChanged();
+ }
+
+ @Test
+ public void test_notifiesExternalListener_aggregatedStateChanged() {
+ doAnswer((invocation) -> {
+ ModifiersAggregatedState argument = invocation.getArgument(0);
+ argument.mHdrHbmEnabled = true;
+ return null;
+ }).when(mMockStatefulModifier).applyStateChange(any());
+ mTestInjector.mCapturedChangeListener.onChanged();
+ mTestHandler.flush();
+
+ verify(mMockExternalListener).onChanged();
+ }
+
private BrightnessClamperController createBrightnessClamperController() {
return new BrightnessClamperController(mTestInjector, mTestHandler, mMockExternalListener,
mMockDisplayDeviceData, mMockContext, mFlags, mSensorManager);
}
+ interface TestDisplayListenerModifier extends BrightnessStateModifier,
+ BrightnessClamperController.DisplayDeviceDataListener {
+ }
+
+ interface TestStatefulModifier extends BrightnessStateModifier,
+ BrightnessClamperController.StatefulModifier {
+ }
+
private class TestInjector extends BrightnessClamperController.Injector {
private final List<BrightnessClamper<? super BrightnessClamperController.DisplayDeviceData>>
@@ -366,7 +410,7 @@
@Override
List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context,
Handler handler, BrightnessClamperController.ClamperChangeListener listener,
- DisplayDeviceConfig displayDeviceConfig) {
+ BrightnessClamperController.DisplayDeviceData displayDeviceData) {
return mModifiers;
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt
new file mode 100644
index 0000000..5fd848f
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.display.brightness.clamper
+
+import android.os.IBinder
+import android.view.Display
+import com.android.server.display.DisplayDeviceConfig
+import com.android.server.display.brightness.clamper.BrightnessClamperController.DisplayDeviceData
+
+fun createDisplayDeviceData(
+ displayDeviceConfig: DisplayDeviceConfig,
+ displayToken: IBinder,
+ uniqueDisplayId: String = "displayId",
+ thermalThrottlingDataId: String = "thermalId",
+ powerThrottlingDataId: String = "powerId",
+ width: Int = 100,
+ height: Int = 100,
+ displayId: Int = Display.DEFAULT_DISPLAY
+): DisplayDeviceData {
+ return DisplayDeviceData(
+ uniqueDisplayId,
+ thermalThrottlingDataId,
+ powerThrottlingDataId,
+ displayDeviceConfig,
+ width,
+ height,
+ displayToken,
+ displayId
+ )
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt
new file mode 100644
index 0000000..e9ec811
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt
@@ -0,0 +1,252 @@
+/*
+ * 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.display.brightness.clamper
+
+import android.hardware.display.DisplayManagerInternal
+import android.os.IBinder
+import android.util.Spline
+import android.view.SurfaceControlHdrLayerInfoListener
+import androidx.test.filters.SmallTest
+import com.android.server.display.DisplayBrightnessState
+import com.android.server.display.DisplayDeviceConfig
+import com.android.server.display.brightness.clamper.BrightnessClamperController.ClamperChangeListener
+import com.android.server.display.brightness.clamper.BrightnessClamperController.ModifiersAggregatedState
+import com.android.server.display.brightness.clamper.HdrBrightnessModifier.DEFAULT_MAX_HDR_SDR_RATIO
+import com.android.server.display.brightness.clamper.HdrBrightnessModifier.Injector
+import com.android.server.display.config.createHdrBrightnessData
+import com.android.server.testutils.TestHandler
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+class HdrBrightnessModifierTest {
+
+ private val testHandler = TestHandler(null)
+ private val testInjector = TestInjector()
+ private val mockChangeListener = mock<ClamperChangeListener>()
+ private val mockDisplayDeviceConfig = mock<DisplayDeviceConfig>()
+ private val mockDisplayBinder = mock<IBinder>()
+ private val mockDisplayBinderOther = mock<IBinder>()
+ private val mockSpline = mock<Spline>()
+ private val mockRequest = mock<DisplayManagerInternal.DisplayPowerRequest>()
+
+ private lateinit var modifier: HdrBrightnessModifier
+ private val dummyData = createDisplayDeviceData(mockDisplayDeviceConfig, mockDisplayBinder)
+ private val dummyHdrData = createHdrBrightnessData()
+
+ @Test
+ fun `change listener is not called on init`() {
+ initHdrModifier()
+
+ verify(mockChangeListener, never()).onChanged()
+ }
+
+ @Test
+ fun `hdr listener registered on init if hdr data is present`() {
+ initHdrModifier()
+
+ assertThat(testInjector.registeredHdrListener).isNotNull()
+ assertThat(testInjector.registeredToken).isEqualTo(mockDisplayBinder)
+ }
+
+ @Test
+ fun `hdr listener not registered on init if hdr data is missing`() {
+ whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(null)
+ modifier = HdrBrightnessModifier(testHandler, mockChangeListener, testInjector, dummyData)
+
+ testHandler.flush()
+
+ assertThat(testInjector.registeredHdrListener).isNull()
+ assertThat(testInjector.registeredToken).isNull()
+ }
+
+ @Test
+ fun `unsubscribes hdr listener when display changed with no hdr data`() {
+ initHdrModifier()
+
+ whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(null)
+ modifier.onDisplayChanged(dummyData)
+ testHandler.flush()
+
+ assertThat(testInjector.registeredHdrListener).isNull()
+ assertThat(testInjector.registeredToken).isNull()
+ verify(mockChangeListener, never()).onChanged()
+ }
+
+ @Test
+ fun `resubscribes hdr listener when display changed with different token`() {
+ initHdrModifier()
+
+ modifier.onDisplayChanged(
+ createDisplayDeviceData(mockDisplayDeviceConfig, mockDisplayBinderOther))
+ testHandler.flush()
+
+ assertThat(testInjector.registeredHdrListener).isNotNull()
+ assertThat(testInjector.registeredToken).isEqualTo(mockDisplayBinderOther)
+ verify(mockChangeListener, never()).onChanged()
+ }
+
+ @Test
+ fun `test NO_HDR mode`() {
+ initHdrModifier()
+
+ whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(createHdrBrightnessData(
+ minimumHdrPercentOfScreenForNbm = 0.5f,
+ minimumHdrPercentOfScreenForHbm = 0.7f,
+ sdrToHdrRatioSpline = mockSpline
+ ))
+ // screen size = 10_000
+ modifier.onDisplayChanged(createDisplayDeviceData(
+ mockDisplayDeviceConfig, mockDisplayBinder,
+ width = 100,
+ height = 100
+ ))
+ testHandler.flush()
+ // hdr size = 900
+ val desiredMaxHdrRatio = 8f
+ val hdrWidth = 30
+ val hdrHeight = 30
+ testInjector.registeredHdrListener!!.onHdrInfoChanged(
+ mockDisplayBinder, 1, hdrWidth, hdrHeight, 0, desiredMaxHdrRatio
+ )
+ testHandler.flush()
+
+ val modifierState = ModifiersAggregatedState()
+ modifier.applyStateChange(modifierState)
+
+ assertThat(modifierState.mHdrHbmEnabled).isFalse()
+ assertThat(modifierState.mMaxDesiredHdrRatio).isEqualTo(DEFAULT_MAX_HDR_SDR_RATIO)
+ assertThat(modifierState.mSdrHdrRatioSpline).isNull()
+
+ val stateBuilder = DisplayBrightnessState.builder()
+ modifier.apply(mockRequest, stateBuilder)
+
+ verify(mockDisplayDeviceConfig, never()).getHdrBrightnessFromSdr(any(), any(), any())
+ assertThat(stateBuilder.hdrBrightness).isEqualTo(DisplayBrightnessState.BRIGHTNESS_NOT_SET)
+ }
+
+ @Test
+ fun `test NBM_HDR mode`() {
+ initHdrModifier()
+ whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(createHdrBrightnessData(
+ minimumHdrPercentOfScreenForNbm = 0.5f,
+ minimumHdrPercentOfScreenForHbm = 0.7f,
+ sdrToHdrRatioSpline = mockSpline
+ ))
+ // screen size = 10_000
+ modifier.onDisplayChanged(createDisplayDeviceData(
+ mockDisplayDeviceConfig, mockDisplayBinder,
+ width = 100,
+ height = 100
+ ))
+ testHandler.flush()
+ // hdr size = 5_100
+ val desiredMaxHdrRatio = 8f
+ val hdrWidth = 100
+ val hdrHeight = 51
+ testInjector.registeredHdrListener!!.onHdrInfoChanged(
+ mockDisplayBinder, 1, hdrWidth, hdrHeight, 0, desiredMaxHdrRatio
+ )
+ testHandler.flush()
+
+ val modifierState = ModifiersAggregatedState()
+ modifier.applyStateChange(modifierState)
+
+ assertThat(modifierState.mHdrHbmEnabled).isFalse()
+ assertThat(modifierState.mMaxDesiredHdrRatio).isEqualTo(desiredMaxHdrRatio)
+ assertThat(modifierState.mSdrHdrRatioSpline).isEqualTo(mockSpline)
+
+ val expectedHdrBrightness = 0.85f
+ whenever(mockDisplayDeviceConfig.getHdrBrightnessFromSdr(
+ 0f, desiredMaxHdrRatio, mockSpline)).thenReturn(expectedHdrBrightness)
+ val stateBuilder = DisplayBrightnessState.builder()
+ modifier.apply(mockRequest, stateBuilder)
+
+ assertThat(stateBuilder.hdrBrightness).isEqualTo(expectedHdrBrightness)
+ }
+
+ @Test
+ fun `test HBM_HDR mode`() {
+ initHdrModifier()
+ whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(createHdrBrightnessData(
+ minimumHdrPercentOfScreenForNbm = 0.5f,
+ minimumHdrPercentOfScreenForHbm = 0.7f,
+ sdrToHdrRatioSpline = mockSpline
+ ))
+ // screen size = 10_000
+ modifier.onDisplayChanged(createDisplayDeviceData(
+ mockDisplayDeviceConfig, mockDisplayBinder,
+ width = 100,
+ height = 100
+ ))
+ testHandler.flush()
+ // hdr size = 7_100
+ val desiredMaxHdrRatio = 8f
+ val hdrWidth = 100
+ val hdrHeight = 71
+ testInjector.registeredHdrListener!!.onHdrInfoChanged(
+ mockDisplayBinder, 1, hdrWidth, hdrHeight, 0, desiredMaxHdrRatio
+ )
+ testHandler.flush()
+
+ val modifierState = ModifiersAggregatedState()
+ modifier.applyStateChange(modifierState)
+
+ assertThat(modifierState.mHdrHbmEnabled).isTrue()
+ assertThat(modifierState.mMaxDesiredHdrRatio).isEqualTo(desiredMaxHdrRatio)
+ assertThat(modifierState.mSdrHdrRatioSpline).isEqualTo(mockSpline)
+
+ val expectedHdrBrightness = 0.83f
+ whenever(mockDisplayDeviceConfig.getHdrBrightnessFromSdr(
+ 0f, desiredMaxHdrRatio, mockSpline)).thenReturn(expectedHdrBrightness)
+ val stateBuilder = DisplayBrightnessState.builder()
+ modifier.apply(mockRequest, stateBuilder)
+
+ assertThat(stateBuilder.hdrBrightness).isEqualTo(expectedHdrBrightness)
+ }
+
+ private fun initHdrModifier() {
+ whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(dummyHdrData)
+ modifier = HdrBrightnessModifier(testHandler, mockChangeListener, testInjector, dummyData)
+ testHandler.flush()
+ }
+
+
+ internal class TestInjector : Injector() {
+ var registeredHdrListener: SurfaceControlHdrLayerInfoListener? = null
+ var registeredToken: IBinder? = null
+
+ override fun registerHdrListener(
+ listener: SurfaceControlHdrLayerInfoListener, token: IBinder
+ ) {
+ registeredHdrListener = listener
+ registeredToken = token
+ }
+
+ override fun unregisterHdrListener(
+ listener: SurfaceControlHdrLayerInfoListener, token: IBinder
+ ) {
+ registeredHdrListener = null
+ registeredToken = null
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
index 72883e2..5bd919f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
@@ -23,6 +23,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import static org.testng.AssertJUnit.assertFalse;
import android.os.Environment;
import android.os.FileUtils;
@@ -51,6 +52,7 @@
private static final int USER_ID_1 = 10;
private static final int USER_ID_2 = 11;
private static final int USER_ID_3 = 12;
+ private static final int USER_ID_SYSTEM = 0;
private static final long TEST_TIMESTAMP = 150_000;
private static final File TEST_SYSTEM_DIR = new File(InstrumentationRegistry
.getInstrumentation().getContext().getDataDir(), "alarmsTestDir");
@@ -110,6 +112,14 @@
}
@Test
+ public void testAddWakeupForSystemUser_shouldDoNothing() {
+ mUserWakeupStore.addUserWakeup(USER_ID_SYSTEM, TEST_TIMESTAMP - 19_000);
+ assertEquals(0, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length);
+ final File file = new File(ROOT_DIR , "usersWithAlarmClocks.xml");
+ assertFalse(file.exists());
+ }
+
+ @Test
public void testAddMultipleWakeupsForUser_ensureOnlyLastWakeupRemains() {
final long finalAlarmTime = TEST_TIMESTAMP - 13_000;
mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 29_000);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
index 23a88a1..b687042 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -21,9 +21,19 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.util.DisplayMetrics.DENSITY_DEFAULT;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.server.wm.DesktopModeLaunchParamsModifier.DESKTOP_MODE_INITIAL_BOUNDS_SCALE;
+import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_INITIAL_BOUNDS_SCALE;
+import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_LANDSCAPE_APP_PADDING;
+import static com.android.server.wm.DesktopModeBoundsCalculator.calculateAspectRatio;
import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY;
import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE;
import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP;
@@ -59,6 +69,10 @@
@RunWith(WindowTestRunner.class)
public class DesktopModeLaunchParamsModifierTests extends
LaunchParamsModifierTestsBase<DesktopModeLaunchParamsModifier> {
+ private static final Rect LANDSCAPE_DISPLAY_BOUNDS = new Rect(0, 0, 2560, 1600);
+ private static final Rect PORTRAIT_DISPLAY_BOUNDS = new Rect(0, 0, 1600, 2560);
+ private static final float LETTERBOX_ASPECT_RATIO = 1.3f;
+
@Before
public void setUp() throws Exception {
mActivity = new ActivityBuilder(mAtm).build();
@@ -158,6 +172,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
public void testUsesDesiredBoundsIfEmptyLayoutAndActivityOptionsBounds() {
setupDesktopModeLaunchParamsModifier();
@@ -169,6 +184,209 @@
(int) (DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
final int desiredHeight =
(int) (DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+ assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+ assertEquals(desiredWidth, mResult.mBounds.width());
+ assertEquals(desiredHeight, mResult.mBounds.height());
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+ public void testDefaultLandscapeBounds_landscapeDevice_resizable_undefinedOrientation() {
+ setupDesktopModeLaunchParamsModifier();
+
+ final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+ LANDSCAPE_DISPLAY_BOUNDS);
+ final Task task = createTask(display, SCREEN_ORIENTATION_UNSPECIFIED, true);
+
+ final int desiredWidth =
+ (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+ final int desiredHeight =
+ (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+ assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+ assertEquals(desiredWidth, mResult.mBounds.width());
+ assertEquals(desiredHeight, mResult.mBounds.height());
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+ public void testDefaultLandscapeBounds_landscapeDevice_resizable_landscapeOrientation() {
+ setupDesktopModeLaunchParamsModifier();
+
+ final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+ LANDSCAPE_DISPLAY_BOUNDS);
+ final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, true);
+
+ final int desiredWidth =
+ (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+ final int desiredHeight =
+ (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+ assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+ assertEquals(desiredWidth, mResult.mBounds.width());
+ assertEquals(desiredHeight, mResult.mBounds.height());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ public void testResizablePortraitBounds_landscapeDevice_resizable_portraitOrientation() {
+ setupDesktopModeLaunchParamsModifier();
+ doReturn(LETTERBOX_ASPECT_RATIO).when(()
+ -> calculateAspectRatio(any(), any()));
+
+ final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+ LANDSCAPE_DISPLAY_BOUNDS);
+ final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, true);
+
+ final int desiredWidth =
+ (int) ((LANDSCAPE_DISPLAY_BOUNDS.height() / LETTERBOX_ASPECT_RATIO) + 0.5f);
+ final int desiredHeight =
+ (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+ assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+ assertEquals(desiredWidth, mResult.mBounds.width());
+ assertEquals(desiredHeight, mResult.mBounds.height());
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+ public void testDefaultLandscapeBounds_landscapeDevice_unResizable_landscapeOrientation() {
+ setupDesktopModeLaunchParamsModifier();
+
+ final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+ LANDSCAPE_DISPLAY_BOUNDS);
+ final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, false);
+
+ final int desiredWidth =
+ (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+ final int desiredHeight =
+ (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+ assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+ assertEquals(desiredWidth, mResult.mBounds.width());
+ assertEquals(desiredHeight, mResult.mBounds.height());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ public void testUnResizablePortraitBounds_landscapeDevice_unResizable_portraitOrientation() {
+ setupDesktopModeLaunchParamsModifier();
+ doReturn(LETTERBOX_ASPECT_RATIO).when(()
+ -> calculateAspectRatio(any(), any()));
+
+ final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+ LANDSCAPE_DISPLAY_BOUNDS);
+ final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, false);
+
+ final int desiredHeight =
+ (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+ final int desiredWidth = (int) (desiredHeight / LETTERBOX_ASPECT_RATIO);
+
+ assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+ assertEquals(desiredWidth, mResult.mBounds.width());
+ assertEquals(desiredHeight, mResult.mBounds.height());
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+ public void testDefaultPortraitBounds_portraitDevice_resizable_undefinedOrientation() {
+ setupDesktopModeLaunchParamsModifier();
+
+ final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+ PORTRAIT_DISPLAY_BOUNDS);
+ final Task task = createTask(display, SCREEN_ORIENTATION_UNSPECIFIED, true);
+
+ final int desiredWidth =
+ (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+ final int desiredHeight =
+ (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+ assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+ assertEquals(desiredWidth, mResult.mBounds.width());
+ assertEquals(desiredHeight, mResult.mBounds.height());
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+ public void testDefaultPortraitBounds_portraitDevice_resizable_portraitOrientation() {
+ setupDesktopModeLaunchParamsModifier();
+
+ final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+ PORTRAIT_DISPLAY_BOUNDS);
+ final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, true);
+
+ final int desiredWidth =
+ (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+ final int desiredHeight =
+ (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+ assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+ assertEquals(desiredWidth, mResult.mBounds.width());
+ assertEquals(desiredHeight, mResult.mBounds.height());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ public void testResizableLandscapeBounds_portraitDevice_resizable_landscapeOrientation() {
+ setupDesktopModeLaunchParamsModifier();
+ doReturn(LETTERBOX_ASPECT_RATIO).when(()
+ -> calculateAspectRatio(any(), any()));
+
+ final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+ PORTRAIT_DISPLAY_BOUNDS);
+ final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, true);
+
+ final int desiredWidth = PORTRAIT_DISPLAY_BOUNDS.width()
+ - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2);
+ final int desiredHeight = (int)
+ ((PORTRAIT_DISPLAY_BOUNDS.width() / LETTERBOX_ASPECT_RATIO) + 0.5f);
+
+ assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+ assertEquals(desiredWidth, mResult.mBounds.width());
+ assertEquals(desiredHeight, mResult.mBounds.height());
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+ public void testDefaultPortraitBounds_portraitDevice_unResizable_portraitOrientation() {
+ setupDesktopModeLaunchParamsModifier();
+
+ final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+ PORTRAIT_DISPLAY_BOUNDS);
+ final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, false);
+
+ final int desiredWidth =
+ (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+ final int desiredHeight =
+ (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+ assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+ assertEquals(desiredWidth, mResult.mBounds.width());
+ assertEquals(desiredHeight, mResult.mBounds.height());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ public void testUnResizableLandscapeBounds_portraitDevice_unResizable_landscapeOrientation() {
+ setupDesktopModeLaunchParamsModifier();
+ doReturn(LETTERBOX_ASPECT_RATIO).when(()
+ -> calculateAspectRatio(any(), any()));
+
+ final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+ PORTRAIT_DISPLAY_BOUNDS);
+ final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, false);
+
+ final int desiredWidth = PORTRAIT_DISPLAY_BOUNDS.width()
+ - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2);
+ final int desiredHeight = (int) (desiredWidth / LETTERBOX_ASPECT_RATIO);
+
assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
assertEquals(desiredWidth, mResult.mBounds.width());
assertEquals(desiredHeight, mResult.mBounds.height());
@@ -192,6 +410,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBounds_CenterToDisplay() {
setupDesktopModeLaunchParamsModifier();
@@ -207,6 +426,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBounds_LeftGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -222,6 +442,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBounds_TopGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -237,6 +458,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBounds_TopLeftGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -252,6 +474,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBounds_RightGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -267,6 +490,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBounds_BottomGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -282,6 +506,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBounds_RightBottomGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -297,6 +522,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutFractionBounds() {
setupDesktopModeLaunchParamsModifier();
@@ -312,6 +538,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_LeftGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -327,6 +554,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -342,6 +570,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopLeftGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -359,6 +588,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_RightGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -374,6 +604,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -389,6 +620,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomRightGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -422,6 +654,38 @@
assertEquals(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode);
}
+ private Task createTask(DisplayContent display, int orientation, Boolean isResizeable) {
+ final int resizeMode = isResizeable ? RESIZE_MODE_RESIZEABLE
+ : RESIZE_MODE_UNRESIZEABLE;
+ final Task task = new TaskBuilder(mSupervisor).setActivityType(
+ ACTIVITY_TYPE_STANDARD).setDisplay(display).build();
+ task.setResizeMode(resizeMode);
+ mActivity = new ActivityBuilder(task.mAtmService)
+ .setTask(task)
+ .setScreenOrientation(orientation)
+ .setOnTop(true).build();
+
+ mActivity.onDisplayChanged(display);
+ mActivity.setOccludesParent(true);
+ mActivity.setVisible(true);
+ mActivity.setVisibleRequested(true);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(/* ignoreOrientationRequest */ true);
+
+ return task;
+ }
+
+ private TestDisplayContent createDisplayContent(int orientation, Rect displayBounds) {
+ final TestDisplayContent display = new TestDisplayContent
+ .Builder(mAtm, displayBounds.width(), displayBounds.height())
+ .setPosition(DisplayContent.POSITION_TOP).build();
+ display.setBounds(displayBounds);
+ display.getConfiguration().densityDpi = DENSITY_DEFAULT;
+ display.getConfiguration().orientation = ORIENTATION_LANDSCAPE;
+ display.getDefaultTaskDisplayArea().setWindowingMode(orientation);
+
+ return display;
+ }
+
private void setupDesktopModeLaunchParamsModifier() {
setupDesktopModeLaunchParamsModifier(/*isDesktopModeSupported=*/ true,
/*enforceDeviceRestrictions=*/ true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index a816aa9..d5d2847 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -203,6 +203,7 @@
.mockStatic(LockGuard.class, mockStubOnly)
.mockStatic(Watchdog.class, mockStubOnly)
.spyStatic(DesktopModeHelper.class)
+ .spyStatic(DesktopModeBoundsCalculator.class)
.strictness(Strictness.LENIENT)
.startMocking();