Merge "[flexiglass] Remove an unflagged change from StackScrollAlgorithm" into main
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index c21fe0e..e8b0a36f 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -3216,11 +3216,13 @@
*/
@FlaggedApi(Flags.FLAG_UI_RICH_ONGOING)
public boolean hasPromotableStyle() {
- //TODO(b/367739672): Add progress style
- return extras == null || !extras.containsKey(Notification.EXTRA_TEMPLATE)
- || isStyle(Notification.BigPictureStyle.class)
- || isStyle(Notification.BigTextStyle.class)
- || isStyle(Notification.CallStyle.class);
+ final Class<? extends Style> notificationStyle = getNotificationStyle();
+
+ return notificationStyle == null
+ || BigPictureStyle.class.equals(notificationStyle)
+ || BigTextStyle.class.equals(notificationStyle)
+ || CallStyle.class.equals(notificationStyle)
+ || ProgressStyle.class.equals(notificationStyle);
}
/**
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 38f59ad..c1c96ea 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -299,6 +299,14 @@
"android.service.wallpaper.extra.FROM_FOREGROUND_APP";
/**
+ * Extra passed on {@link Intent.ACTION_WALLPAPER_CHANGED} indicating if wallpaper was set from
+ * a foreground app.
+ * @hide
+ */
+ public static final String EXTRA_WHICH_WALLPAPER_CHANGED =
+ "android.service.wallpaper.extra.WHICH_WALLPAPER_CHANGED";
+
+ /**
* The different screen orientations. {@link #getOrientation} provides their exact definition.
* This is only used internally by the framework and the WallpaperBackupAgent.
* @hide
diff --git a/core/java/android/app/wallpaper.aconfig b/core/java/android/app/wallpaper.aconfig
index 4091622..c5bd56f 100644
--- a/core/java/android/app/wallpaper.aconfig
+++ b/core/java/android/app/wallpaper.aconfig
@@ -1,8 +1,16 @@
package: "android.app"
container: "system"
+
flag {
name: "remove_next_wallpaper_component"
namespace: "systemui"
description: "Remove deprecated field WallpaperData#nextWallpaperComponent. Only effective after rebooting."
bug: "365991991"
}
+
+flag {
+ name: "fix_wallpaper_changed"
+ namespace: "systemui"
+ description: "Fixes timing of wallpaper changed notification and adds extra information. Only effective after rebooting."
+ bug: "369814294"
+}
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 303197d..e173255 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -1079,9 +1079,12 @@
// in ensureManualZenRule() and setManualZenMode().
rt.manualRule.pkg = PACKAGE_ANDROID;
rt.manualRule.type = AutomaticZenRule.TYPE_OTHER;
- rt.manualRule.condition = new Condition(
- rt.manualRule.conditionId != null ? rt.manualRule.conditionId
- : Uri.EMPTY, "", Condition.STATE_TRUE);
+ // conditionId in rule must match condition.id to pass isValidManualRule().
+ if (rt.manualRule.conditionId == null) {
+ rt.manualRule.conditionId = Uri.EMPTY;
+ }
+ rt.manualRule.condition = new Condition(rt.manualRule.conditionId, "",
+ Condition.STATE_TRUE);
}
}
return rt;
diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS
index 31a8dfa..1ea58bc 100644
--- a/core/java/android/view/OWNERS
+++ b/core/java/android/view/OWNERS
@@ -48,6 +48,7 @@
per-file VelocityTracker.java = file:/services/core/java/com/android/server/input/OWNERS
per-file VerifiedInputEvent.java = file:/services/core/java/com/android/server/input/OWNERS
per-file VerifiedInputEvent.aidl = file:/services/core/java/com/android/server/input/OWNERS
+per-file LetterboxScrollProcessor*.java = file:/services/core/java/com/android/server/input/OWNERS
# InputWindowHandle
per-file InputWindowHandle.java = file:/services/core/java/com/android/server/input/OWNERS
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index ccaaf63..086063f 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -299,3 +299,11 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "ensure_keyguard_does_transition_starting"
+ namespace: "windowing_frontend"
+ description: "Ensure that keyguard is the one starting transitions, instead of delegating to Core"
+ bug: "364930619"
+ is_fixed_read_only: true
+}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 92c3906..5c0dca2 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6183,6 +6183,10 @@
is enabled and activity is connected to the camera in fullscreen. -->
<bool name="config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled">false</bool>
+ <!-- Which aspect ratio to use when camera compat treatment is enabled and an activity eligible
+ for treatment is connected to the camera. -->
+ <item name="config_windowManagerCameraCompatAspectRatio" format="float" type="dimen">1.0</item>
+
<!-- Docking is a uiMode configuration change and will cause activities to relaunch if it's not
handled. If true, the configuration change will be sent but activities will not be
relaunched upon docking. Apps with desk resources will behave like normal, since they may
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 5f40a6c..807df1b 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4778,6 +4778,7 @@
<java-symbol type="bool" name="config_isCompatFakeFocusEnabled" />
<java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" />
<java-symbol type="bool" name="config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled" />
+ <java-symbol type="dimen" name="config_windowManagerCameraCompatAspectRatio" />
<java-symbol type="bool" name="config_skipActivityRelaunchWhenDocking" />
<java-symbol type="bool" name="config_hideDisplayCutoutWithDisplayArea" />
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index edcea24..e9b137c 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -439,6 +439,27 @@
}
@Test
+ @EnableFlags({Flags.FLAG_UI_RICH_ONGOING, Flags.FLAG_API_RICH_ONGOING})
+ public void testHasPromotableStyle_progress() {
+ Notification n = new Notification.Builder(mContext, "test")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setStyle(new Notification.ProgressStyle())
+ .build();
+ assertThat(n.hasPromotableStyle()).isTrue();
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_UI_RICH_ONGOING})
+ public void testHasPromotableStyle_unknownStyle() {
+ Notification n = new Notification.Builder(mContext, "test")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setStyle(new NotAPlatformStyle())
+ .build();
+
+ assertThat(n.hasPromotableStyle()).isTrue();
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_UI_RICH_ONGOING)
public void testHasPromotableCharacteristics() {
Notification n = new Notification.Builder(mContext, "test")
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index af68442..7f61186 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -592,8 +592,9 @@
public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "onActivityRestartAttempt: %s", task.topActivity);
- if (task.getWindowingMode() != WINDOWING_MODE_PINNED) {
+ "onActivityRestartAttempt: topActivity=%s, wasVisible=%b",
+ task.topActivity, wasVisible);
+ if (task.getWindowingMode() != WINDOWING_MODE_PINNED || !wasVisible) {
return;
}
if (mPipTaskOrganizer.isLaunchToSplit(task)) {
diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt
index 3ed33db..bb0fc41 100644
--- a/libs/appfunctions/api/current.txt
+++ b/libs/appfunctions/api/current.txt
@@ -5,6 +5,11 @@
ctor public AppFunctionManager(android.content.Context);
method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
method @Deprecated public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+ method public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>);
+ method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>);
+ field public static final int APP_FUNCTION_STATE_DEFAULT = 0; // 0x0
+ field public static final int APP_FUNCTION_STATE_DISABLED = 2; // 0x2
+ field public static final int APP_FUNCTION_STATE_ENABLED = 1; // 0x1
}
public abstract class AppFunctionService extends android.app.Service {
@@ -41,6 +46,7 @@
field public static final String PROPERTY_RETURN_VALUE = "returnValue";
field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2
field public static final int RESULT_DENIED = 1; // 0x1
+ field public static final int RESULT_DISABLED = 6; // 0x6
field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3
field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4
field public static final int RESULT_OK = 0; // 0x0
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
index 815fe05..d660926 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
@@ -16,11 +16,18 @@
package com.google.android.appfunctions.sidecar;
+import android.Manifest;
import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.annotation.UserHandleAware;
import android.content.Context;
import android.os.CancellationSignal;
+import android.os.OutcomeReceiver;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -37,6 +44,39 @@
// TODO(b/357551503): Implement get and set enabled app function APIs.
// TODO(b/367329899): Add sidecar library to Android B builds.
public final class AppFunctionManager {
+ /**
+ * The default state of the app function. Call {@link #setAppFunctionEnabled} with this to reset
+ * enabled state to the default value.
+ */
+ public static final int APP_FUNCTION_STATE_DEFAULT = 0;
+
+ /**
+ * The app function is enabled. To enable an app function, call {@link #setAppFunctionEnabled}
+ * with this value.
+ */
+ public static final int APP_FUNCTION_STATE_ENABLED = 1;
+
+ /**
+ * The app function is disabled. To disable an app function, call {@link #setAppFunctionEnabled}
+ * with this value.
+ */
+ public static final int APP_FUNCTION_STATE_DISABLED = 2;
+
+ /**
+ * The enabled state of the app function.
+ *
+ * @hide
+ */
+ @IntDef(
+ prefix = {"APP_FUNCTION_STATE_"},
+ value = {
+ APP_FUNCTION_STATE_DEFAULT,
+ APP_FUNCTION_STATE_ENABLED,
+ APP_FUNCTION_STATE_DISABLED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface EnabledState {}
+
private final android.app.appfunctions.AppFunctionManager mManager;
private final Context mContext;
@@ -111,4 +151,64 @@
new CancellationSignal(),
callback);
}
+
+ /**
+ * Returns a boolean through a callback, indicating whether the app function is enabled.
+ *
+ * <p>* This method can only check app functions owned by the caller, or those where the caller
+ * has visibility to the owner package and holds either the {@link
+ * Manifest.permission#EXECUTE_APP_FUNCTIONS} or {@link
+ * Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permission.
+ *
+ * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors:
+ *
+ * <ul>
+ * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not
+ * have access to it.
+ * </ul>
+ *
+ * @param functionIdentifier the identifier of the app function to check (unique within the
+ * target package) and in most cases, these are automatically generated by the AppFunctions
+ * SDK
+ * @param targetPackage the package name of the app function's owner
+ * @param executor the executor to run the request
+ * @param callback the callback to receive the function enabled check result
+ */
+ public void isAppFunctionEnabled(
+ @NonNull String functionIdentifier,
+ @NonNull String targetPackage,
+ @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Boolean, Exception> callback) {
+ mManager.isAppFunctionEnabled(functionIdentifier, targetPackage, executor, callback);
+ }
+
+ /**
+ * Sets the enabled state of the app function owned by the calling package.
+ *
+ * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors:
+ *
+ * <ul>
+ * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not
+ * have access to it.
+ * </ul>
+ *
+ * @param functionIdentifier the identifier of the app function to enable (unique within the
+ * calling package). In most cases, identifiers are automatically generated by the
+ * AppFunctions SDK
+ * @param newEnabledState the new state of the app function
+ * @param executor the executor to run the callback
+ * @param callback the callback to receive the result of the function enablement. The call was
+ * successful if no exception was thrown.
+ */
+ // Constants in @EnabledState should always mirror those in
+ // android.app.appfunctions.AppFunctionManager.
+ @SuppressLint("WrongConstant")
+ @UserHandleAware
+ public void setAppFunctionEnabled(
+ @NonNull String functionIdentifier,
+ @EnabledState int newEnabledState,
+ @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Void, Exception> callback) {
+ mManager.setAppFunctionEnabled(functionIdentifier, newEnabledState, executor, callback);
+ }
}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
index 60c25fa..c7ce95b 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
@@ -76,6 +76,9 @@
/** The operation was timed out. */
public static final int RESULT_TIMED_OUT = 5;
+ /** The caller tried to execute a disabled app function. */
+ public static final int RESULT_DISABLED = 6;
+
/** The result code of the app function execution. */
@ResultCode private final int mResultCode;
@@ -234,6 +237,7 @@
RESULT_INTERNAL_ERROR,
RESULT_INVALID_ARGUMENT,
RESULT_TIMED_OUT,
+ RESULT_DISABLED
})
@Retention(RetentionPolicy.SOURCE)
public @interface ResultCode {}
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index 06f471e..e77c2a0 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -39,6 +39,7 @@
"SettingsLibDisplayUtils",
"SettingsLibEmergencyNumber",
"SettingsLibEntityHeaderWidgets",
+ "SettingsLibExpandablePreference",
"SettingsLibFooterPreference",
"SettingsLibHelpUtils",
"SettingsLibIllustrationPreference",
diff --git a/packages/SettingsLib/ExpandablePreference/Android.bp b/packages/SettingsLib/ExpandablePreference/Android.bp
new file mode 100644
index 0000000..e6de3f1
--- /dev/null
+++ b/packages/SettingsLib/ExpandablePreference/Android.bp
@@ -0,0 +1,33 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_library {
+ name: "SettingsLibExpandablePreference",
+ use_resource_processor: true,
+ defaults: [
+ "SettingsLintDefaults",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ resource_dirs: ["res"],
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.preference_preference",
+ "SettingsLibSettingsTheme",
+ ],
+ sdk_version: "system_current",
+ min_sdk_version: "21",
+ apex_available: [
+ "//apex_available:platform",
+ ],
+}
diff --git a/packages/SettingsLib/ExpandablePreference/AndroidManifest.xml b/packages/SettingsLib/ExpandablePreference/AndroidManifest.xml
new file mode 100644
index 0000000..7bfa19b
--- /dev/null
+++ b/packages/SettingsLib/ExpandablePreference/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.widget.preference.expandable">
+
+ <uses-sdk android:minSdkVersion="21" />
+
+</manifest>
diff --git a/packages/SettingsLib/ExpandablePreference/res/drawable/settingslib_ic_expand.xml b/packages/SettingsLib/ExpandablePreference/res/drawable/settingslib_ic_expand.xml
new file mode 100644
index 0000000..4502311
--- /dev/null
+++ b/packages/SettingsLib/ExpandablePreference/res/drawable/settingslib_ic_expand.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape
+ android:shape="oval">
+ <size android:width="32dp" android:height="40dp"/>
+ <solid android:color="@color/settingslib_materialColorSurfaceContainer"/>
+ </shape>
+ </item>
+ <item>
+ <vector
+ android:width="32dp"
+ android:height="40dp"
+ android:viewportWidth="32"
+ android:viewportHeight="40">
+ <path
+ android:pathData="M16,23.063L11,18.063L12.063,17L16,20.938L19.938,17L21,18.063L16,23.063Z"
+ android:fillColor="@color/settingslib_materialColorOnSurface"/>
+ </vector>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/packages/SettingsLib/ExpandablePreference/res/layout/settingslib_widget_expandable_icon.xml b/packages/SettingsLib/ExpandablePreference/res/layout/settingslib_widget_expandable_icon.xml
new file mode 100644
index 0000000..f7fefef
--- /dev/null
+++ b/packages/SettingsLib/ExpandablePreference/res/layout/settingslib_widget_expandable_icon.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@+id/expand_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/settingslib_ic_expand"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/ExpandablePreference/src/com/android/settingslib/widget/ExpandablePreference.kt b/packages/SettingsLib/ExpandablePreference/src/com/android/settingslib/widget/ExpandablePreference.kt
new file mode 100644
index 0000000..1b93ebe
--- /dev/null
+++ b/packages/SettingsLib/ExpandablePreference/src/com/android/settingslib/widget/ExpandablePreference.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.ImageView
+import androidx.preference.Preference
+import androidx.preference.PreferenceGroup
+import androidx.preference.PreferenceViewHolder
+import com.android.settingslib.widget.preference.expandable.R
+
+class ExpandablePreference @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0
+) : PreferenceGroup(context, attrs, defStyleAttr, defStyleRes), Expandable {
+
+ private var isExpanded = false
+ private var expandIcon: ImageView? = null
+ private var isDirty = true // Flag to track changes
+
+ init {
+ layoutResource = com.android.settingslib.widget.theme.R.layout.settingslib_expressive_preference
+ widgetLayoutResource = R.layout.settingslib_widget_expandable_icon
+ }
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+
+ holder.isDividerAllowedAbove = false
+ holder.isDividerAllowedBelow = false
+
+ expandIcon = holder.findViewById(R.id.expand_icon) as ImageView?
+
+ updateExpandedState()
+
+ holder.itemView.setOnClickListener { toggleExpansion() }
+ }
+
+ override fun addPreference(preference: Preference): Boolean {
+ preference.isVisible = isExpanded
+ return super.addPreference(preference)
+ }
+
+ override fun onPrepareAddPreference(preference: Preference): Boolean {
+ preference.isVisible = isExpanded
+ return super.onPrepareAddPreference(preference)
+ }
+
+ override fun isExpanded(): Boolean {
+ return isExpanded
+ }
+
+ private fun toggleExpansion() {
+ isExpanded = !isExpanded
+ isDirty = true // Mark as dirty when expansion state changes
+ updateExpandedState()
+ notifyChanged()
+ }
+
+ private fun updateExpandedState() {
+ expandIcon?.rotation = when (isExpanded) {
+ true -> ROTATION_EXPANDED
+ false -> ROTATION_COLLAPSED
+ }
+
+ if (isDirty) {
+ (0 until preferenceCount).forEach { i ->
+ getPreference(i).isVisible = isExpanded
+ }
+ isDirty = false
+ }
+ }
+
+ companion object {
+ private const val ROTATION_EXPANDED = 180f
+ private const val ROTATION_COLLAPSED = 0f
+ }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/Expandable.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/Expandable.kt
new file mode 100644
index 0000000..1f84118
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/Expandable.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.widget
+
+interface Expandable {
+ fun isExpanded(): Boolean
+}
\ No newline at end of file
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index a9d4c89..cb1411b 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -559,6 +559,13 @@
}
flag {
+ name: "volume_redesign"
+ namespace: "systemui"
+ description: "Enables Volume BC25 visuals update"
+ bug: "368308908"
+}
+
+flag {
name: "clipboard_shared_transitions"
namespace: "systemui"
description: "Show shared transitions from clipboard"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt
index e41a7df..a88ad94 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt
@@ -21,11 +21,10 @@
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.SceneScope
-import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.communal.shared.model.CommunalBackgroundType
+import com.android.systemui.communal.ui.viewmodel.CommunalUserActionsViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.util.CommunalColors
import com.android.systemui.dagger.SysUISingleton
@@ -33,38 +32,32 @@
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.Scene
import javax.inject.Inject
-import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
/** The communal scene shows glanceable hub when the device is locked and docked. */
@SysUISingleton
class CommunalScene
@Inject
constructor(
- private val viewModel: CommunalViewModel,
+ private val contentViewModel: CommunalViewModel,
+ actionsViewModelFactory: CommunalUserActionsViewModel.Factory,
private val communalColors: CommunalColors,
private val communalContent: CommunalContent,
) : ExclusiveActivatable(), Scene {
override val key = Scenes.Communal
- override val userActions: Flow<Map<UserAction, UserActionResult>> =
- MutableStateFlow(
- mapOf(
- Swipe(SwipeDirection.End) to Scenes.Lockscreen,
- )
- )
- .asStateFlow()
+ private val actionsViewModel: CommunalUserActionsViewModel = actionsViewModelFactory.create()
+
+ override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions
override suspend fun onActivated(): Nothing {
- awaitCancellation()
+ actionsViewModel.activate()
}
@Composable
override fun SceneScope.Content(modifier: Modifier) {
val backgroundType by
- viewModel.communalBackground.collectAsStateWithLifecycle(
+ contentViewModel.communalBackground.collectAsStateWithLifecycle(
initialValue = CommunalBackgroundType.ANIMATED
)
@@ -72,7 +65,7 @@
backgroundType = backgroundType,
colors = communalColors,
content = communalContent,
- viewModel = viewModel,
+ viewModel = contentViewModel,
modifier = modifier.horizontalNestedScrollToScene(),
)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
index 4162891..6f1349f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
@@ -18,7 +18,6 @@
package com.android.systemui.shade.ui.composable
-import android.view.HapticFeedbackConstants
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@@ -40,20 +39,17 @@
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.LowestZIndexContentPicker
import com.android.compose.animation.scene.SceneScope
import com.android.compose.windowsizeclass.LocalWindowSizeClass
-import com.android.systemui.scene.shared.model.Scenes
/** Renders a lightweight shade UI container, as an overlay. */
@Composable
@@ -62,13 +58,6 @@
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
- val view = LocalView.current
- LaunchedEffect(Unit) {
- if (layoutState.currentTransition?.fromContent == Scenes.Gone) {
- view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START)
- }
- }
-
Box(modifier) {
Scrim(onClicked = onScrimClicked)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index db0fe3e..ef415b1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -16,7 +16,6 @@
package com.android.systemui.shade.ui.composable
-import android.view.HapticFeedbackConstants
import android.view.ViewGroup
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
@@ -60,7 +59,6 @@
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner
-import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.dp
@@ -226,12 +224,6 @@
shadeSession: SaveableSession,
usingCollapsedLandscapeMedia: Boolean,
) {
- val view = LocalView.current
- LaunchedEffect(Unit) {
- if (layoutState.currentTransition?.fromContent == Scenes.Gone) {
- view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START)
- }
- }
val shadeMode by viewModel.shadeMode.collectAsStateWithLifecycle()
when (shadeMode) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 9891025..367faed 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -188,38 +188,47 @@
return createSwipeAnimation(layoutImpl, result, isUpOrLeft, orientation)
}
+ private fun resolveSwipeSource(startedPosition: Offset?): SwipeSource.Resolved? {
+ if (startedPosition == null) return null
+ return layoutImpl.swipeSourceDetector.source(
+ layoutSize = layoutImpl.lastSize,
+ position = startedPosition.round(),
+ density = layoutImpl.density,
+ orientation = orientation,
+ )
+ }
+
+ private fun resolveSwipe(
+ pointersDown: Int,
+ fromSource: SwipeSource.Resolved?,
+ isUpOrLeft: Boolean,
+ ): Swipe.Resolved {
+ return Swipe.Resolved(
+ direction =
+ when (orientation) {
+ Orientation.Horizontal ->
+ if (isUpOrLeft) {
+ SwipeDirection.Resolved.Left
+ } else {
+ SwipeDirection.Resolved.Right
+ }
+
+ Orientation.Vertical ->
+ if (isUpOrLeft) {
+ SwipeDirection.Resolved.Up
+ } else {
+ SwipeDirection.Resolved.Down
+ }
+ },
+ pointerCount = pointersDown,
+ fromSource = fromSource,
+ )
+ }
+
private fun computeSwipes(startedPosition: Offset?, pointersDown: Int): Swipes {
- val fromSource =
- startedPosition?.let { position ->
- layoutImpl.swipeSourceDetector.source(
- layoutImpl.lastSize,
- position.round(),
- layoutImpl.density,
- orientation,
- )
- }
-
- val upOrLeft =
- Swipe.Resolved(
- direction =
- when (orientation) {
- Orientation.Horizontal -> SwipeDirection.Resolved.Left
- Orientation.Vertical -> SwipeDirection.Resolved.Up
- },
- pointerCount = pointersDown,
- fromSource = fromSource,
- )
-
- val downOrRight =
- Swipe.Resolved(
- direction =
- when (orientation) {
- Orientation.Horizontal -> SwipeDirection.Resolved.Right
- Orientation.Vertical -> SwipeDirection.Resolved.Down
- },
- pointerCount = pointersDown,
- fromSource = fromSource,
- )
+ val fromSource = resolveSwipeSource(startedPosition)
+ val upOrLeft = resolveSwipe(pointersDown, fromSource, isUpOrLeft = true)
+ val downOrRight = resolveSwipe(pointersDown, fromSource, isUpOrLeft = false)
return if (fromSource == null) {
Swipes(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt
new file mode 100644
index 0000000..58b59ff
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt
@@ -0,0 +1,223 @@
+/*
+ * 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.communal.ui.viewmodel
+
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
+class CommunalUserActionsViewModelTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private lateinit var underTest: CommunalUserActionsViewModel
+
+ @Before
+ fun setUp() {
+ underTest = kosmos.communalUserActionsViewModel
+ underTest.activateIn(testScope)
+ }
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun actions_singleShade() =
+ testScope.runTest {
+ val actions by collectLastValue(underTest.actions)
+
+ setUpState(
+ isShadeTouchable = true,
+ isDeviceUnlocked = false,
+ shadeMode = ShadeMode.Single,
+ )
+ assertThat(actions).isNotEmpty()
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
+ assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade))
+
+ setUpState(
+ isShadeTouchable = false,
+ isDeviceUnlocked = false,
+ shadeMode = ShadeMode.Single,
+ )
+ assertThat(actions).isEmpty()
+
+ setUpState(
+ isShadeTouchable = true,
+ isDeviceUnlocked = true,
+ shadeMode = ShadeMode.Single,
+ )
+ assertThat(actions).isNotEmpty()
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
+ assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade))
+ }
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun actions_splitShade() =
+ testScope.runTest {
+ val actions by collectLastValue(underTest.actions)
+
+ setUpState(
+ isShadeTouchable = true,
+ isDeviceUnlocked = false,
+ shadeMode = ShadeMode.Split,
+ )
+ assertThat(actions).isNotEmpty()
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
+ assertThat(actions?.get(Swipe.Down))
+ .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade))
+
+ setUpState(
+ isShadeTouchable = false,
+ isDeviceUnlocked = false,
+ shadeMode = ShadeMode.Split,
+ )
+ assertThat(actions).isEmpty()
+
+ setUpState(
+ isShadeTouchable = true,
+ isDeviceUnlocked = true,
+ shadeMode = ShadeMode.Split,
+ )
+ assertThat(actions).isNotEmpty()
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
+ assertThat(actions?.get(Swipe.Down))
+ .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade))
+ }
+
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun actions_dualShade() =
+ testScope.runTest {
+ val actions by collectLastValue(underTest.actions)
+
+ setUpState(
+ isShadeTouchable = true,
+ isDeviceUnlocked = false,
+ shadeMode = ShadeMode.Dual,
+ )
+ assertThat(actions).isNotEmpty()
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
+ assertThat(actions?.get(Swipe.Down))
+ .isEqualTo(UserActionResult(Overlays.NotificationsShade))
+
+ setUpState(
+ isShadeTouchable = false,
+ isDeviceUnlocked = false,
+ shadeMode = ShadeMode.Dual,
+ )
+ assertThat(actions).isEmpty()
+
+ setUpState(isShadeTouchable = true, isDeviceUnlocked = true, shadeMode = ShadeMode.Dual)
+ assertThat(actions).isNotEmpty()
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
+ assertThat(actions?.get(Swipe.Down))
+ .isEqualTo(UserActionResult(Overlays.NotificationsShade))
+ }
+
+ private fun TestScope.setUpState(
+ isShadeTouchable: Boolean,
+ isDeviceUnlocked: Boolean,
+ shadeMode: ShadeMode,
+ ) {
+ if (isShadeTouchable) {
+ kosmos.powerInteractor.setAwakeForTest()
+ } else {
+ kosmos.powerInteractor.setAsleepForTest()
+ }
+
+ if (isDeviceUnlocked) {
+ unlockDevice()
+ } else {
+ lockDevice()
+ }
+
+ if (shadeMode == ShadeMode.Dual) {
+ assertThat(DualShade.isEnabled).isTrue()
+ } else {
+ assertThat(DualShade.isEnabled).isFalse()
+ kosmos.fakeShadeRepository.setShadeLayoutWide(shadeMode == ShadeMode.Split)
+ }
+ runCurrent()
+ }
+
+ private fun TestScope.lockDevice() {
+ val deviceUnlockStatus by
+ collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus)
+
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+ kosmos.sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+ runCurrent()
+ }
+
+ private fun TestScope.unlockDevice() {
+ val deviceUnlockStatus by
+ collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus)
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+ kosmos.sceneInteractor.changeScene(Scenes.Gone, "reason")
+ runCurrent()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
index a330cf0..fb1bf28 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
@@ -147,7 +147,7 @@
}
}
- private fun expectedLeftDestination(
+ private fun expectedStartDestination(
isCommunalAvailable: Boolean,
isShadeTouchable: Boolean,
): SceneKey? {
@@ -246,17 +246,17 @@
)
)
- val leftScene by
+ val startScene by
collectLastValue(
- (userActions?.get(Swipe.Left) as? UserActionResult.ChangeScene)?.toScene?.let {
- scene ->
- kosmos.sceneInteractor.resolveSceneFamily(scene)
- } ?: flowOf(null)
+ (userActions?.get(Swipe.Start) as? UserActionResult.ChangeScene)
+ ?.toScene
+ ?.let { scene -> kosmos.sceneInteractor.resolveSceneFamily(scene) }
+ ?: flowOf(null)
)
- assertThat(leftScene)
+ assertThat(startScene)
.isEqualTo(
- expectedLeftDestination(
+ expectedStartDestination(
isCommunalAvailable = isCommunalAvailable,
isShadeTouchable = isShadeTouchable,
)
@@ -341,17 +341,17 @@
)
)
- val leftScene by
+ val startScene by
collectLastValue(
- (userActions?.get(Swipe.Left) as? UserActionResult.ChangeScene)?.toScene?.let {
- scene ->
- kosmos.sceneInteractor.resolveSceneFamily(scene)
- } ?: flowOf(null)
+ (userActions?.get(Swipe.Start) as? UserActionResult.ChangeScene)
+ ?.toScene
+ ?.let { scene -> kosmos.sceneInteractor.resolveSceneFamily(scene) }
+ ?: flowOf(null)
)
- assertThat(leftScene)
+ assertThat(startScene)
.isEqualTo(
- expectedLeftDestination(
+ expectedStartDestination(
isCommunalAvailable = isCommunalAvailable,
isShadeTouchable = isShadeTouchable,
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt
index 57a9377..acd69af 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt
@@ -82,11 +82,11 @@
(0u..31u).map { bitfield ->
TestData(
statusBarState,
- expanded = (bitfield or 1u) == 1u,
- stackScrollerOverScrolling = (bitfield or 2u) == 1u,
- bypassEnabled = (bitfield or 4u) == 1u,
- transitioningToFullShade = (bitfield or 8u) == 1u,
- inSplitShade = (bitfield or 16u) == 1u,
+ expanded = (bitfield and 1u) == 1u,
+ stackScrollerOverScrolling = (bitfield and 2u) == 2u,
+ bypassEnabled = (bitfield and 4u) == 4u,
+ transitioningToFullShade = (bitfield and 8u) == 8u,
+ inSplitShade = (bitfield and 16u) == 16u,
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index c9e958d..d2bf9b88 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -50,9 +50,7 @@
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
-import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
import com.android.systemui.scene.domain.startable.sceneContainerStartable
-import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
@@ -185,7 +183,6 @@
fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
testScope.runTest {
val actions by collectLastValue(kosmos.shadeUserActionsViewModel.actions)
- val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
kosmos.setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true)
kosmos.assertCurrentScene(Scenes.Lockscreen)
@@ -195,9 +192,8 @@
val upDestinationSceneKey =
(actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene
- assertThat(upDestinationSceneKey).isEqualTo(SceneFamilies.Home)
- assertThat(homeScene).isEqualTo(Scenes.Lockscreen)
- kosmos.emulateUserDrivenTransition(to = homeScene)
+ assertThat(upDestinationSceneKey).isEqualTo(Scenes.Lockscreen)
+ kosmos.emulateUserDrivenTransition(to = Scenes.Lockscreen)
}
@Test
@@ -205,7 +201,6 @@
testScope.runTest {
val actions by collectLastValue(kosmos.shadeUserActionsViewModel.actions)
val canSwipeToEnter by collectLastValue(kosmos.deviceEntryInteractor.canSwipeToEnter)
- val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
kosmos.setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true)
@@ -222,9 +217,8 @@
val upDestinationSceneKey =
(actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene
- assertThat(upDestinationSceneKey).isEqualTo(SceneFamilies.Home)
- assertThat(homeScene).isEqualTo(Scenes.Gone)
- kosmos.emulateUserDrivenTransition(to = homeScene)
+ assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone)
+ kosmos.emulateUserDrivenTransition(to = Scenes.Gone)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt
new file mode 100644
index 0000000..664315d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt
@@ -0,0 +1,233 @@
+/*
+ * 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.scene.ui.viewmodel
+
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.view.HapticFeedbackConstants
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.ObservableTransitionState.Transition.ShowOrHideOverlay
+import com.android.compose.animation.scene.OverlayKey
+import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.haptics.msdl.fakeMSDLPlayer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.sceneContainerHapticsViewModelFactory
+import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.testKosmos
+import com.google.android.msdl.data.model.MSDLToken
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyZeroInteractions
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
+class SceneContainerHapticsViewModelTest() : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
+ private val msdlPlayer = kosmos.fakeMSDLPlayer
+ private val view = mock<View>()
+
+ private lateinit var underTest: SceneContainerHapticsViewModel
+
+ @Before
+ fun setup() {
+ underTest = kosmos.sceneContainerHapticsViewModelFactory.create(view)
+ underTest.activateIn(testScope)
+ }
+
+ @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+ @DisableFlags(Flags.FLAG_DUAL_SHADE)
+ @Test
+ fun onValidSceneTransition_withMSDL_playsMSDLShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN a valid scene transition to play haptics
+ val validTransition = createTransitionState(from = Scenes.Gone, to = Scenes.Shade)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(validTransition))
+ runCurrent()
+
+ // THEN the expected token plays without interaction properties
+ assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.SWIPE_THRESHOLD_INDICATOR)
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+ }
+
+ @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+ @DisableFlags(Flags.FLAG_DUAL_SHADE)
+ @Test
+ fun onInValidSceneTransition_withMSDL_doesNotPlayMSDLShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN an invalid scene transition to play haptics
+ val invalidTransition = createTransitionState(from = Scenes.Shade, to = Scenes.Gone)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition))
+ runCurrent()
+
+ // THEN the no token plays with no interaction properties
+ assertThat(msdlPlayer.latestTokenPlayed).isNull()
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+ }
+
+ @DisableFlags(Flags.FLAG_DUAL_SHADE, Flags.FLAG_MSDL_FEEDBACK)
+ @Test
+ fun onValidSceneTransition_withoutMSDL_playsHapticConstantForShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN a valid scene transition to play haptics
+ val validTransition = createTransitionState(from = Scenes.Gone, to = Scenes.Shade)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(validTransition))
+ runCurrent()
+
+ // THEN the expected haptic feedback constant plays
+ verify(view).performHapticFeedback(eq(HapticFeedbackConstants.GESTURE_START))
+ }
+
+ @DisableFlags(Flags.FLAG_DUAL_SHADE, Flags.FLAG_MSDL_FEEDBACK)
+ @Test
+ fun onInValidSceneTransition_withoutMSDL_doesNotPlayHapticConstantForShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN an invalid scene transition to play haptics
+ val invalidTransition = createTransitionState(from = Scenes.Shade, to = Scenes.Gone)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition))
+ runCurrent()
+
+ // THEN the view does not play a haptic feedback constant
+ verifyZeroInteractions(view)
+ }
+
+ @EnableFlags(Flags.FLAG_MSDL_FEEDBACK, Flags.FLAG_DUAL_SHADE)
+ @Test
+ fun onValidOverlayTransition_withMSDL_playsMSDLShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN a valid scene transition to play haptics
+ val validTransition =
+ createTransitionState(from = Scenes.Gone, to = Overlays.NotificationsShade)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(validTransition))
+ runCurrent()
+
+ // THEN the expected token plays without interaction properties
+ assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.SWIPE_THRESHOLD_INDICATOR)
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+ }
+
+ @EnableFlags(Flags.FLAG_MSDL_FEEDBACK, Flags.FLAG_DUAL_SHADE)
+ @Test
+ fun onInValidOverlayTransition_withMSDL_doesNotPlayMSDLShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN an invalid scene transition to play haptics
+ val invalidTransition =
+ createTransitionState(from = Scenes.Bouncer, to = Overlays.NotificationsShade)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition))
+ runCurrent()
+
+ // THEN the no token plays with no interaction properties
+ assertThat(msdlPlayer.latestTokenPlayed).isNull()
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+ }
+
+ @EnableFlags(Flags.FLAG_DUAL_SHADE)
+ @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
+ @Test
+ fun onValidOverlayTransition_withoutMSDL_playsHapticConstantForShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN a valid scene transition to play haptics
+ val validTransition =
+ createTransitionState(from = Scenes.Gone, to = Overlays.NotificationsShade)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(validTransition))
+ runCurrent()
+
+ // THEN the expected haptic feedback constant plays
+ verify(view).performHapticFeedback(eq(HapticFeedbackConstants.GESTURE_START))
+ }
+
+ @EnableFlags(Flags.FLAG_DUAL_SHADE)
+ @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
+ @Test
+ fun onInValidOverlayTransition_withoutMSDL_doesNotPlayHapticConstantForShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN an invalid scene transition to play haptics
+ val invalidTransition =
+ createTransitionState(from = Scenes.Bouncer, to = Overlays.NotificationsShade)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition))
+ runCurrent()
+
+ // THEN the view does not play a haptic feedback constant
+ verifyZeroInteractions(view)
+ }
+
+ private fun createTransitionState(from: SceneKey, to: ContentKey) =
+ when (to) {
+ is SceneKey ->
+ ObservableTransitionState.Transition(
+ fromScene = from,
+ toScene = to,
+ currentScene = flowOf(from),
+ progress = MutableStateFlow(0.2f),
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true),
+ )
+ is OverlayKey ->
+ ShowOrHideOverlay(
+ overlay = to,
+ fromContent = from,
+ toContent = to,
+ currentScene = from,
+ currentOverlays = sceneInteractor.currentOverlays,
+ progress = MutableStateFlow(0.2f),
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true),
+ previewProgress = flowOf(0f),
+ isInPreviewStage = flowOf(false),
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index e60e742..a37f511 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -21,23 +21,21 @@
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.view.MotionEvent
+import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.DefaultEdgeDetector
import com.android.systemui.SysuiTestCase
-import com.android.systemui.classifier.domain.interactor.falsingInteractor
import com.android.systemui.classifier.fakeFalsingManager
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.power.data.repository.fakePowerRepository
-import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.fakeOverlaysByKeys
import com.android.systemui.scene.sceneContainerConfig
-import com.android.systemui.scene.sceneContainerGestureFilterFactory
-import com.android.systemui.scene.shared.logger.sceneLogger
+import com.android.systemui.scene.sceneContainerViewModelFactory
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
@@ -72,6 +70,7 @@
private val fakeShadeRepository by lazy { kosmos.fakeShadeRepository }
private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig }
private val falsingManager by lazy { kosmos.fakeFalsingManager }
+ private val view = mock<View>()
private lateinit var underTest: SceneContainerViewModel
@@ -81,16 +80,10 @@
@Before
fun setUp() {
underTest =
- SceneContainerViewModel(
- sceneInteractor = sceneInteractor,
- falsingInteractor = kosmos.falsingInteractor,
- powerInteractor = kosmos.powerInteractor,
- shadeInteractor = kosmos.shadeInteractor,
- splitEdgeDetector = kosmos.splitEdgeDetector,
- logger = kosmos.sceneLogger,
- gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory,
- displayId = kosmos.displayTracker.defaultDisplayId,
- motionEventHandlerReceiver = { motionEventHandler ->
+ kosmos.sceneContainerViewModelFactory.create(
+ view,
+ kosmos.displayTracker.defaultDisplayId,
+ { motionEventHandler ->
this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler
},
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt
index 9f3e126e..15d6881 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt
@@ -41,6 +41,7 @@
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
@@ -76,6 +77,7 @@
@Before
fun setUp() {
+ kosmos.sceneContainerStartable.start()
underTest.activateIn(testScope)
}
@@ -232,6 +234,20 @@
.isEmpty()
}
+ @Test
+ fun upTransitionSceneKey_backToCommunal() =
+ testScope.runTest {
+ val actions by collectLastValue(underTest.actions)
+ val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ kosmos.sceneInteractor.changeScene(Scenes.Communal, "")
+ assertThat(currentScene).isEqualTo(Scenes.Communal)
+ kosmos.sceneInteractor.changeScene(Scenes.Shade, "")
+ assertThat(currentScene).isEqualTo(Scenes.Shade)
+
+ assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Communal))
+ }
+
private fun TestScope.setDeviceEntered(isEntered: Boolean) {
if (isEntered) {
// Unlock the device marking the device has entered.
diff --git a/packages/SystemUI/src/com/android/systemui/CoreStartable.java b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
index 55ccaa6..92bc95a 100644
--- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java
+++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
@@ -70,4 +70,12 @@
* {@link #onBootCompleted()} will never be called before {@link #start()}. */
default void onBootCompleted() {
}
+
+ /** No op implementation that can be used when feature flagging on the Dagger Module level. */
+ CoreStartable NOP = new Nop();
+
+ class Nop implements CoreStartable {
+ @Override
+ public void start() {}
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt
new file mode 100644
index 0000000..e35fdfe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.communal.ui.viewmodel
+
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
+import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.shade.ui.viewmodel.dualShadeActions
+import com.android.systemui.shade.ui.viewmodel.singleShadeActions
+import com.android.systemui.shade.ui.viewmodel.splitShadeActions
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/** Provides scene container user actions and results. */
+class CommunalUserActionsViewModel
+@AssistedInject
+constructor(
+ private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
+ private val shadeInteractor: ShadeInteractor,
+) : UserActionsViewModel() {
+
+ override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
+ shadeInteractor.isShadeTouchable
+ .flatMapLatestConflated { isShadeTouchable ->
+ if (!isShadeTouchable) {
+ flowOf(emptyMap())
+ } else {
+ combine(
+ deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked },
+ shadeInteractor.shadeMode,
+ ) { isDeviceUnlocked, shadeMode ->
+ buildList {
+ val bouncerOrGone =
+ if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer
+ add(Swipe.Up to bouncerOrGone)
+
+ // "Home" is either Lockscreen, or Gone - if the device is entered.
+ add(Swipe.End to SceneFamilies.Home)
+
+ addAll(
+ when (shadeMode) {
+ ShadeMode.Single -> singleShadeActions()
+ ShadeMode.Split -> splitShadeActions()
+ ShadeMode.Dual -> dualShadeActions()
+ }
+ )
+ }
+ .associate { it }
+ }
+ }
+ }
+ .collect { setActions(it) }
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): CommunalUserActionsViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt
index 3b266f9..6f29004 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt
@@ -18,20 +18,18 @@
package com.android.systemui.keyguard.ui.viewmodel
-import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
-import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.shade.ui.viewmodel.dualShadeActions
+import com.android.systemui.shade.ui.viewmodel.singleShadeActions
+import com.android.systemui.shade.ui.viewmodel.splitShadeActions
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -62,7 +60,7 @@
) { isDeviceUnlocked, isCommunalAvailable, shadeMode ->
buildList {
if (isCommunalAvailable) {
- add(Swipe.Left to Scenes.Communal)
+ add(Swipe.Start to Scenes.Communal)
}
add(Swipe.Up to if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer)
@@ -81,45 +79,6 @@
.collect { setActions(it) }
}
- private fun singleShadeActions(): Array<Pair<UserAction, UserActionResult>> {
- return arrayOf(
- // Swiping down, not from the edge, always goes to shade.
- Swipe.Down to Scenes.Shade,
- swipeDown(pointerCount = 2) to Scenes.Shade,
- // Swiping down from the top edge goes to QS.
- swipeDownFromTop(pointerCount = 1) to Scenes.QuickSettings,
- swipeDownFromTop(pointerCount = 2) to Scenes.QuickSettings,
- )
- }
-
- private fun splitShadeActions(): Array<Pair<UserAction, UserActionResult>> {
- val splitShadeSceneKey = UserActionResult(Scenes.Shade, ToSplitShade)
- return arrayOf(
- // Swiping down, not from the edge, always goes to shade.
- Swipe.Down to splitShadeSceneKey,
- swipeDown(pointerCount = 2) to splitShadeSceneKey,
- // Swiping down from the top edge goes to QS.
- swipeDownFromTop(pointerCount = 1) to splitShadeSceneKey,
- swipeDownFromTop(pointerCount = 2) to splitShadeSceneKey,
- )
- }
-
- private fun dualShadeActions(): Array<Pair<UserAction, UserActionResult>> {
- return arrayOf(
- Swipe.Down to Overlays.NotificationsShade,
- Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to
- Overlays.QuickSettingsShade,
- )
- }
-
- private fun swipeDownFromTop(pointerCount: Int): Swipe {
- return Swipe(SwipeDirection.Down, fromSource = Edge.Top, pointerCount = pointerCount)
- }
-
- private fun swipeDown(pointerCount: Int): Swipe {
- return Swipe(SwipeDirection.Down, pointerCount = pointerCount)
- }
-
@AssistedFactory
interface Factory {
fun create(): LockscreenUserActionsViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
index a7e7d8b..a8be580 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
@@ -108,7 +108,11 @@
traceName = "SceneWindowRootViewBinder",
minWindowLifecycleState = WindowLifecycleState.ATTACHED,
factory = {
- viewModelFactory.create(view.context.displayId, motionEventHandlerReceiver)
+ viewModelFactory.create(
+ view,
+ view.context.displayId,
+ motionEventHandlerReceiver,
+ )
},
) { viewModel ->
try {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt
index 5ff507a..fc172e8 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt
@@ -16,16 +16,13 @@
package com.android.systemui.scene.ui.viewmodel
-import com.android.compose.animation.scene.Edge
-import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
-import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.shade.ui.viewmodel.dualShadeActions
+import com.android.systemui.shade.ui.viewmodel.singleShadeActions
+import com.android.systemui.shade.ui.viewmodel.splitShadeActions
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -36,41 +33,21 @@
override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
shadeInteractor.shadeMode.collect { shadeMode ->
setActions(
- when (shadeMode) {
- ShadeMode.Single -> singleShadeActions()
- ShadeMode.Split -> splitShadeActions()
- ShadeMode.Dual -> dualShadeActions()
- }
+ buildList {
+ addAll(
+ when (shadeMode) {
+ ShadeMode.Single ->
+ singleShadeActions(requireTwoPointersForTopEdgeForQs = true)
+ ShadeMode.Split -> splitShadeActions()
+ ShadeMode.Dual -> dualShadeActions()
+ }
+ )
+ }
+ .associate { it }
)
}
}
- private fun singleShadeActions(): Map<UserAction, UserActionResult> {
- return mapOf(
- Swipe.Down to Scenes.Shade,
- swipeDownFromTopWithTwoFingers() to Scenes.QuickSettings,
- )
- }
-
- private fun splitShadeActions(): Map<UserAction, UserActionResult> {
- return mapOf(
- Swipe.Down to UserActionResult(Scenes.Shade, ToSplitShade),
- swipeDownFromTopWithTwoFingers() to UserActionResult(Scenes.Shade, ToSplitShade),
- )
- }
-
- private fun dualShadeActions(): Map<UserAction, UserActionResult> {
- return mapOf(
- Swipe.Down to Overlays.NotificationsShade,
- Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to
- Overlays.QuickSettingsShade,
- )
- }
-
- private fun swipeDownFromTopWithTwoFingers(): UserAction {
- return Swipe(direction = SwipeDirection.Down, pointerCount = 2, fromSource = Edge.Top)
- }
-
@AssistedFactory
interface Factory {
fun create(): GoneUserActionsViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModel.kt
new file mode 100644
index 0000000..4ef8e0f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModel.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.scene.ui.viewmodel
+
+import android.view.HapticFeedbackConstants
+import android.view.View
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.google.android.msdl.data.model.MSDLToken
+import com.google.android.msdl.domain.MSDLPlayer
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+/**
+ * Models haptics UI state for the scene container.
+ *
+ * This model gets a [View] to play haptics using the [View.performHapticFeedback] API. This should
+ * be the only purpose of this reference.
+ */
+class SceneContainerHapticsViewModel
+@AssistedInject
+constructor(
+ @Assisted private val view: View,
+ sceneInteractor: SceneInteractor,
+ shadeInteractor: ShadeInteractor,
+ private val msdlPlayer: MSDLPlayer,
+) : ExclusiveActivatable() {
+
+ /** Should haptics be played by pulling down the shade */
+ private val isShadePullHapticsRequired: Flow<Boolean> =
+ combine(shadeInteractor.isUserInteracting, sceneInteractor.transitionState) {
+ interacting,
+ transitionState ->
+ interacting && transitionState.isValidForShadePullHaptics()
+ }
+ .distinctUntilChanged()
+
+ override suspend fun onActivated(): Nothing {
+ isShadePullHapticsRequired.collect { playShadePullHaptics ->
+ if (!playShadePullHaptics) return@collect
+
+ if (Flags.msdlFeedback()) {
+ msdlPlayer.playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR)
+ } else {
+ view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START)
+ }
+ }
+ awaitCancellation()
+ }
+
+ private fun ObservableTransitionState.isValidForShadePullHaptics(): Boolean {
+ val validOrigin =
+ isTransitioning(from = Scenes.Gone) || isTransitioning(from = Scenes.Lockscreen)
+ val validDestination =
+ isTransitioning(to = Scenes.Shade) ||
+ isTransitioning(to = Scenes.QuickSettings) ||
+ isTransitioning(to = Overlays.QuickSettingsShade) ||
+ isTransitioning(to = Overlays.NotificationsShade)
+ return validOrigin && validDestination
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(view: View): SceneContainerHapticsViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index 0bf2d49..f505385 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -17,8 +17,10 @@
package com.android.systemui.scene.ui.viewmodel
import android.view.MotionEvent
+import android.view.View
import androidx.compose.runtime.getValue
import androidx.compose.ui.geometry.Offset
+import com.android.app.tracing.coroutines.launch
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.DefaultEdgeDetector
import com.android.compose.animation.scene.ObservableTransitionState
@@ -60,6 +62,8 @@
private val splitEdgeDetector: SplitEdgeDetector,
private val logger: SceneLogger,
gestureFilterFactory: SceneContainerGestureFilter.Factory,
+ hapticsViewModelFactory: SceneContainerHapticsViewModel.Factory,
+ @Assisted view: View,
@Assisted displayId: Int,
@Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
) : ExclusiveActivatable() {
@@ -72,6 +76,8 @@
/** Whether the container is visible. */
val isVisible: Boolean by hydrator.hydratedStateOf("isVisible", sceneInteractor.isVisible)
+ private val hapticsViewModel = hapticsViewModelFactory.create(view)
+
/**
* The [SwipeSourceDetector] to use for defining which edges of the screen can be defined in the
* [UserAction]s for this container.
@@ -107,6 +113,7 @@
coroutineScope {
launch { hydrator.activate() }
launch { gestureFilter.activate() }
+ launch("SceneContainerHapticsViewModel") { hapticsViewModel.activate() }
}
awaitCancellation()
} finally {
@@ -281,6 +288,7 @@
@AssistedFactory
interface Factory {
fun create(
+ view: View,
displayId: Int,
motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
): SceneContainerViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 0c05dbd..5896659 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -23,6 +23,7 @@
import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
import static com.android.keyguard.KeyguardClockSwitch.LARGE;
import static com.android.keyguard.KeyguardClockSwitch.SMALL;
+import static com.android.systemui.Flags.msdlFeedback;
import static com.android.systemui.Flags.predictiveBackAnimateShade;
import static com.android.systemui.Flags.smartspaceRelocateToBottom;
import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
@@ -236,6 +237,9 @@
import dalvik.annotation.optimization.NeverCompile;
+import com.google.android.msdl.data.model.MSDLToken;
+import com.google.android.msdl.domain.MSDLPlayer;
+
import kotlin.Unit;
import kotlinx.coroutines.CoroutineDispatcher;
@@ -312,6 +316,7 @@
private final StatusBarStateListener mStatusBarStateListener = new StatusBarStateListener();
private final NotificationPanelView mView;
private final VibratorHelper mVibratorHelper;
+ private final MSDLPlayer mMSDLPlayer;
private final MetricsLogger mMetricsLogger;
private final ConfigurationController mConfigurationController;
private final Provider<FlingAnimationUtils.Builder> mFlingAnimationUtilsBuilder;
@@ -777,7 +782,8 @@
SplitShadeStateController splitShadeStateController,
PowerInteractor powerInteractor,
KeyguardClockPositionAlgorithm keyguardClockPositionAlgorithm,
- NaturalScrollingSettingObserver naturalScrollingSettingObserver) {
+ NaturalScrollingSettingObserver naturalScrollingSettingObserver,
+ MSDLPlayer msdlPlayer) {
SceneContainerFlag.assertInLegacyMode();
keyguardStateController.addCallback(new KeyguardStateController.Callback() {
@Override
@@ -855,6 +861,7 @@
mNotificationsDragEnabled = mResources.getBoolean(
R.bool.config_enableNotificationShadeDrag);
mVibratorHelper = vibratorHelper;
+ mMSDLPlayer = msdlPlayer;
mVibrateOnOpening = mResources.getBoolean(R.bool.config_vibrateOnIconAnimation);
mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
mSystemClock = systemClock;
@@ -2911,7 +2918,7 @@
}
if (!mStatusBarStateController.isDozing()) {
- mVibratorHelper.performHapticFeedback(mView, HapticFeedbackConstants.REJECT);
+ performHapticFeedback(HapticFeedbackConstants.REJECT);
}
}
@@ -3279,7 +3286,20 @@
}
public void performHapticFeedback(int constant) {
- mVibratorHelper.performHapticFeedback(mView, constant);
+ if (msdlFeedback()) {
+ MSDLToken token;
+ switch (constant) {
+ case HapticFeedbackConstants.GESTURE_START ->
+ token = MSDLToken.SWIPE_THRESHOLD_INDICATOR;
+ case HapticFeedbackConstants.REJECT -> token = MSDLToken.FAILURE;
+ default -> token = null;
+ }
+ if (token != null) {
+ mMSDLPlayer.playToken(token, null);
+ }
+ } else {
+ mVibratorHelper.performHapticFeedback(mView, constant);
+ }
}
private class ShadeHeadsUpTrackerImpl implements ShadeHeadsUpTracker {
@@ -3736,10 +3756,7 @@
private void maybeVibrateOnOpening(boolean openingWithTouch) {
if (mVibrateOnOpening && mBarState != KEYGUARD && mBarState != SHADE_LOCKED) {
if (!openingWithTouch || !mHasVibratedOnOpen) {
- mVibratorHelper.performHapticFeedback(
- mView,
- HapticFeedbackConstants.GESTURE_START
- );
+ performHapticFeedback(HapticFeedbackConstants.GESTURE_START);
mHasVibratedOnOpen = true;
mShadeLog.v("Vibrating on opening, mHasVibratedOnOpen=true");
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt
new file mode 100644
index 0000000..65b6231
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.shade.ui.viewmodel
+
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+
+/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the single shade. */
+fun singleShadeActions(
+ requireTwoPointersForTopEdgeForQs: Boolean = false
+): Array<Pair<UserAction, UserActionResult>> {
+ return arrayOf(
+ // Swiping down, not from the edge, always goes to shade.
+ Swipe.Down to Scenes.Shade,
+ swipeDown(pointerCount = 2) to Scenes.Shade,
+
+ // Swiping down from the top edge.
+ swipeDownFromTop(pointerCount = 1) to
+ if (requireTwoPointersForTopEdgeForQs) {
+ Scenes.Shade
+ } else {
+ Scenes.QuickSettings
+ },
+ swipeDownFromTop(pointerCount = 2) to Scenes.QuickSettings,
+ )
+}
+
+/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the split shade. */
+fun splitShadeActions(): Array<Pair<UserAction, UserActionResult>> {
+ val splitShadeSceneKey = UserActionResult(Scenes.Shade, ToSplitShade)
+ return arrayOf(
+ // Swiping down, not from the edge, always goes to shade.
+ Swipe.Down to splitShadeSceneKey,
+ swipeDown(pointerCount = 2) to splitShadeSceneKey,
+ // Swiping down from the top edge goes to QS.
+ swipeDownFromTop(pointerCount = 1) to splitShadeSceneKey,
+ swipeDownFromTop(pointerCount = 2) to splitShadeSceneKey,
+ )
+}
+
+/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the dual shade. */
+fun dualShadeActions(): Array<Pair<UserAction, UserActionResult>> {
+ return arrayOf(
+ Swipe.Down to Overlays.NotificationsShade,
+ Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to
+ Overlays.QuickSettingsShade,
+ )
+}
+
+private fun swipeDownFromTop(pointerCount: Int): Swipe {
+ return Swipe(SwipeDirection.Down, fromSource = Edge.Top, pointerCount = pointerCount)
+}
+
+private fun swipeDown(pointerCount: Int): Swipe {
+ return Swipe(SwipeDirection.Down, pointerCount = pointerCount)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt
index f8a850a..cc6e8c2 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt
@@ -16,11 +16,13 @@
package com.android.systemui.shade.ui.viewmodel
+import com.android.app.tracing.coroutines.flow.map
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.qs.ui.adapter.QSSceneAdapter
+import com.android.systemui.scene.domain.interactor.SceneBackInteractor
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
@@ -41,21 +43,23 @@
constructor(
private val qsSceneAdapter: QSSceneAdapter,
private val shadeInteractor: ShadeInteractor,
+ private val sceneBackInteractor: SceneBackInteractor,
) : UserActionsViewModel() {
override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
combine(
shadeInteractor.shadeMode,
qsSceneAdapter.isCustomizerShowing,
- ) { shadeMode, isCustomizerShowing ->
+ sceneBackInteractor.backScene.map { it ?: SceneFamilies.Home },
+ ) { shadeMode, isCustomizerShowing, backScene ->
buildMap<UserAction, UserActionResult> {
if (!isCustomizerShowing) {
set(
Swipe(SwipeDirection.Up),
UserActionResult(
- SceneFamilies.Home,
- ToSplitShade.takeIf { shadeMode is ShadeMode.Split }
- )
+ backScene,
+ ToSplitShade.takeIf { shadeMode is ShadeMode.Split },
+ ),
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/CommandQueueInitializer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/CommandQueueInitializer.kt
new file mode 100644
index 0000000..57c8bc6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/CommandQueueInitializer.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.core
+
+import android.app.StatusBarManager
+import android.content.Context
+import android.os.Binder
+import android.os.RemoteException
+import android.view.WindowInsets
+import com.android.internal.statusbar.IStatusBarService
+import com.android.internal.statusbar.RegisterStatusBarResult
+import com.android.systemui.CoreStartable
+import com.android.systemui.InitController
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.navigationbar.NavigationBarController
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
+import dagger.Lazy
+import javax.inject.Inject
+
+@SysUISingleton
+class CommandQueueInitializer
+@Inject
+constructor(
+ private val context: Context,
+ private val commandQueue: CommandQueue,
+ private val commandQueueCallbacksLazy: Lazy<CommandQueue.Callbacks>,
+ private val statusBarModeRepository: StatusBarModeRepositoryStore,
+ private val initController: InitController,
+ private val barService: IStatusBarService,
+ private val navigationBarController: NavigationBarController,
+) : CoreStartable {
+
+ override fun start() {
+ StatusBarSimpleFragment.assertInNewMode()
+ val result: RegisterStatusBarResult =
+ try {
+ barService.registerStatusBar(commandQueue)
+ } catch (ex: RemoteException) {
+ ex.rethrowFromSystemServer()
+ return
+ }
+
+ createNavigationBar(result)
+
+ if ((result.mTransientBarTypes and WindowInsets.Type.statusBars()) != 0) {
+ statusBarModeRepository.defaultDisplay.showTransient()
+ }
+ val displayId = context.display.displayId
+ val commandQueueCallbacks = commandQueueCallbacksLazy.get()
+ commandQueueCallbacks.onSystemBarAttributesChanged(
+ displayId,
+ result.mAppearance,
+ result.mAppearanceRegions,
+ result.mNavbarColorManagedByIme,
+ result.mBehavior,
+ result.mRequestedVisibleTypes,
+ result.mPackageName,
+ result.mLetterboxDetails,
+ )
+
+ // StatusBarManagerService has a back up of IME token and it's restored here.
+ commandQueueCallbacks.setImeWindowStatus(
+ displayId,
+ result.mImeWindowVis,
+ result.mImeBackDisposition,
+ result.mShowImeSwitcher,
+ )
+
+ // Set up the initial icon state
+ val numIcons: Int = result.mIcons.size
+ for (i in 0 until numIcons) {
+ commandQueue.setIcon(result.mIcons.keyAt(i), result.mIcons.valueAt(i))
+ }
+
+ // set the initial view visibility
+ val disabledFlags1 = result.mDisabledFlags1
+ val disabledFlags2 = result.mDisabledFlags2
+ initController.addPostInitTask {
+ commandQueue.disable(displayId, disabledFlags1, disabledFlags2, /* animate= */ false)
+ try {
+ // NOTE(b/262059863): Force-update the disable flags after applying the flags
+ // returned from registerStatusBar(). The result's disabled flags may be stale
+ // if StatusBarManager's disabled flags are updated between registering the bar
+ // and this handling this post-init task. We force an update in this case, and use a
+ // new token to not conflict with any other disabled flags already requested by
+ // SysUI
+ val token = Binder()
+ barService.disable(StatusBarManager.DISABLE_HOME, token, context.packageName)
+ barService.disable(0, token, context.packageName)
+ } catch (ex: RemoteException) {
+ ex.rethrowFromSystemServer()
+ }
+ }
+ }
+
+ private fun createNavigationBar(result: RegisterStatusBarResult) {
+ navigationBarController.createNavigationBars(/* includeDefaultDisplay= */ true, result)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt
new file mode 100644
index 0000000..8bd990b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt
@@ -0,0 +1,248 @@
+/*
+ * 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.core
+
+import android.view.View
+import com.android.systemui.CoreStartable
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.plugins.PluginDependencyProvider
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.shade.NotificationShadeWindowViewController
+import com.android.systemui.shade.ShadeSurface
+import com.android.systemui.statusbar.AutoHideUiElement
+import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.statusbar.data.model.StatusBarMode
+import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
+import com.android.systemui.statusbar.phone.AutoHideController
+import com.android.systemui.statusbar.phone.CentralSurfaces
+import com.android.systemui.statusbar.phone.PhoneStatusBarTransitions
+import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.statusbar.window.data.model.StatusBarWindowState
+import com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepositoryStore
+import com.android.wm.shell.bubbles.Bubbles
+import dagger.Lazy
+import java.io.PrintWriter
+import java.util.Optional
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.launch
+
+/**
+ * Class responsible for managing the lifecycle and state of the status bar.
+ *
+ * It is a temporary class, created to pull status bar related logic out of CentralSurfacesImpl. The
+ * plan is break it out into individual classes.
+ */
+@SysUISingleton
+class StatusBarOrchestrator
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ private val statusBarInitializer: StatusBarInitializer,
+ private val statusBarWindowController: StatusBarWindowController,
+ private val statusBarModeRepository: StatusBarModeRepositoryStore,
+ private val demoModeController: DemoModeController,
+ private val pluginDependencyProvider: PluginDependencyProvider,
+ private val autoHideController: AutoHideController,
+ private val remoteInputManager: NotificationRemoteInputManager,
+ private val notificationShadeWindowViewControllerLazy:
+ Lazy<NotificationShadeWindowViewController>,
+ private val shadeSurface: ShadeSurface,
+ private val bubblesOptional: Optional<Bubbles>,
+ private val statusBarWindowStateRepositoryStore: StatusBarWindowStateRepositoryStore,
+ powerInteractor: PowerInteractor,
+ primaryBouncerInteractor: PrimaryBouncerInteractor,
+) : CoreStartable {
+
+ private val phoneStatusBarViewController =
+ MutableStateFlow<PhoneStatusBarViewController?>(value = null)
+
+ private val phoneStatusBarTransitions =
+ MutableStateFlow<PhoneStatusBarTransitions?>(value = null)
+
+ private val shouldAnimateNextBarModeChange =
+ combine(
+ statusBarModeRepository.defaultDisplay.isTransientShown,
+ powerInteractor.isAwake,
+ statusBarWindowStateRepositoryStore.defaultDisplay.windowState,
+ ) { isTransientShown, isDeviceAwake, statusBarWindowState ->
+ !isTransientShown &&
+ isDeviceAwake &&
+ statusBarWindowState != StatusBarWindowState.Hidden
+ }
+
+ private val controllerAndBouncerShowing =
+ combine(
+ phoneStatusBarViewController.filterNotNull(),
+ primaryBouncerInteractor.isShowing,
+ ::Pair,
+ )
+
+ private val barTransitionsAndDeviceAsleep =
+ combine(phoneStatusBarTransitions.filterNotNull(), powerInteractor.isAsleep, ::Pair)
+
+ private val statusBarVisible =
+ combine(
+ statusBarModeRepository.defaultDisplay.statusBarMode,
+ statusBarWindowStateRepositoryStore.defaultDisplay.windowState,
+ ) { mode, statusBarWindowState ->
+ mode != StatusBarMode.LIGHTS_OUT &&
+ mode != StatusBarMode.LIGHTS_OUT_TRANSPARENT &&
+ statusBarWindowState != StatusBarWindowState.Hidden
+ }
+
+ private val barModeUpdate =
+ combine(
+ shouldAnimateNextBarModeChange,
+ phoneStatusBarTransitions.filterNotNull(),
+ statusBarModeRepository.defaultDisplay.statusBarMode,
+ ::Triple,
+ )
+ .distinctUntilChangedBy { (_, barTransitions, statusBarMode) ->
+ // We only want to collect when either bar transitions or status bar mode
+ // changed.
+ Pair(barTransitions, statusBarMode)
+ }
+
+ override fun start() {
+ StatusBarSimpleFragment.assertInNewMode()
+ applicationScope.launch {
+ launch {
+ controllerAndBouncerShowing.collect { (controller, bouncerShowing) ->
+ setBouncerShowingForStatusBarComponents(controller, bouncerShowing)
+ }
+ }
+ launch {
+ barTransitionsAndDeviceAsleep.collect { (barTransitions, deviceAsleep) ->
+ if (deviceAsleep) {
+ barTransitions.finishAnimations()
+ }
+ }
+ }
+ launch { statusBarVisible.collect { updateBubblesVisibility(it) } }
+ launch {
+ barModeUpdate.collect { (animate, barTransitions, statusBarMode) ->
+ updateBarMode(animate, barTransitions, statusBarMode)
+ }
+ }
+ }
+ createAndAddWindow()
+ setupPluginDependencies()
+ setUpAutoHide()
+ }
+
+ private fun createAndAddWindow() {
+ initializeStatusBarFragment()
+ statusBarWindowController.attach()
+ }
+
+ private fun initializeStatusBarFragment() {
+ statusBarInitializer.statusBarViewUpdatedListener =
+ object : StatusBarInitializer.OnStatusBarViewUpdatedListener {
+ override fun onStatusBarViewUpdated(
+ statusBarViewController: PhoneStatusBarViewController,
+ statusBarTransitions: PhoneStatusBarTransitions,
+ ) {
+ phoneStatusBarViewController.value = statusBarViewController
+ phoneStatusBarTransitions.value = statusBarTransitions
+
+ notificationShadeWindowViewControllerLazy
+ .get()
+ .setStatusBarViewController(statusBarViewController)
+ // Ensure we re-propagate panel expansion values to the panel controller and
+ // any listeners it may have, such as PanelBar. This will also ensure we
+ // re-display the notification panel if necessary (for example, if
+ // a heads-up notification was being displayed and should continue being
+ // displayed).
+ shadeSurface.updateExpansionAndVisibility()
+ }
+ }
+ }
+
+ private fun setupPluginDependencies() {
+ pluginDependencyProvider.allowPluginDependency(DarkIconDispatcher::class.java)
+ pluginDependencyProvider.allowPluginDependency(StatusBarStateController::class.java)
+ }
+
+ private fun setUpAutoHide() {
+ autoHideController.setStatusBar(
+ object : AutoHideUiElement {
+ override fun synchronizeState() {}
+
+ override fun shouldHideOnTouch(): Boolean {
+ return !remoteInputManager.isRemoteInputActive
+ }
+
+ override fun isVisible(): Boolean {
+ return statusBarModeRepository.defaultDisplay.isTransientShown.value
+ }
+
+ override fun hide() {
+ statusBarModeRepository.defaultDisplay.clearTransient()
+ }
+ })
+ }
+
+ private fun updateBarMode(
+ animate: Boolean,
+ barTransitions: PhoneStatusBarTransitions,
+ barMode: StatusBarMode,
+ ) {
+ if (!demoModeController.isInDemoMode) {
+ barTransitions.transitionTo(barMode.toTransitionModeInt(), animate)
+ }
+ autoHideController.touchAutoHide()
+ }
+
+ private fun updateBubblesVisibility(statusBarVisible: Boolean) {
+ bubblesOptional.ifPresent { bubbles: Bubbles ->
+ bubbles.onStatusBarVisibilityChanged(statusBarVisible)
+ }
+ }
+
+ private fun setBouncerShowingForStatusBarComponents(
+ controller: PhoneStatusBarViewController,
+ bouncerShowing: Boolean,
+ ) {
+ val importance =
+ if (bouncerShowing) {
+ View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ } else {
+ View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+ }
+ controller.setImportantForAccessibility(importance)
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.println(statusBarWindowStateRepositoryStore.defaultDisplay.windowState.value)
+ CentralSurfaces.dumpBarTransitions(
+ pw,
+ "PhoneStatusBarTransitions",
+ phoneStatusBarTransitions.value,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index 3903ff3..cf238d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -46,6 +46,7 @@
*/
@Module(includes = [StatusBarDataLayerModule::class, SystemBarUtilsProxyImpl.Module::class])
abstract class StatusBarModule {
+
@Binds
@IntoMap
@ClassKey(OngoingCallController::class)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 1d3f0e1..5f4f72f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -241,10 +241,10 @@
import com.android.wm.shell.startingsurface.SplashscreenContentDrawer;
import com.android.wm.shell.startingsurface.StartingSurface;
-import dagger.Lazy;
-
import dalvik.annotation.optimization.NeverCompile;
+import dagger.Lazy;
+
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Map;
@@ -304,6 +304,7 @@
};
void onStatusBarWindowStateChanged(@WindowVisibleState int state) {
+ StatusBarSimpleFragment.assertInLegacyMode();
mStatusBarWindowState = state;
updateBubblesVisibility();
}
@@ -813,8 +814,9 @@
mStartingSurfaceOptional = startingSurfaceOptional;
mDreamManager = dreamManager;
lockscreenShadeTransitionController.setCentralSurfaces(this);
- statusBarWindowStateController.addListener(this::onStatusBarWindowStateChanged);
-
+ if (!StatusBarSimpleFragment.isEnabled()) {
+ statusBarWindowStateController.addListener(this::onStatusBarWindowStateChanged);
+ }
mScreenOffAnimationController = screenOffAnimationController;
ShadeExpansionListener shadeExpansionListener = this::onPanelExpansionChanged;
@@ -901,10 +903,12 @@
mWallpaperSupported = mWallpaperManager.isWallpaperSupported();
RegisterStatusBarResult result = null;
- try {
- result = mBarService.registerStatusBar(mCommandQueue);
- } catch (RemoteException ex) {
- ex.rethrowFromSystemServer();
+ if (!StatusBarSimpleFragment.isEnabled()) {
+ try {
+ result = mBarService.registerStatusBar(mCommandQueue);
+ } catch (RemoteException ex) {
+ ex.rethrowFromSystemServer();
+ }
}
createAndAddWindows(result);
@@ -912,30 +916,45 @@
// Set up the initial notification state. This needs to happen before CommandQueue.disable()
setUpPresenter();
- if ((result.mTransientBarTypes & WindowInsets.Type.statusBars()) != 0) {
- mStatusBarModeRepository.getDefaultDisplay().showTransient();
- }
- mCommandQueueCallbacks.onSystemBarAttributesChanged(mDisplayId, result.mAppearance,
- result.mAppearanceRegions, result.mNavbarColorManagedByIme, result.mBehavior,
- result.mRequestedVisibleTypes, result.mPackageName, result.mLetterboxDetails);
-
- // StatusBarManagerService has a back up of IME token and it's restored here.
- mCommandQueueCallbacks.setImeWindowStatus(mDisplayId, result.mImeWindowVis,
- result.mImeBackDisposition, result.mShowImeSwitcher);
-
- // Set up the initial icon state
- int numIcons = result.mIcons.size();
- for (int i = 0; i < numIcons; i++) {
- mCommandQueue.setIcon(result.mIcons.keyAt(i), result.mIcons.valueAt(i));
- }
-
- if (DEBUG) {
- Log.d(TAG, String.format(
- "init: icons=%d disabled=0x%08x lights=0x%08x imeButton=0x%08x",
- numIcons,
- result.mDisabledFlags1,
+ // When the StatusBarSimpleFragment flag is enabled, this logic will be done in
+ // StatusBarOrchestrator
+ if (!StatusBarSimpleFragment.isEnabled()) {
+ if ((result.mTransientBarTypes & WindowInsets.Type.statusBars()) != 0) {
+ mStatusBarModeRepository.getDefaultDisplay().showTransient();
+ }
+ mCommandQueueCallbacks.onSystemBarAttributesChanged(
+ mDisplayId,
result.mAppearance,
- result.mImeWindowVis));
+ result.mAppearanceRegions,
+ result.mNavbarColorManagedByIme,
+ result.mBehavior,
+ result.mRequestedVisibleTypes,
+ result.mPackageName,
+ result.mLetterboxDetails);
+
+ // StatusBarManagerService has a back up of IME token and it's restored here.
+ mCommandQueueCallbacks.setImeWindowStatus(
+ mDisplayId,
+ result.mImeWindowVis,
+ result.mImeBackDisposition,
+ result.mShowImeSwitcher);
+
+ // Set up the initial icon state
+ int numIcons = result.mIcons.size();
+ for (int i = 0; i < numIcons; i++) {
+ mCommandQueue.setIcon(result.mIcons.keyAt(i), result.mIcons.valueAt(i));
+ }
+
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "init: icons=%d disabled=0x%08x lights=0x%08x imeButton=0x%08x",
+ numIcons,
+ result.mDisabledFlags1,
+ result.mAppearance,
+ result.mImeWindowVis));
+ }
}
IntentFilter internalFilter = new IntentFilter();
@@ -1005,24 +1024,30 @@
mAccessibilityFloatingMenuController.init();
- // set the initial view visibility
- int disabledFlags1 = result.mDisabledFlags1;
- int disabledFlags2 = result.mDisabledFlags2;
- mInitController.addPostInitTask(() -> {
- setUpDisableFlags(disabledFlags1, disabledFlags2);
- try {
- // NOTE(b/262059863): Force-update the disable flags after applying the flags
- // returned from registerStatusBar(). The result's disabled flags may be stale
- // if StatusBarManager's disabled flags are updated between registering the bar and
- // this handling this post-init task. We force an update in this case, and use a new
- // token to not conflict with any other disabled flags already requested by SysUI
- Binder token = new Binder();
- mBarService.disable(DISABLE_HOME, token, mContext.getPackageName());
- mBarService.disable(0, token, mContext.getPackageName());
- } catch (RemoteException ex) {
- ex.rethrowFromSystemServer();
- }
- });
+ // When the StatusBarSimpleFragment flag is enabled, this logic will be done in
+ // StatusBarOrchestrator
+ if (!StatusBarSimpleFragment.isEnabled()) {
+ // set the initial view visibility
+ int disabledFlags1 = result.mDisabledFlags1;
+ int disabledFlags2 = result.mDisabledFlags2;
+ mInitController.addPostInitTask(
+ () -> {
+ setUpDisableFlags(disabledFlags1, disabledFlags2);
+ try {
+ // NOTE(b/262059863): Force-update the disable flags after applying the
+ // flags returned from registerStatusBar(). The result's disabled flags
+ // may be stale if StatusBarManager's disabled flags are updated between
+ // registering the bar and this handling this post-init task. We force
+ // an update in this case, and use a new token to not conflict with any
+ // other disabled flags already requested by SysUI
+ Binder token = new Binder();
+ mBarService.disable(DISABLE_HOME, token, mContext.getPackageName());
+ mBarService.disable(0, token, mContext.getPackageName());
+ } catch (RemoteException ex) {
+ ex.rethrowFromSystemServer();
+ }
+ });
+ }
registerCallbacks();
@@ -1101,7 +1126,7 @@
/**
* @deprecated use {@link
- * WindowRootViewVisibilityInteractor.isLockscreenOrShadeVisible} instead.
+ * WindowRootViewVisibilityInteractor#isLockscreenOrShadeVisible()} instead.
*/ @VisibleForTesting
@Deprecated
void initShadeVisibilityListener() {
@@ -1168,13 +1193,16 @@
mWallpaperController.setRootView(getNotificationShadeWindowView());
mDemoModeController.addCallback(mDemoModeCallback);
- mJavaAdapter.alwaysCollectFlow(
- mStatusBarModeRepository.getDefaultDisplay().isTransientShown(),
- this::onTransientShownChanged);
- mJavaAdapter.alwaysCollectFlow(
- mStatusBarModeRepository.getDefaultDisplay().getStatusBarMode(),
- this::updateBarMode);
-
+ // When the StatusBarSimpleFragment flag is enabled, this logic will be done in
+ // StatusBarOrchestrator.
+ if (!StatusBarSimpleFragment.isEnabled()) {
+ mJavaAdapter.alwaysCollectFlow(
+ mStatusBarModeRepository.getDefaultDisplay().isTransientShown(),
+ this::onTransientShownChanged);
+ mJavaAdapter.alwaysCollectFlow(
+ mStatusBarModeRepository.getDefaultDisplay().getStatusBarMode(),
+ this::updateBarMode);
+ }
mCommandQueueCallbacks = mCommandQueueCallbacksLazy.get();
mCommandQueue.addCallback(mCommandQueueCallbacks);
@@ -1184,59 +1212,70 @@
mShadeExpansionStateManager.addExpansionListener(mWakeUpCoordinator);
mWakeUpCoordinator.onPanelExpansionChanged(currentState);
- // Allow plugins to reference DarkIconDispatcher and StatusBarStateController
- mPluginDependencyProvider.allowPluginDependency(DarkIconDispatcher.class);
- mPluginDependencyProvider.allowPluginDependency(StatusBarStateController.class);
-
- // Set up CollapsedStatusBarFragment and PhoneStatusBarView
- mStatusBarInitializer.setStatusBarViewUpdatedListener(
- (statusBarViewController, statusBarTransitions) -> {
- mPhoneStatusBarViewController = statusBarViewController;
- mStatusBarTransitions = statusBarTransitions;
- getNotificationShadeWindowViewController()
- .setStatusBarViewController(mPhoneStatusBarViewController);
- // Ensure we re-propagate panel expansion values to the panel controller and
- // any listeners it may have, such as PanelBar. This will also ensure we
- // re-display the notification panel if necessary (for example, if
- // a heads-up notification was being displayed and should continue being
- // displayed).
- mShadeSurface.updateExpansionAndVisibility();
- setBouncerShowingForStatusBarComponents(mBouncerShowing);
- checkBarModes();
- });
- // When the flag is on, we register the fragment as a core startable and this is not needed
+ // When the StatusBarSimpleFragment flag is enabled, all this logic will be done in
+ // StatusBarOrchestrator.
if (!StatusBarSimpleFragment.isEnabled()) {
+ // Allow plugins to reference DarkIconDispatcher and StatusBarStateController
+ mPluginDependencyProvider.allowPluginDependency(DarkIconDispatcher.class);
+ mPluginDependencyProvider.allowPluginDependency(StatusBarStateController.class);
+
+ // Set up CollapsedStatusBarFragment and PhoneStatusBarView
+ mStatusBarInitializer.setStatusBarViewUpdatedListener(
+ (statusBarViewController, statusBarTransitions) -> {
+
+ mPhoneStatusBarViewController = statusBarViewController;
+ mStatusBarTransitions = statusBarTransitions;
+ getNotificationShadeWindowViewController()
+ .setStatusBarViewController(mPhoneStatusBarViewController);
+ // Ensure we re-propagate panel expansion values to the panel controller and
+ // any listeners it may have, such as PanelBar. This will also ensure we
+ // re-display the notification panel if necessary (for example, if
+ // a heads-up notification was being displayed and should continue being
+ // displayed).
+ mShadeSurface.updateExpansionAndVisibility();
+ setBouncerShowingForStatusBarComponents(mBouncerShowing);
+ checkBarModes();
+ });
+ // When the flag is on, we register the fragment as a core startable and this is not
+ // needed
mStatusBarInitializer.initializeStatusBar();
}
mStatusBarTouchableRegionManager.setup(getNotificationShadeWindowView());
- createNavigationBar(result);
+ if (!StatusBarSimpleFragment.isEnabled()) {
+ createNavigationBar(result);
+ }
mAmbientIndicationContainer = getNotificationShadeWindowView().findViewById(
R.id.ambient_indication_container);
- mAutoHideController.setStatusBar(new AutoHideUiElement() {
- @Override
- public void synchronizeState() {
- checkBarModes();
- }
+ // When the StatusBarSimpleFragment flag is enabled, all this logic will be done in
+ // StatusBarOrchestrator.
+ if (!StatusBarSimpleFragment.isEnabled()) {
+ mAutoHideController.setStatusBar(
+ new AutoHideUiElement() {
+ @Override
+ public void synchronizeState() {
+ checkBarModes();
+ }
- @Override
- public boolean shouldHideOnTouch() {
- return !mRemoteInputManager.isRemoteInputActive();
- }
+ @Override
+ public boolean shouldHideOnTouch() {
+ return !mRemoteInputManager.isRemoteInputActive();
+ }
- @Override
- public boolean isVisible() {
- return isTransientShown();
- }
+ @Override
+ public boolean isVisible() {
+ return isTransientShown();
+ }
- @Override
- public void hide() {
- mStatusBarModeRepository.getDefaultDisplay().clearTransient();
- }
- });
+ @Override
+ public void hide() {
+ mStatusBarModeRepository.getDefaultDisplay().clearTransient();
+ }
+ });
+ }
ScrimView scrimBehind = getNotificationShadeWindowView().findViewById(R.id.scrim_behind);
ScrimView notificationsScrim = getNotificationShadeWindowView()
@@ -1479,12 +1518,14 @@
* @param state2 disable2 flags
*/
protected void setUpDisableFlags(int state1, int state2) {
+ StatusBarSimpleFragment.assertInLegacyMode();
mCommandQueue.disable(mDisplayId, state1, state2, false /* animate */);
}
// TODO(b/117478341): This was left such that CarStatusBar can override this method.
// Try to remove this.
protected void createNavigationBar(@Nullable RegisterStatusBarResult result) {
+ StatusBarSimpleFragment.assertInLegacyMode();
mNavigationBarController.createNavigationBars(true /* includeDefaultDisplay */, result);
}
@@ -1697,14 +1738,16 @@
@Override
public void checkBarModes() {
if (mDemoModeController.isInDemoMode()) return;
- if (mStatusBarTransitions != null) {
+ // When the StatusBarSimpleFragment flag is enabled, this logic will be done in
+ // StatusBarOrchestrator.
+ if (!StatusBarSimpleFragment.isEnabled() && mStatusBarTransitions != null) {
checkBarMode(
mStatusBarModeRepository.getDefaultDisplay().getStatusBarMode().getValue(),
mStatusBarWindowState,
mStatusBarTransitions);
+ mNoAnimationOnNextBarModeChange = false;
}
mNavigationBarController.checkNavBarModes(mDisplayId);
- mNoAnimationOnNextBarModeChange = false;
}
/** Temporarily hides Bubbles if the status bar is hidden. */
@@ -1728,7 +1771,9 @@
}
private void finishBarAnimations() {
- if (mStatusBarTransitions != null) {
+ // When the StatusBarSimpleFragment flag is enabled, this logic will be done in
+ // StatusBarOrchestrator.
+ if (!StatusBarSimpleFragment.isEnabled() && mStatusBarTransitions != null) {
mStatusBarTransitions.finishAnimations();
}
mNavigationBarController.finishBarAnimations(mDisplayId);
@@ -1770,14 +1815,17 @@
}
pw.print(" mInteractingWindows="); pw.println(mInteractingWindows);
- pw.print(" mStatusBarWindowState=");
- pw.println(windowStateToString(mStatusBarWindowState));
+ if (!StatusBarSimpleFragment.isEnabled()) {
+ pw.print(" mStatusBarWindowState=");
+ pw.println(windowStateToString(mStatusBarWindowState));
+ }
pw.print(" mDozing="); pw.println(mDozing);
pw.print(" mWallpaperSupported= "); pw.println(mWallpaperSupported);
- CentralSurfaces.dumpBarTransitions(
- pw, "PhoneStatusBarTransitions", mStatusBarTransitions);
-
+ if (!StatusBarSimpleFragment.isEnabled()) {
+ CentralSurfaces.dumpBarTransitions(
+ pw, "PhoneStatusBarTransitions", mStatusBarTransitions);
+ }
pw.println(" mMediaManager: ");
if (mMediaManager != null) {
mMediaManager.dump(pw, args);
@@ -1850,7 +1898,11 @@
private void createAndAddWindows(@Nullable RegisterStatusBarResult result) {
makeStatusBarView(result);
mNotificationShadeWindowController.attach();
- mStatusBarWindowController.attach();
+ // When the StatusBarSimpleFragment flag is enabled, this logic will be done in
+ // StatusBarOrchestrator
+ if (!StatusBarSimpleFragment.isEnabled()) {
+ mStatusBarWindowController.attach();
+ }
}
// called by makeStatusbar and also by PhoneStatusBarView
@@ -2475,7 +2527,7 @@
int importance = bouncerShowing
? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
: IMPORTANT_FOR_ACCESSIBILITY_AUTO;
- if (mPhoneStatusBarViewController != null) {
+ if (!StatusBarSimpleFragment.isEnabled() && mPhoneStatusBarViewController != null) {
mPhoneStatusBarViewController.setImportantForAccessibility(importance);
}
mShadeSurface.setImportantForAccessibility(importance);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt
index 13b651e8..5b03198 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt
@@ -16,10 +16,20 @@
package com.android.systemui.statusbar.phone.dagger
import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.core.CommandQueueInitializer
import com.android.systemui.statusbar.core.StatusBarInitializer
import com.android.systemui.statusbar.core.StatusBarInitializerImpl
+import com.android.systemui.statusbar.core.StatusBarOrchestrator
+import com.android.systemui.statusbar.core.StatusBarSimpleFragment
+import com.android.systemui.statusbar.phone.CentralSurfacesCommandQueueCallbacks
+import com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepositoryStore
+import com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepositoryStoreImpl
import dagger.Binds
+import dagger.Lazy
import dagger.Module
+import dagger.Provides
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@@ -27,6 +37,16 @@
@Module
interface StatusBarPhoneModule {
+ @Binds
+ abstract fun windowStateRepoStore(
+ impl: StatusBarWindowStateRepositoryStoreImpl
+ ): StatusBarWindowStateRepositoryStore
+
+ @Binds
+ abstract fun commandQCallbacks(
+ impl: CentralSurfacesCommandQueueCallbacks
+ ): CommandQueue.Callbacks
+
/** Binds {@link StatusBarInitializer} as a {@link CoreStartable}. */
@Binds
@IntoMap
@@ -34,4 +54,34 @@
fun bindStatusBarInitializer(impl: StatusBarInitializerImpl): CoreStartable
@Binds fun statusBarInitializer(impl: StatusBarInitializerImpl): StatusBarInitializer
+
+ companion object {
+ @Provides
+ @SysUISingleton
+ @IntoMap
+ @ClassKey(StatusBarOrchestrator::class)
+ fun orchestratorCoreStartable(
+ orchestratorLazy: Lazy<StatusBarOrchestrator>
+ ): CoreStartable {
+ return if (StatusBarSimpleFragment.isEnabled) {
+ orchestratorLazy.get()
+ } else {
+ CoreStartable.NOP
+ }
+ }
+
+ @Provides
+ @SysUISingleton
+ @IntoMap
+ @ClassKey(CommandQueueInitializer::class)
+ fun commandQueueInitializerCoreStartable(
+ initializerLazy: Lazy<CommandQueueInitializer>
+ ): CoreStartable {
+ return if (StatusBarSimpleFragment.isEnabled) {
+ initializerLazy.get()
+ } else {
+ CoreStartable.NOP
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt
index 3c50c7b..09b1f45 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt
@@ -1,6 +1,7 @@
package com.android.systemui.wallet.controller
import android.content.Intent
+import android.os.DeadObjectException
import android.os.IBinder
import android.util.Log
import androidx.annotation.VisibleForTesting
@@ -47,7 +48,11 @@
controller.allWalletCards.collect { cards ->
val cardsSize = cards.size
Log.i(TAG, "Number of cards registered $cardsSize")
- listener?.registerNewWalletCards(cards)
+ try {
+ listener?.registerNewWalletCards(cards)
+ } catch (e: DeadObjectException) {
+ Log.e(TAG, "Failed to register wallet cards because IWalletCardsUpdatedListener is dead")
+ }
}
}
} else {
@@ -55,7 +60,11 @@
controller.allWalletCards.collect { cards ->
val cardsSize = cards.size
Log.i(TAG, "Number of cards registered $cardsSize")
- listener?.registerNewWalletCards(cards)
+ try {
+ listener?.registerNewWalletCards(cards)
+ } catch (e: DeadObjectException) {
+ Log.e(TAG, "Failed to register wallet cards because IWalletCardsUpdatedListener is dead")
+ }
}
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
index 5cc6454..0d369a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
@@ -19,6 +19,7 @@
import android.app.AlertDialog
import android.platform.test.annotations.MotionTest
import android.testing.TestableLooper.RunWithLooper
+import android.view.View
import androidx.activity.BackEventCompat
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
@@ -52,27 +53,21 @@
import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
import com.android.systemui.bouncer.ui.viewmodel.BouncerUserActionsViewModel
import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel
-import com.android.systemui.classifier.domain.interactor.falsingInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.motion.createSysUiComposeMotionTestRule
-import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.startable.sceneContainerStartable
-import com.android.systemui.scene.sceneContainerGestureFilterFactory
-import com.android.systemui.scene.shared.logger.sceneLogger
+import com.android.systemui.scene.sceneContainerViewModelFactory
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
import com.android.systemui.scene.ui.composable.Scene
import com.android.systemui.scene.ui.composable.SceneContainer
-import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
import com.android.systemui.settings.displayTracker
-import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.testKosmos
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.awaitCancellation
@@ -85,6 +80,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.mock
import platform.test.motion.compose.ComposeFeatureCaptures.positionInRoot
import platform.test.motion.compose.ComposeRecordingSpec
import platform.test.motion.compose.MotionControl
@@ -121,24 +117,17 @@
val navigationDistances = mapOf(Scenes.Lockscreen to 1, Scenes.Bouncer to 0)
SceneContainerConfig(sceneKeys, initialSceneKey, emptyList(), navigationDistances)
}
+ private val view = mock<View>()
private val transitionState by lazy {
MutableStateFlow<ObservableTransitionState>(
ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey)
)
}
+
private val sceneContainerViewModel by lazy {
- SceneContainerViewModel(
- sceneInteractor = kosmos.sceneInteractor,
- falsingInteractor = kosmos.falsingInteractor,
- powerInteractor = kosmos.powerInteractor,
- shadeInteractor = kosmos.shadeInteractor,
- splitEdgeDetector = kosmos.splitEdgeDetector,
- logger = kosmos.sceneLogger,
- gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory,
- displayId = kosmos.displayTracker.defaultDisplayId,
- motionEventHandlerReceiver = {},
- )
+ kosmos.sceneContainerViewModelFactory
+ .create(view, kosmos.displayTracker.defaultDisplayId, {})
.apply { setTransitionState(transitionState) }
}
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 a6afd0e..f5a90196 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -22,10 +22,6 @@
import static com.google.common.truth.Truth.assertThat;
-import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
-import static kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow;
-import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
-
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
@@ -40,6 +36,10 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+import static kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow;
+import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
+
import android.animation.Animator;
import android.annotation.IdRes;
import android.content.ContentResolver;
@@ -95,6 +95,7 @@
import com.android.systemui.flags.Flags;
import com.android.systemui.fragments.FragmentHostManager;
import com.android.systemui.fragments.FragmentService;
+import com.android.systemui.haptics.msdl.FakeMSDLPlayer;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.KeyguardViewConfigurator;
import com.android.systemui.keyguard.data.repository.FakeKeyguardClockRepository;
@@ -151,6 +152,7 @@
import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository;
import com.android.systemui.statusbar.notification.ConversationNotificationManager;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
+import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinatorLogger;
import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardViewStateRepository;
@@ -167,7 +169,6 @@
import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
-import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
import com.android.systemui.statusbar.phone.KeyguardBottomAreaViewController;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -200,12 +201,6 @@
import com.android.systemui.util.time.SystemClock;
import com.android.wm.shell.animation.FlingAnimationUtils;
-import dagger.Lazy;
-
-import kotlinx.coroutines.CoroutineDispatcher;
-import kotlinx.coroutines.channels.BufferOverflow;
-import kotlinx.coroutines.test.TestScope;
-
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -220,6 +215,11 @@
import java.util.List;
import java.util.Optional;
+import dagger.Lazy;
+import kotlinx.coroutines.CoroutineDispatcher;
+import kotlinx.coroutines.channels.BufferOverflow;
+import kotlinx.coroutines.test.TestScope;
+
public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
protected static final int SPLIT_SHADE_FULL_TRANSITION_DISTANCE = 400;
@@ -374,6 +374,7 @@
protected View.OnLayoutChangeListener mLayoutChangeListener;
protected KeyguardStatusViewController mKeyguardStatusViewController;
protected ShadeRepository mShadeRepository;
+ protected FakeMSDLPlayer mMSDLPlayer = mKosmos.getMsdlPlayer();
protected final FalsingManagerFake mFalsingManager = new FalsingManagerFake();
protected final Optional<SysUIUnfoldComponent> mSysUIUnfoldComponent = Optional.empty();
@@ -761,7 +762,8 @@
new ResourcesSplitShadeStateController(),
mPowerInteractor,
mKeyguardClockPositionAlgorithm,
- mNaturalScrollingSettingObserver);
+ mNaturalScrollingSettingObserver,
+ mMSDLPlayer);
mNotificationPanelViewController.initDependencies(
mCentralSurfaces,
null,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index a7fd160..43dbb40 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -49,6 +49,7 @@
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.testing.TestableLooper;
+import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -69,6 +70,8 @@
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
import com.android.systemui.statusbar.phone.KeyguardClockPositionAlgorithm;
+import com.google.android.msdl.data.model.MSDLToken;
+
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@@ -1458,4 +1461,23 @@
assertThat(mNotificationPanelViewController.getFalsingThreshold()).isGreaterThan(14);
}
+
+ @Test
+ @EnableFlags(com.android.systemui.Flags.FLAG_MSDL_FEEDBACK)
+ public void performHapticFeedback_withMSDL_forGestureStart_deliversDragThresholdToken() {
+ mNotificationPanelViewController
+ .performHapticFeedback(HapticFeedbackConstants.GESTURE_START);
+
+ assertThat(mMSDLPlayer.getLatestTokenPlayed())
+ .isEqualTo(MSDLToken.SWIPE_THRESHOLD_INDICATOR);
+ }
+
+ @Test
+ @EnableFlags(com.android.systemui.Flags.FLAG_MSDL_FEEDBACK)
+ public void performHapticFeedback_withMSDL_forReject_deliversFailureToken() {
+ mNotificationPanelViewController
+ .performHapticFeedback(HapticFeedbackConstants.REJECT);
+
+ assertThat(mMSDLPlayer.getLatestTokenPlayed()).isEqualTo(MSDLToken.FAILURE);
+ }
}
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 90655c3..97441f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
@@ -33,7 +33,6 @@
import com.android.systemui.statusbar.StatusBarState.KEYGUARD
import com.android.systemui.statusbar.StatusBarState.SHADE
import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
-import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -157,6 +156,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
fun doubleTapRequired_onKeyguard_usesPerformHapticFeedback() = runTest {
launch(Dispatchers.Main.immediate) {
val listener = getFalsingTapListener()
@@ -184,6 +184,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
fun doubleTapRequired_shadeLocked_usesPerformHapticFeedback() = runTest {
launch(Dispatchers.Main.immediate) {
val listener = getFalsingTapListener()
@@ -209,7 +210,7 @@
KEYGUARD /*statusBarState*/,
false /*keyguardFadingAway*/,
false /*goingToFullShade*/,
- SHADE /*oldStatusBarState*/
+ SHADE, /*oldStatusBarState*/
)
}
advanceUntilIdle()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/CommandQueueInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/CommandQueueInitializerTest.kt
new file mode 100644
index 0000000..2a196c6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/CommandQueueInitializerTest.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.core
+
+import android.internal.statusbar.fakeStatusBarService
+import android.platform.test.annotations.EnableFlags
+import android.view.WindowInsets
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.initController
+import com.android.systemui.keyguard.data.repository.fakeCommandQueue
+import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
+import com.android.systemui.statusbar.mockCommandQueueCallbacks
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.verify
+
+@EnableFlags(StatusBarSimpleFragment.FLAG_NAME)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommandQueueInitializerTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val initController = kosmos.initController
+ private val commandQueue = kosmos.fakeCommandQueue
+ private val commandQueueCallbacks = kosmos.mockCommandQueueCallbacks
+ private val statusBarModeRepository = kosmos.fakeStatusBarModeRepository
+ private val fakeStatusBarService = kosmos.fakeStatusBarService
+ private val initializer = kosmos.commandQueueInitializer
+
+ @Test
+ fun start_registersStatusBar() {
+ initializer.start()
+
+ assertThat(fakeStatusBarService.registeredStatusBar).isNotNull()
+ }
+
+ @Test
+ fun start_barResultHasTransientStatusBar_transientStateIsTrue() {
+ fakeStatusBarService.transientBarTypes = WindowInsets.Type.statusBars()
+
+ initializer.start()
+
+ assertThat(statusBarModeRepository.defaultDisplay.isTransientShown.value).isTrue()
+ }
+
+ @Test
+ fun start_barResultDoesNotHaveTransientStatusBar_transientStateIsFalse() {
+ fakeStatusBarService.transientBarTypes = WindowInsets.Type.navigationBars()
+
+ initializer.start()
+
+ assertThat(statusBarModeRepository.defaultDisplay.isTransientShown.value).isFalse()
+ }
+
+ @Test
+ fun start_callsOnSystemBarAttributesChanged_basedOnRegisterBarResult() {
+ initializer.start()
+
+ verify(commandQueueCallbacks)
+ .onSystemBarAttributesChanged(
+ context.displayId,
+ fakeStatusBarService.appearance,
+ fakeStatusBarService.appearanceRegions,
+ fakeStatusBarService.navbarColorManagedByIme,
+ fakeStatusBarService.behavior,
+ fakeStatusBarService.requestedVisibleTypes,
+ fakeStatusBarService.packageName,
+ fakeStatusBarService.letterboxDetails,
+ )
+ }
+
+ @Test
+ fun start_callsSetIcon_basedOnRegisterBarResult() {
+ initializer.start()
+
+ assertThat(commandQueue.icons).isEqualTo(fakeStatusBarService.statusBarIcons)
+ }
+
+ @Test
+ fun start_callsSetImeWindowStatus_basedOnRegisterBarResult() {
+ initializer.start()
+
+ verify(commandQueueCallbacks)
+ .setImeWindowStatus(
+ context.displayId,
+ fakeStatusBarService.imeWindowVis,
+ fakeStatusBarService.imeBackDisposition,
+ fakeStatusBarService.showImeSwitcher,
+ )
+ }
+
+ @Test
+ fun start_afterPostInitTaskExecuted_callsDisableFlags_basedOnRegisterBarResult() {
+ initializer.start()
+
+ initController.executePostInitTasks()
+
+ assertThat(commandQueue.disableFlags1ForDisplay(context.displayId))
+ .isEqualTo(fakeStatusBarService.disabledFlags1)
+ assertThat(commandQueue.disableFlags2ForDisplay(context.displayId))
+ .isEqualTo(fakeStatusBarService.disabledFlags2)
+ }
+
+ @Test
+ fun start_beforePostInitTaskExecuted_doesNotCallsDisableFlags() {
+ initializer.start()
+
+ assertThat(commandQueue.disableFlags1ForDisplay(context.displayId)).isNull()
+ assertThat(commandQueue.disableFlags2ForDisplay(context.displayId)).isNull()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt
new file mode 100644
index 0000000..5803365
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt
@@ -0,0 +1,335 @@
+/*
+ * 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.core
+
+import android.platform.test.annotations.EnableFlags
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.plugins.mockPluginDependencyProvider
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.shade.mockNotificationShadeWindowViewController
+import com.android.systemui.shade.mockShadeSurface
+import com.android.systemui.statusbar.data.model.StatusBarMode
+import com.android.systemui.statusbar.data.model.StatusBarMode.LIGHTS_OUT
+import com.android.systemui.statusbar.data.model.StatusBarMode.LIGHTS_OUT_TRANSPARENT
+import com.android.systemui.statusbar.data.model.StatusBarMode.OPAQUE
+import com.android.systemui.statusbar.data.model.StatusBarMode.TRANSPARENT
+import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
+import com.android.systemui.statusbar.phone.mockPhoneStatusBarTransitions
+import com.android.systemui.statusbar.phone.mockPhoneStatusBarViewController
+import com.android.systemui.statusbar.window.data.model.StatusBarWindowState
+import com.android.systemui.statusbar.window.data.repository.fakeStatusBarWindowStateRepositoryStore
+import com.android.systemui.statusbar.window.data.repository.statusBarWindowStateRepositoryStore
+import com.android.systemui.statusbar.window.fakeStatusBarWindowController
+import com.android.systemui.testKosmos
+import com.android.wm.shell.bubbles.bubbles
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@EnableFlags(StatusBarSimpleFragment.FLAG_NAME)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class StatusBarOrchestratorTest : SysuiTestCase() {
+
+ private val kosmos =
+ testKosmos().also {
+ it.testDispatcher = it.unconfinedTestDispatcher
+ it.statusBarWindowStateRepositoryStore = it.fakeStatusBarWindowStateRepositoryStore
+ }
+ private val testScope = kosmos.testScope
+ private val statusBarViewController = kosmos.mockPhoneStatusBarViewController
+ private val statusBarWindowController = kosmos.fakeStatusBarWindowController
+ private val statusBarModeRepository = kosmos.fakeStatusBarModeRepository
+ private val pluginDependencyProvider = kosmos.mockPluginDependencyProvider
+ private val notificationShadeWindowViewController =
+ kosmos.mockNotificationShadeWindowViewController
+ private val shadeSurface = kosmos.mockShadeSurface
+ private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository
+ private val fakeStatusBarWindowStateRepositoryStore =
+ kosmos.fakeStatusBarWindowStateRepositoryStore
+ private val fakePowerRepository = kosmos.fakePowerRepository
+ private val mockPhoneStatusBarTransitions = kosmos.mockPhoneStatusBarTransitions
+ private val mockBubbles = kosmos.bubbles
+
+ private val orchestrator = kosmos.statusBarOrchestrator
+
+ @Test
+ fun start_setsUpPluginDependencies() {
+ orchestrator.start()
+
+ verify(pluginDependencyProvider).allowPluginDependency(DarkIconDispatcher::class.java)
+ verify(pluginDependencyProvider).allowPluginDependency(StatusBarStateController::class.java)
+ }
+
+ @Test
+ fun start_attachesWindow() {
+ orchestrator.start()
+
+ assertThat(statusBarWindowController.isAttached).isTrue()
+ }
+
+ @Test
+ fun start_setsStatusBarControllerOnShade() {
+ orchestrator.start()
+
+ verify(notificationShadeWindowViewController)
+ .setStatusBarViewController(statusBarViewController)
+ }
+
+ @Test
+ fun start_updatesShadeExpansion() {
+ orchestrator.start()
+
+ verify(shadeSurface).updateExpansionAndVisibility()
+ }
+
+ @Test
+ fun bouncerShowing_setsImportanceForA11yToNoHideDescendants() =
+ testScope.runTest {
+ orchestrator.start()
+
+ bouncerRepository.setPrimaryShow(isShowing = true)
+
+ verify(statusBarViewController)
+ .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS)
+ }
+
+ @Test
+ fun bouncerNotShowing_setsImportanceForA11yToNoHideDescendants() =
+ testScope.runTest {
+ orchestrator.start()
+
+ bouncerRepository.setPrimaryShow(isShowing = false)
+
+ verify(statusBarViewController)
+ .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO)
+ }
+
+ @Test
+ fun deviceGoesToSleep_barTransitionsAnimationsAreFinished() =
+ testScope.runTest {
+ putDeviceToSleep()
+
+ orchestrator.start()
+
+ verify(mockPhoneStatusBarTransitions).finishAnimations()
+ }
+
+ @Test
+ fun deviceIsAwake_barTransitionsAnimationsAreNotFinished() =
+ testScope.runTest {
+ awakeDevice()
+
+ orchestrator.start()
+
+ verify(mockPhoneStatusBarTransitions, never()).finishAnimations()
+ }
+
+ @Test
+ fun statusBarVisible_notifiesBubbles() =
+ testScope.runTest {
+ setStatusBarMode(TRANSPARENT)
+ setStatusBarWindowState(StatusBarWindowState.Showing)
+
+ orchestrator.start()
+
+ verify(mockBubbles).onStatusBarVisibilityChanged(/* visible= */ true)
+ }
+
+ @Test
+ fun statusBarInLightsOutMode_notifiesBubblesWithStatusBarInvisible() =
+ testScope.runTest {
+ setStatusBarMode(LIGHTS_OUT)
+ setStatusBarWindowState(StatusBarWindowState.Showing)
+
+ orchestrator.start()
+
+ verify(mockBubbles).onStatusBarVisibilityChanged(/* visible= */ false)
+ }
+
+ @Test
+ fun statusBarInLightsOutTransparentMode_notifiesBubblesWithStatusBarInvisible() =
+ testScope.runTest {
+ setStatusBarMode(LIGHTS_OUT_TRANSPARENT)
+ setStatusBarWindowState(StatusBarWindowState.Showing)
+
+ orchestrator.start()
+
+ verify(mockBubbles).onStatusBarVisibilityChanged(/* visible= */ false)
+ }
+
+ @Test
+ fun statusBarWindowNotShowing_notifiesBubblesWithStatusBarInvisible() =
+ testScope.runTest {
+ setStatusBarMode(TRANSPARENT)
+ setStatusBarWindowState(StatusBarWindowState.Hidden)
+
+ orchestrator.start()
+
+ verify(mockBubbles).onStatusBarVisibilityChanged(/* visible= */ false)
+ }
+
+ @Test
+ fun statusBarModeChange_transitionsToModeWithAnimation() =
+ testScope.runTest {
+ awakeDevice()
+ clearTransientStatusBar()
+ setStatusBarWindowState(StatusBarWindowState.Showing)
+ setStatusBarMode(TRANSPARENT)
+
+ orchestrator.start()
+
+ verify(mockPhoneStatusBarTransitions)
+ .transitionTo(TRANSPARENT.toTransitionModeInt(), /* animate= */ true)
+ }
+
+ @Test
+ fun statusBarModeChange_keepsTransitioningAsModeChanges() =
+ testScope.runTest {
+ awakeDevice()
+ clearTransientStatusBar()
+ setStatusBarWindowState(StatusBarWindowState.Showing)
+ setStatusBarMode(TRANSPARENT)
+
+ orchestrator.start()
+
+ verify(mockPhoneStatusBarTransitions)
+ .transitionTo(TRANSPARENT.toTransitionModeInt(), /* animate= */ true)
+
+ setStatusBarMode(OPAQUE)
+ verify(mockPhoneStatusBarTransitions)
+ .transitionTo(OPAQUE.toTransitionModeInt(), /* animate= */ true)
+
+ setStatusBarMode(LIGHTS_OUT)
+ verify(mockPhoneStatusBarTransitions)
+ .transitionTo(LIGHTS_OUT.toTransitionModeInt(), /* animate= */ true)
+
+ setStatusBarMode(LIGHTS_OUT_TRANSPARENT)
+ verify(mockPhoneStatusBarTransitions)
+ .transitionTo(LIGHTS_OUT_TRANSPARENT.toTransitionModeInt(), /* animate= */ true)
+ }
+
+ @Test
+ fun statusBarModeChange_transientIsShown_transitionsToModeWithoutAnimation() =
+ testScope.runTest {
+ awakeDevice()
+ setTransientStatusBar()
+ setStatusBarWindowState(StatusBarWindowState.Showing)
+ setStatusBarMode(TRANSPARENT)
+
+ orchestrator.start()
+
+ verify(mockPhoneStatusBarTransitions)
+ .transitionTo(/* mode= */ TRANSPARENT.toTransitionModeInt(), /* animate= */ false)
+ }
+
+ @Test
+ fun statusBarModeChange_windowIsHidden_transitionsToModeWithoutAnimation() =
+ testScope.runTest {
+ awakeDevice()
+ clearTransientStatusBar()
+ setStatusBarWindowState(StatusBarWindowState.Hidden)
+ setStatusBarMode(TRANSPARENT)
+
+ orchestrator.start()
+
+ verify(mockPhoneStatusBarTransitions)
+ .transitionTo(/* mode= */ TRANSPARENT.toTransitionModeInt(), /* animate= */ false)
+ }
+
+ @Test
+ fun statusBarModeChange_deviceIsAsleep_transitionsToModeWithoutAnimation() =
+ testScope.runTest {
+ putDeviceToSleep()
+ clearTransientStatusBar()
+ setStatusBarWindowState(StatusBarWindowState.Showing)
+ setStatusBarMode(TRANSPARENT)
+
+ orchestrator.start()
+
+ verify(mockPhoneStatusBarTransitions)
+ .transitionTo(/* mode= */ TRANSPARENT.toTransitionModeInt(), /* animate= */ false)
+ }
+
+ @Test
+ fun statusBarModeAnimationConditionsChange_withoutBarModeChange_noNewTransitionsHappen() =
+ testScope.runTest {
+ awakeDevice()
+ clearTransientStatusBar()
+ setStatusBarWindowState(StatusBarWindowState.Showing)
+ setStatusBarMode(TRANSPARENT)
+
+ orchestrator.start()
+
+ putDeviceToSleep()
+ awakeDevice()
+ setTransientStatusBar()
+ clearTransientStatusBar()
+
+ verify(mockPhoneStatusBarTransitions, times(1))
+ .transitionTo(TRANSPARENT.toTransitionModeInt(), /* animate= */ true)
+ }
+
+ private fun putDeviceToSleep() {
+ fakePowerRepository.updateWakefulness(
+ rawState = WakefulnessState.ASLEEP,
+ lastWakeReason = WakeSleepReason.KEY,
+ lastSleepReason = WakeSleepReason.KEY,
+ powerButtonLaunchGestureTriggered = true,
+ )
+ }
+
+ private fun awakeDevice() {
+ fakePowerRepository.updateWakefulness(
+ rawState = WakefulnessState.AWAKE,
+ lastWakeReason = WakeSleepReason.KEY,
+ lastSleepReason = WakeSleepReason.KEY,
+ powerButtonLaunchGestureTriggered = true,
+ )
+ }
+
+ private fun setTransientStatusBar() {
+ statusBarModeRepository.defaultDisplay.showTransient()
+ }
+
+ private fun clearTransientStatusBar() {
+ statusBarModeRepository.defaultDisplay.clearTransient()
+ }
+
+ private fun setStatusBarWindowState(state: StatusBarWindowState) {
+ fakeStatusBarWindowStateRepositoryStore.defaultDisplay.setWindowState(state)
+ }
+
+ private fun setStatusBarMode(statusBarMode: StatusBarMode) {
+ statusBarModeRepository.defaultDisplay.statusBarMode.value = statusBarMode
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index c710c56..15ea811 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -169,6 +169,7 @@
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.StatusBarStateControllerImpl;
import com.android.systemui.statusbar.core.StatusBarInitializerImpl;
+import com.android.systemui.statusbar.core.StatusBarOrchestrator;
import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.NotificationActivityStarter;
@@ -346,6 +347,7 @@
@Mock private EmergencyGestureIntentFactory mEmergencyGestureIntentFactory;
@Mock private NotificationSettingsInteractor mNotificationSettingsInteractor;
@Mock private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
+ @Mock private StatusBarOrchestrator mStatusBarOrchestrator;
private ShadeController mShadeController;
private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
private final FakeGlobalSettings mFakeGlobalSettings = new FakeGlobalSettings();
diff --git a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt
new file mode 100644
index 0000000..cc0597b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt
@@ -0,0 +1,355 @@
+/*
+ * 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 android.internal.statusbar
+
+import android.app.Notification
+import android.content.ComponentName
+import android.graphics.Rect
+import android.graphics.drawable.Icon
+import android.hardware.biometrics.IBiometricContextListener
+import android.hardware.biometrics.IBiometricSysuiReceiver
+import android.hardware.biometrics.PromptInfo
+import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback
+import android.media.INearbyMediaDevicesProvider
+import android.media.MediaRoute2Info
+import android.net.Uri
+import android.os.Bundle
+import android.os.IBinder
+import android.os.UserHandle
+import android.util.ArrayMap
+import android.view.KeyEvent
+import com.android.internal.logging.InstanceId
+import com.android.internal.statusbar.IAddTileResultCallback
+import com.android.internal.statusbar.ISessionListener
+import com.android.internal.statusbar.IStatusBar
+import com.android.internal.statusbar.IStatusBarService
+import com.android.internal.statusbar.IUndoMediaTransferCallback
+import com.android.internal.statusbar.LetterboxDetails
+import com.android.internal.statusbar.NotificationVisibility
+import com.android.internal.statusbar.RegisterStatusBarResult
+import com.android.internal.statusbar.StatusBarIcon
+import com.android.internal.view.AppearanceRegion
+import org.mockito.kotlin.mock
+
+class FakeStatusBarService : IStatusBarService.Stub() {
+
+ var registeredStatusBar: IStatusBar? = null
+ private set
+
+ var statusBarIcons =
+ ArrayMap<String, StatusBarIcon>().also {
+ it["slot1"] = mock<StatusBarIcon>()
+ it["slot2"] = mock<StatusBarIcon>()
+ }
+ var disabledFlags1 = 1234567
+ var appearance = 123
+ var appearanceRegions =
+ arrayOf(
+ AppearanceRegion(
+ /* appearance = */ 123,
+ /* bounds = */ Rect(/* left= */ 4, /* top= */ 3, /* right= */ 2, /* bottom= */ 1),
+ ),
+ AppearanceRegion(
+ /* appearance = */ 345,
+ /* bounds = */ Rect(/* left= */ 1, /* top= */ 2, /* right= */ 3, /* bottom= */ 4),
+ ),
+ )
+ var imeWindowVis = 987
+ var imeBackDisposition = 654
+ var showImeSwitcher = true
+ var disabledFlags2 = 7654321
+ var navbarColorManagedByIme = true
+ var behavior = 234
+ var requestedVisibleTypes = 345
+ var packageName = "fake.bar.ser.vice"
+ var transientBarTypes = 0
+ var letterboxDetails =
+ arrayOf(
+ LetterboxDetails(
+ /* letterboxInnerBounds = */ Rect(
+ /* left= */ 5,
+ /* top= */ 6,
+ /* right= */ 7,
+ /* bottom= */ 8,
+ ),
+ /* letterboxFullBounds = */ Rect(
+ /* left= */ 1,
+ /* top= */ 2,
+ /* right= */ 3,
+ /* bottom= */ 4,
+ ),
+ /* appAppearance = */ 123,
+ )
+ )
+
+ override fun expandNotificationsPanel() {}
+
+ override fun collapsePanels() {}
+
+ override fun togglePanel() {}
+
+ override fun disable(what: Int, token: IBinder, pkg: String) {
+ disableForUser(what, token, pkg, userId = 0)
+ }
+
+ override fun disableForUser(what: Int, token: IBinder, pkg: String, userId: Int) {}
+
+ override fun disable2(what: Int, token: IBinder, pkg: String) {
+ disable2ForUser(what, token, pkg, userId = 0)
+ }
+
+ override fun disable2ForUser(what: Int, token: IBinder, pkg: String, userId: Int) {}
+
+ override fun getDisableFlags(token: IBinder, userId: Int): IntArray {
+ return intArrayOf(disabledFlags1, disabledFlags2)
+ }
+
+ override fun setIcon(
+ slot: String,
+ iconPackage: String,
+ iconId: Int,
+ iconLevel: Int,
+ contentDescription: String,
+ ) {}
+
+ override fun setIconVisibility(slot: String, visible: Boolean) {}
+
+ override fun removeIcon(slot: String) {}
+
+ override fun setImeWindowStatus(
+ displayId: Int,
+ vis: Int,
+ backDisposition: Int,
+ showImeSwitcher: Boolean,
+ ) {}
+
+ override fun expandSettingsPanel(subPanel: String) {}
+
+ override fun registerStatusBar(callbacks: IStatusBar): RegisterStatusBarResult {
+ registeredStatusBar = callbacks
+ return RegisterStatusBarResult(
+ statusBarIcons,
+ disabledFlags1,
+ appearance,
+ appearanceRegions,
+ imeWindowVis,
+ imeBackDisposition,
+ showImeSwitcher,
+ disabledFlags2,
+ navbarColorManagedByIme,
+ behavior,
+ requestedVisibleTypes,
+ packageName,
+ transientBarTypes,
+ letterboxDetails,
+ )
+ }
+
+ override fun onPanelRevealed(clearNotificationEffects: Boolean, numItems: Int) {}
+
+ override fun onPanelHidden() {}
+
+ override fun clearNotificationEffects() {}
+
+ override fun onNotificationClick(key: String, nv: NotificationVisibility) {}
+
+ override fun onNotificationActionClick(
+ key: String,
+ actionIndex: Int,
+ action: Notification.Action,
+ nv: NotificationVisibility,
+ generatedByAssistant: Boolean,
+ ) {}
+
+ override fun onNotificationError(
+ pkg: String,
+ tag: String,
+ id: Int,
+ uid: Int,
+ initialPid: Int,
+ message: String,
+ userId: Int,
+ ) {}
+
+ override fun onClearAllNotifications(userId: Int) {}
+
+ override fun onNotificationClear(
+ pkg: String,
+ userId: Int,
+ key: String,
+ dismissalSurface: Int,
+ dismissalSentiment: Int,
+ nv: NotificationVisibility,
+ ) {}
+
+ override fun onNotificationVisibilityChanged(
+ newlyVisibleKeys: Array<NotificationVisibility>,
+ noLongerVisibleKeys: Array<NotificationVisibility>,
+ ) {}
+
+ override fun onNotificationExpansionChanged(
+ key: String,
+ userAction: Boolean,
+ expanded: Boolean,
+ notificationLocation: Int,
+ ) {}
+
+ override fun onNotificationDirectReplied(key: String) {}
+
+ override fun onNotificationSmartSuggestionsAdded(
+ key: String,
+ smartReplyCount: Int,
+ smartActionCount: Int,
+ generatedByAssistant: Boolean,
+ editBeforeSending: Boolean,
+ ) {}
+
+ override fun onNotificationSmartReplySent(
+ key: String,
+ replyIndex: Int,
+ reply: CharSequence,
+ notificationLocation: Int,
+ modifiedBeforeSending: Boolean,
+ ) {}
+
+ override fun onNotificationSettingsViewed(key: String) {}
+
+ override fun onNotificationBubbleChanged(key: String, isBubble: Boolean, flags: Int) {}
+
+ override fun onBubbleMetadataFlagChanged(key: String, flags: Int) {}
+
+ override fun hideCurrentInputMethodForBubbles(displayId: Int) {}
+
+ override fun grantInlineReplyUriPermission(
+ key: String,
+ uri: Uri,
+ user: UserHandle,
+ packageName: String,
+ ) {}
+
+ override fun clearInlineReplyUriPermissions(key: String) {}
+
+ override fun onNotificationFeedbackReceived(key: String, feedback: Bundle) {}
+
+ override fun onGlobalActionsShown() {}
+
+ override fun onGlobalActionsHidden() {}
+
+ override fun shutdown() {}
+
+ override fun reboot(safeMode: Boolean) {}
+
+ override fun restart() {}
+
+ override fun addTile(tile: ComponentName) {}
+
+ override fun remTile(tile: ComponentName) {}
+
+ override fun clickTile(tile: ComponentName) {}
+
+ override fun handleSystemKey(key: KeyEvent) {}
+
+ override fun getLastSystemKey(): Int {
+ return -1
+ }
+
+ override fun showPinningEnterExitToast(entering: Boolean) {}
+
+ override fun showPinningEscapeToast() {}
+
+ override fun showAuthenticationDialog(
+ promptInfo: PromptInfo,
+ sysuiReceiver: IBiometricSysuiReceiver,
+ sensorIds: IntArray,
+ credentialAllowed: Boolean,
+ requireConfirmation: Boolean,
+ userId: Int,
+ operationId: Long,
+ opPackageName: String,
+ requestId: Long,
+ ) {}
+
+ override fun onBiometricAuthenticated(modality: Int) {}
+
+ override fun onBiometricHelp(modality: Int, message: String) {}
+
+ override fun onBiometricError(modality: Int, error: Int, vendorCode: Int) {}
+
+ override fun hideAuthenticationDialog(requestId: Long) {}
+
+ override fun setBiometicContextListener(listener: IBiometricContextListener) {}
+
+ override fun setUdfpsRefreshRateCallback(callback: IUdfpsRefreshRateRequestCallback) {}
+
+ override fun showInattentiveSleepWarning() {}
+
+ override fun dismissInattentiveSleepWarning(animated: Boolean) {}
+
+ override fun startTracing() {}
+
+ override fun stopTracing() {}
+
+ override fun isTracing(): Boolean {
+ return false
+ }
+
+ override fun suppressAmbientDisplay(suppress: Boolean) {}
+
+ override fun requestTileServiceListeningState(componentName: ComponentName, userId: Int) {}
+
+ override fun requestAddTile(
+ componentName: ComponentName,
+ label: CharSequence,
+ icon: Icon,
+ userId: Int,
+ callback: IAddTileResultCallback,
+ ) {}
+
+ override fun cancelRequestAddTile(packageName: String) {}
+
+ override fun setNavBarMode(navBarMode: Int) {}
+
+ override fun getNavBarMode(): Int {
+ return -1
+ }
+
+ override fun registerSessionListener(sessionFlags: Int, listener: ISessionListener) {}
+
+ override fun unregisterSessionListener(sessionFlags: Int, listener: ISessionListener) {}
+
+ override fun onSessionStarted(sessionType: Int, instanceId: InstanceId) {}
+
+ override fun onSessionEnded(sessionType: Int, instanceId: InstanceId) {}
+
+ override fun updateMediaTapToTransferSenderDisplay(
+ displayState: Int,
+ routeInfo: MediaRoute2Info,
+ undoCallback: IUndoMediaTransferCallback,
+ ) {}
+
+ override fun updateMediaTapToTransferReceiverDisplay(
+ displayState: Int,
+ routeInfo: MediaRoute2Info,
+ appIcon: Icon,
+ appName: CharSequence,
+ ) {}
+
+ override fun registerNearbyMediaDevicesProvider(provider: INearbyMediaDevicesProvider) {}
+
+ override fun unregisterNearbyMediaDevicesProvider(provider: INearbyMediaDevicesProvider) {}
+
+ override fun showRearDisplayDialog(currentBaseState: Int) {}
+}
diff --git a/packages/SystemUI/tests/utils/src/android/internal/statusbar/StatusBarServiceKosmos.kt b/packages/SystemUI/tests/utils/src/android/internal/statusbar/StatusBarServiceKosmos.kt
new file mode 100644
index 0000000..1304161
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/internal/statusbar/StatusBarServiceKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * 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 android.internal.statusbar
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeStatusBarService by Kosmos.Fixture { FakeStatusBarService() }
+
+var Kosmos.statusBarService by Kosmos.Fixture { fakeStatusBarService }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/DemoModeKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/DemoModeKosmos.kt
new file mode 100644
index 0000000..39384fd
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/DemoModeKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * 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
+
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.mockDemoModeController by Kosmos.Fixture { mock<DemoModeController>() }
+
+var Kosmos.demoModeController by Kosmos.Fixture { mockDemoModeController }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/InitControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/InitControllerKosmos.kt
new file mode 100644
index 0000000..13169e1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/InitControllerKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * 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
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.initController by Kosmos.Fixture { InitController() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelKosmos.kt
new file mode 100644
index 0000000..1c84133
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelKosmos.kt
@@ -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.systemui.communal.ui.viewmodel
+
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+
+val Kosmos.communalUserActionsViewModel by Fixture {
+ CommunalUserActionsViewModel(
+ deviceUnlockedInteractor = deviceUnlockedInteractor,
+ shadeInteractor = shadeInteractor,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt
index 3a59f6a..601c145 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt
@@ -18,6 +18,8 @@
package com.android.systemui.keyguard.data.repository
import android.content.Context
+import androidx.collection.ArrayMap
+import com.android.internal.statusbar.StatusBarIcon
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.settings.DisplayTracker
import com.android.systemui.statusbar.CommandQueue
@@ -31,6 +33,11 @@
CommandQueue(mock(Context::class.java), mock(DisplayTracker::class.java)) {
private val callbacks = mutableListOf<Callbacks>()
+ val icons = ArrayMap<String, StatusBarIcon>()
+
+ private val perDisplayDisableFlags1 = mutableMapOf<Int, Int>()
+ private val perDisplayDisableFlags2 = mutableMapOf<Int, Int>()
+
override fun addCallback(callback: Callbacks) {
callbacks.add(callback)
}
@@ -44,6 +51,23 @@
}
fun callbackCount(): Int = callbacks.size
+
+ override fun setIcon(slot: String, icon: StatusBarIcon) {
+ icons[slot] = icon
+ }
+
+ override fun disable(displayId: Int, state1: Int, state2: Int, animate: Boolean) {
+ perDisplayDisableFlags1[displayId] = state1
+ perDisplayDisableFlags2[displayId] = state2
+ }
+
+ override fun disable(displayId: Int, state1: Int, state2: Int) {
+ disable(displayId, state1, state2, /* animate= */ false)
+ }
+
+ fun disableFlags1ForDisplay(displayId: Int) = perDisplayDisableFlags1[displayId]
+
+ fun disableFlags2ForDisplay(displayId: Int) = perDisplayDisableFlags2[displayId]
}
@Module
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/NavigationBarControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/NavigationBarControllerKosmos.kt
new file mode 100644
index 0000000..9e2039e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/NavigationBarControllerKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.navigationbar
+
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.kotlin.mock
+
+val Kosmos.mockNavigationBarController by Kosmos.Fixture { mock<NavigationBarController>() }
+
+var Kosmos.navigationBarController by Kosmos.Fixture { mockNavigationBarController }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/PluginDependencyKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/PluginDependencyKosmos.kt
new file mode 100644
index 0000000..f1388e9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/PluginDependencyKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.plugins
+
+import android.testing.LeakCheck
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.utils.leaks.FakePluginManager
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.mock
+
+val Kosmos.leakCheck by Kosmos.Fixture { LeakCheck() }
+
+val Kosmos.fakePluginManager by Kosmos.Fixture { FakePluginManager(leakCheck) }
+
+var Kosmos.pluginManager by Kosmos.Fixture { fakePluginManager }
+
+val Kosmos.pluginDependencyProvider by Kosmos.Fixture { PluginDependencyProvider { pluginManager } }
+
+val Kosmos.mockPluginDependencyProvider by Kosmos.Fixture { mock<PluginDependencyProvider>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index 737aaf2..f842db4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -1,7 +1,9 @@
package com.android.systemui.scene
+import android.view.View
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.haptics.msdl.msdlPlayer
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.power.domain.interactor.powerInteractor
@@ -13,11 +15,13 @@
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.FakeOverlay
import com.android.systemui.scene.ui.viewmodel.SceneContainerGestureFilter
+import com.android.systemui.scene.ui.viewmodel.SceneContainerHapticsViewModel
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
import com.android.systemui.settings.displayTracker
import com.android.systemui.shade.domain.interactor.shadeInteractor
import kotlinx.coroutines.flow.MutableStateFlow
+import org.mockito.kotlin.mock
var Kosmos.sceneKeys by Fixture {
listOf(
@@ -68,18 +72,32 @@
}
val Kosmos.sceneContainerViewModel by Fixture {
- SceneContainerViewModel(
- sceneInteractor = sceneInteractor,
- falsingInteractor = falsingInteractor,
- powerInteractor = powerInteractor,
- shadeInteractor = shadeInteractor,
- splitEdgeDetector = splitEdgeDetector,
- gestureFilterFactory = sceneContainerGestureFilterFactory,
- displayId = displayTracker.defaultDisplayId,
- motionEventHandlerReceiver = {},
- logger = sceneLogger,
- )
- .apply { setTransitionState(transitionState) }
+ sceneContainerViewModelFactory.create(mock<View>(), displayTracker.defaultDisplayId, {}).apply {
+ setTransitionState(transitionState)
+ }
+}
+
+val Kosmos.sceneContainerViewModelFactory by Fixture {
+ object : SceneContainerViewModel.Factory {
+ override fun create(
+ view: View,
+ displayId: Int,
+ motionEventHandlerReceiver: (SceneContainerViewModel.MotionEventHandler?) -> Unit,
+ ): SceneContainerViewModel =
+ SceneContainerViewModel(
+ sceneInteractor = sceneInteractor,
+ falsingInteractor = falsingInteractor,
+ powerInteractor = powerInteractor,
+ shadeInteractor = shadeInteractor,
+ splitEdgeDetector = splitEdgeDetector,
+ logger = sceneLogger,
+ gestureFilterFactory = sceneContainerGestureFilterFactory,
+ hapticsViewModelFactory = sceneContainerHapticsViewModelFactory,
+ view = view,
+ displayId = displayId,
+ motionEventHandlerReceiver = motionEventHandlerReceiver,
+ )
+ }
}
val Kosmos.sceneContainerGestureFilterFactory by Fixture {
@@ -92,3 +110,16 @@
}
}
}
+
+val Kosmos.sceneContainerHapticsViewModelFactory by Fixture {
+ object : SceneContainerHapticsViewModel.Factory {
+ override fun create(view: View): SceneContainerHapticsViewModel {
+ return SceneContainerHapticsViewModel(
+ view = view,
+ sceneInteractor = sceneInteractor,
+ shadeInteractor = shadeInteractor,
+ msdlPlayer = msdlPlayer,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt
index 1ceab68..a9f9c82 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt
@@ -20,3 +20,13 @@
import com.android.systemui.util.mockito.mock
var Kosmos.shadeViewController by Kosmos.Fixture { mock<ShadeViewController>() }
+
+val Kosmos.mockNotificationShadeWindowViewController by
+ Kosmos.Fixture { mock<NotificationShadeWindowViewController>() }
+
+var Kosmos.notificationShadeWindowViewController by
+ Kosmos.Fixture { mockNotificationShadeWindowViewController }
+
+val Kosmos.mockShadeSurface by Kosmos.Fixture { mock<ShadeSurface>() }
+
+var Kosmos.shadeSurface by Kosmos.Fixture { mockShadeSurface }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelKosmos.kt
index 48c5121..0aeea4e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelKosmos.kt
@@ -19,11 +19,13 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.qs.ui.adapter.qsSceneAdapter
+import com.android.systemui.scene.domain.interactor.sceneBackInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
val Kosmos.shadeUserActionsViewModel: ShadeUserActionsViewModel by Fixture {
ShadeUserActionsViewModel(
qsSceneAdapter = qsSceneAdapter,
shadeInteractor = shadeInteractor,
+ sceneBackInteractor = sceneBackInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/CommandQueueKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/CommandQueueKosmos.kt
index 27f7f68..f571c1b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/CommandQueueKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/CommandQueueKosmos.kt
@@ -19,4 +19,10 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.util.mockito.mock
-var Kosmos.commandQueue by Kosmos.Fixture { mock<CommandQueue>() }
+val Kosmos.mockCommandQueue by Kosmos.Fixture { mock<CommandQueue>() }
+
+var Kosmos.commandQueue by Kosmos.Fixture { mockCommandQueue }
+
+val Kosmos.mockCommandQueueCallbacks by Kosmos.Fixture { mock<CommandQueue.Callbacks>() }
+
+var Kosmos.commandQueueCallbacks by Kosmos.Fixture { mockCommandQueue }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt
index 554bdbe..d436cd4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt
@@ -19,5 +19,7 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.util.mockito.mock
-var Kosmos.notificationRemoteInputManager by
+val Kosmos.mockNotificationRemoteInputManager by
Kosmos.Fixture { mock<NotificationRemoteInputManager>() }
+
+var Kosmos.notificationRemoteInputManager by Kosmos.Fixture { mockNotificationRemoteInputManager }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/CommandQueueInitializerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/CommandQueueInitializerKosmos.kt
new file mode 100644
index 0000000..cba4e8e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/CommandQueueInitializerKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.core
+
+import android.content.testableContext
+import android.internal.statusbar.fakeStatusBarService
+import com.android.systemui.initController
+import com.android.systemui.keyguard.data.repository.fakeCommandQueue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.navigationbar.mockNavigationBarController
+import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
+import com.android.systemui.statusbar.mockCommandQueueCallbacks
+
+var Kosmos.commandQueueInitializer by
+ Kosmos.Fixture {
+ CommandQueueInitializer(
+ testableContext,
+ fakeCommandQueue,
+ { mockCommandQueueCallbacks },
+ fakeStatusBarModeRepository,
+ initController,
+ fakeStatusBarService,
+ mockNavigationBarController,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/FakeStatusBarInitializer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/FakeStatusBarInitializer.kt
new file mode 100644
index 0000000..edd6604
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/FakeStatusBarInitializer.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.core
+
+import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewUpdatedListener
+import com.android.systemui.statusbar.phone.PhoneStatusBarTransitions
+import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
+
+class FakeStatusBarInitializer(
+ private val statusBarViewController: PhoneStatusBarViewController,
+ private val statusBarTransitions: PhoneStatusBarTransitions,
+) : StatusBarInitializer {
+
+ override var statusBarViewUpdatedListener: OnStatusBarViewUpdatedListener? = null
+ set(value) {
+ field = value
+ value?.onStatusBarViewUpdated(statusBarViewController, statusBarTransitions)
+ }
+
+ override fun initializeStatusBar() {}
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt
new file mode 100644
index 0000000..d103200
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.core
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.phone.phoneStatusBarTransitions
+import com.android.systemui.statusbar.phone.phoneStatusBarViewController
+
+val Kosmos.fakeStatusBarInitializer by
+ Kosmos.Fixture {
+ FakeStatusBarInitializer(phoneStatusBarViewController, phoneStatusBarTransitions)
+ }
+
+var Kosmos.statusBarInitializer by Kosmos.Fixture { fakeStatusBarInitializer }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt
new file mode 100644
index 0000000..c53e44d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.core
+
+import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.mockDemoModeController
+import com.android.systemui.plugins.mockPluginDependencyProvider
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.shade.mockNotificationShadeWindowViewController
+import com.android.systemui.shade.mockShadeSurface
+import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
+import com.android.systemui.statusbar.mockNotificationRemoteInputManager
+import com.android.systemui.statusbar.phone.mockAutoHideController
+import com.android.systemui.statusbar.window.data.repository.statusBarWindowStateRepositoryStore
+import com.android.systemui.statusbar.window.fakeStatusBarWindowController
+import com.android.wm.shell.bubbles.bubblesOptional
+
+val Kosmos.statusBarOrchestrator by
+ Kosmos.Fixture {
+ StatusBarOrchestrator(
+ applicationCoroutineScope,
+ fakeStatusBarInitializer,
+ fakeStatusBarWindowController,
+ fakeStatusBarModeRepository,
+ mockDemoModeController,
+ mockPluginDependencyProvider,
+ mockAutoHideController,
+ mockNotificationRemoteInputManager,
+ { mockNotificationShadeWindowViewController },
+ mockShadeSurface,
+ bubblesOptional,
+ statusBarWindowStateRepositoryStore,
+ powerInteractor,
+ primaryBouncerInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/AutoHideKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/AutoHideKosmos.kt
new file mode 100644
index 0000000..090ce31
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/AutoHideKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.phone
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.mockAutoHideController by Kosmos.Fixture { mock<AutoHideController>() }
+
+var Kosmos.autoHideController by Kosmos.Fixture { mockAutoHideController }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/PhoneStatusBarKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/PhoneStatusBarKosmos.kt
new file mode 100644
index 0000000..603ee08
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/PhoneStatusBarKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.phone
+
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.Mockito.mock
+
+val Kosmos.mockPhoneStatusBarViewController: PhoneStatusBarViewController by
+ Kosmos.Fixture { mock(PhoneStatusBarViewController::class.java) }
+
+var Kosmos.phoneStatusBarViewController by Kosmos.Fixture { mockPhoneStatusBarViewController }
+
+val Kosmos.mockPhoneStatusBarTransitions: PhoneStatusBarTransitions by
+ Kosmos.Fixture { mock(PhoneStatusBarTransitions::class.java) }
+
+var Kosmos.phoneStatusBarTransitions by Kosmos.Fixture { mockPhoneStatusBarTransitions }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowController.kt
new file mode 100644
index 0000000..528c9d9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowController.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.window
+
+import android.view.View
+import android.view.ViewGroup
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.fragments.FragmentHostManager
+import java.util.Optional
+
+class FakeStatusBarWindowController : StatusBarWindowController {
+
+ var isAttached = false
+ private set
+
+ override val statusBarHeight: Int = 0
+
+ override fun refreshStatusBarHeight() {}
+
+ override fun attach() {
+ isAttached = true
+ }
+
+ override fun addViewToWindow(view: View, layoutParams: ViewGroup.LayoutParams) {}
+
+ override val backgroundView: View
+ get() = throw NotImplementedError()
+
+ override val fragmentHostManager: FragmentHostManager
+ get() = throw NotImplementedError()
+
+ override fun wrapAnimationControllerIfInStatusBar(
+ rootView: View,
+ animationController: ActivityTransitionAnimator.Controller,
+ ): Optional<ActivityTransitionAnimator.Controller> = Optional.empty()
+
+ override fun setForceStatusBarVisible(forceStatusBarVisible: Boolean) {}
+
+ override fun setOngoingProcessRequiresStatusBarVisible(visible: Boolean) {}
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
new file mode 100644
index 0000000..c198b35
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.window
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeStatusBarWindowController by Kosmos.Fixture { FakeStatusBarWindowController() }
+
+var Kosmos.statusBarWindowController by Kosmos.Fixture { fakeStatusBarWindowController }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/FakeStatusBarWindowStatePerDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/FakeStatusBarWindowStatePerDisplayRepository.kt
new file mode 100644
index 0000000..6532a7e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/FakeStatusBarWindowStatePerDisplayRepository.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.window.data.repository
+
+import android.view.Display
+import com.android.systemui.statusbar.window.data.model.StatusBarWindowState
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeStatusBarWindowStateRepositoryStore : StatusBarWindowStateRepositoryStore {
+
+ private val perDisplayRepos = mutableMapOf<Int, FakeStatusBarWindowStatePerDisplayRepository>()
+
+ override val defaultDisplay: FakeStatusBarWindowStatePerDisplayRepository =
+ forDisplay(Display.DEFAULT_DISPLAY)
+
+ override fun forDisplay(displayId: Int): FakeStatusBarWindowStatePerDisplayRepository =
+ perDisplayRepos.computeIfAbsent(displayId) {
+ FakeStatusBarWindowStatePerDisplayRepository()
+ }
+}
+
+class FakeStatusBarWindowStatePerDisplayRepository : StatusBarWindowStatePerDisplayRepository {
+
+ private val _windowState = MutableStateFlow(StatusBarWindowState.Hidden)
+
+ override val windowState = _windowState.asStateFlow()
+
+ fun setWindowState(state: StatusBarWindowState) {
+ _windowState.value = state
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt
index e2b7f5f..2205a3b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt
@@ -21,6 +21,9 @@
import com.android.systemui.settings.displayTracker
import com.android.systemui.statusbar.commandQueue
+val Kosmos.fakeStatusBarWindowStateRepositoryStore by
+ Kosmos.Fixture { FakeStatusBarWindowStateRepositoryStore() }
+
class KosmosStatusBarWindowStatePerDisplayRepositoryFactory(private val kosmos: Kosmos) :
StatusBarWindowStatePerDisplayRepositoryFactory {
override fun create(displayId: Int): StatusBarWindowStatePerDisplayRepositoryImpl {
@@ -32,7 +35,7 @@
}
}
-val Kosmos.statusBarWindowStateRepositoryStore by
+var Kosmos.statusBarWindowStateRepositoryStore: StatusBarWindowStateRepositoryStore by
Kosmos.Fixture {
StatusBarWindowStateRepositoryStoreImpl(
displayId = displayTracker.defaultDisplayId,
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java
index fbe593f..682eb76 100644
--- a/services/core/java/com/android/server/PackageWatchdog.java
+++ b/services/core/java/com/android/server/PackageWatchdog.java
@@ -25,6 +25,7 @@
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -91,6 +92,7 @@
* Monitors the health of packages on the system and notifies interested observers when packages
* fail. On failure, the registered observer with the least user impacting mitigation will
* be notified.
+ * @hide
*/
public class PackageWatchdog {
private static final String TAG = "PackageWatchdog";
@@ -108,13 +110,25 @@
private static final long NUMBER_OF_NATIVE_CRASH_POLLS = 10;
+ /** Reason for package failure could not be determined. */
public static final int FAILURE_REASON_UNKNOWN = 0;
+
+ /** The package had a native crash. */
public static final int FAILURE_REASON_NATIVE_CRASH = 1;
+
+ /** The package failed an explicit health check. */
public static final int FAILURE_REASON_EXPLICIT_HEALTH_CHECK = 2;
+
+ /** The app crashed. */
public static final int FAILURE_REASON_APP_CRASH = 3;
+
+ /** The app was not responding. */
public static final int FAILURE_REASON_APP_NOT_RESPONDING = 4;
+
+ /** The device was boot looping. */
public static final int FAILURE_REASON_BOOT_LOOP = 5;
+ /** @hide */
@IntDef(prefix = { "FAILURE_REASON_" }, value = {
FAILURE_REASON_UNKNOWN,
FAILURE_REASON_NATIVE_CRASH,
@@ -186,7 +200,8 @@
// aborted.
private static final String METADATA_FILE = "/metadata/watchdog/mitigation_count.txt";
- @GuardedBy("PackageWatchdog.class")
+ private static final Object sPackageWatchdogLock = new Object();
+ @GuardedBy("sPackageWatchdogLock")
private static PackageWatchdog sPackageWatchdog;
private final Object mLock = new Object();
@@ -278,8 +293,8 @@
}
/** Creates or gets singleton instance of PackageWatchdog. */
- public static PackageWatchdog getInstance(Context context) {
- synchronized (PackageWatchdog.class) {
+ public static @NonNull PackageWatchdog getInstance(@NonNull Context context) {
+ synchronized (sPackageWatchdogLock) {
if (sPackageWatchdog == null) {
new PackageWatchdog(context);
}
@@ -290,6 +305,7 @@
/**
* Called during boot to notify when packages are ready on the device so we can start
* binding.
+ * @hide
*/
public void onPackagesReady() {
synchronized (mLock) {
@@ -311,6 +327,7 @@
*
* <p>Observers are expected to call this on boot. It does not specify any packages but
* it will resume observing any packages requested from a previous boot.
+ * @hide
*/
public void registerHealthObserver(PackageHealthObserver observer) {
synchronized (mLock) {
@@ -344,6 +361,7 @@
*
* <p>If {@code durationMs} is less than 1, a default monitoring duration
* {@link #DEFAULT_OBSERVING_DURATION_MS} will be used.
+ * @hide
*/
public void startObservingHealth(PackageHealthObserver observer, List<String> packageNames,
long durationMs) {
@@ -407,6 +425,7 @@
* Unregisters {@code observer} from listening to package failure.
* Additionally, this stops observing any packages that may have previously been observed
* even from a previous boot.
+ * @hide
*/
public void unregisterHealthObserver(PackageHealthObserver observer) {
mLongTaskHandler.post(() -> {
@@ -425,7 +444,7 @@
*
* <p>This method could be called frequently if there is a severe problem on the device.
*/
- public void onPackageFailure(List<VersionedPackage> packages,
+ public void onPackageFailure(@NonNull List<VersionedPackage> packages,
@FailureReasons int failureReason) {
if (packages == null) {
Slog.w(TAG, "Could not resolve a list of failing packages");
@@ -566,6 +585,7 @@
*
* Note: PackageWatchdog considers system_server restart loop as bootloop. Full reboots
* are not counted in bootloop.
+ * @hide
*/
@SuppressWarnings("GuardedBy")
public void noteBoot() {
@@ -620,7 +640,7 @@
// TODO(b/120598832): Optimize write? Maybe only write a separate smaller file? Also
// avoid holding lock?
// This currently adds about 7ms extra to shutdown thread
- /** Writes the package information to file during shutdown. */
+ /** @hide Writes the package information to file during shutdown. */
public void writeNow() {
synchronized (mLock) {
// Must only run synchronous tasks as this runs on the ShutdownThread and no other
@@ -674,6 +694,7 @@
* Since this method can eventually trigger a rollback, it should be called
* only once boot has completed {@code onBootCompleted} and not earlier, because the install
* session must be entirely completed before we try to rollback.
+ * @hide
*/
public void scheduleCheckAndMitigateNativeCrashes() {
Slog.i(TAG, "Scheduling " + mNumberOfNativeCrashPollsRemaining + " polls to check "
@@ -695,7 +716,9 @@
return mPackagesExemptFromImpactLevelThreshold;
}
- /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}. */
+ /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}.
+ * @hide
+ */
@Retention(SOURCE)
@IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_LEVEL_0,
PackageHealthObserverImpact.USER_IMPACT_LEVEL_10,
@@ -787,7 +810,7 @@
* Identifier for the observer, should not change across device updates otherwise the
* watchdog may drop observing packages with the old name.
*/
- String getUniqueIdentifier();
+ @NonNull String getUniqueIdentifier();
/**
* An observer will not be pruned if this is set, even if the observer is not explicitly
@@ -804,7 +827,7 @@
* <p> A persistent observer may choose to start observing certain failing packages, even if
* it has not explicitly asked to watch the package with {@link #startObservingHealth}.
*/
- default boolean mayObservePackage(String packageName) {
+ default boolean mayObservePackage(@NonNull String packageName) {
return false;
}
}
@@ -1240,7 +1263,7 @@
}
}
- /** Convert a {@code LongArrayQueue} to a String of comma-separated values. */
+ /** @hide Convert a {@code LongArrayQueue} to a String of comma-separated values. */
public static String longArrayQueueToString(LongArrayQueue queue) {
if (queue.size() > 0) {
StringBuilder sb = new StringBuilder();
@@ -1254,7 +1277,7 @@
return "";
}
- /** Parse a comma-separated String of longs into a LongArrayQueue. */
+ /** @hide Parse a comma-separated String of longs into a LongArrayQueue. */
public static LongArrayQueue parseLongArrayQueue(String commaSeparatedValues) {
LongArrayQueue result = new LongArrayQueue();
if (!TextUtils.isEmpty(commaSeparatedValues)) {
@@ -1268,7 +1291,7 @@
/** Dump status of every observer in mAllObservers. */
- public void dump(PrintWriter pw) {
+ public void dump(@NonNull PrintWriter pw) {
IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
ipw.println("Package Watchdog status");
ipw.increaseIndent();
@@ -1395,6 +1418,7 @@
/**
* Increments failure counts of {@code packageName}.
* @returns {@code true} if failure threshold is exceeded, {@code false} otherwise
+ * @hide
*/
@GuardedBy("mLock")
public boolean onPackageFailureLocked(String packageName) {
@@ -1514,6 +1538,7 @@
}
}
+ /** @hide */
@Retention(SOURCE)
@IntDef(value = {
HealthCheckState.ACTIVE,
@@ -1603,7 +1628,9 @@
updateHealthCheckStateLocked();
}
- /** Writes the salient fields to disk using {@code out}. */
+ /** Writes the salient fields to disk using {@code out}.
+ * @hide
+ */
@GuardedBy("mLock")
public void writeLocked(TypedXmlSerializer out) throws IOException {
out.startTag(null, TAG_PACKAGE);
diff --git a/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java b/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java
index 5f2fbce..8a81aaa 100644
--- a/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java
+++ b/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java
@@ -23,7 +23,10 @@
import com.android.server.SystemService;
-/** This class encapsulate the lifecycle methods of CrashRecovery module. */
+/** This class encapsulate the lifecycle methods of CrashRecovery module.
+ *
+ * @hide
+ */
public class CrashRecoveryModule {
private static final String TAG = "CrashRecoveryModule";
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 1f9eb08..67e2ca2 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -48,6 +48,7 @@
import static android.provider.Settings.Secure.RESOLUTION_MODE_FULL;
import static android.provider.Settings.Secure.RESOLUTION_MODE_HIGH;
import static android.provider.Settings.Secure.RESOLUTION_MODE_UNKNOWN;
+import static android.text.TextUtils.formatSimple;
import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID;
import static com.android.server.display.layout.Layout.Display.POSITION_REAR;
@@ -279,6 +280,8 @@
private InputManagerInternal mInputManagerInternal;
private ActivityManagerInternal mActivityManagerInternal;
private final UidImportanceListener mUidImportanceListener = new UidImportanceListener();
+ private final DisplayFrozenProcessListener mDisplayFrozenProcessListener;
+
@Nullable
private IMediaProjectionManager mProjectionService;
private DeviceStateManagerInternal mDeviceStateManager;
@@ -321,6 +324,12 @@
@GuardedBy("mSyncRoot")
private final SparseArray<CallbackRecord> mCallbacks = new SparseArray<>();
+ // All callback records indexed by [uid][pid], for fast lookup by uid.
+ // This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
+ @GuardedBy("mSyncRoot")
+ private final SparseArray<SparseArray<CallbackRecord>> mCallbackRecordByPidByUid =
+ new SparseArray<>();
+
/**
* All {@link IVirtualDevice} and {@link DisplayWindowPolicyController}s indexed by
* {@link DisplayInfo#displayId}.
@@ -472,6 +481,7 @@
// Pending callback records indexed by calling process uid and pid.
// Must be used outside of the lock mSyncRoot and should be self-locked.
+ // This is only used when {@link deferDisplayEventsWhenFrozen()} is false.
@GuardedBy("mPendingCallbackSelfLocked")
private final SparseArray<SparseArray<PendingCallback>> mPendingCallbackSelfLocked =
new SparseArray<>();
@@ -611,6 +621,7 @@
mFlags = injector.getFlags();
mHandler = new DisplayManagerHandler(displayThreadLooper);
mHandlerExecutor = new HandlerExecutor(mHandler);
+ mDisplayFrozenProcessListener = new DisplayFrozenProcessListener();
mUiHandler = UiThread.getHandler();
mDisplayDeviceRepo = new DisplayDeviceRepository(mSyncRoot, mPersistentDataStore);
mLogicalDisplayMapper = new LogicalDisplayMapper(mContext,
@@ -1034,10 +1045,14 @@
private class UidImportanceListener implements ActivityManager.OnUidImportanceListener {
@Override
public void onUidImportance(int uid, int importance) {
- onUidImportanceInternal(uid, importance);
+ if (deferDisplayEventsWhenFrozen()) {
+ onUidImportanceFlagged(uid, importance);
+ } else {
+ onUidImportanceUnflagged(uid, importance);
+ }
}
- private void onUidImportanceInternal(int uid, int importance) {
+ private void onUidImportanceUnflagged(int uid, int importance) {
synchronized (mPendingCallbackSelfLocked) {
if (importance >= IMPORTANCE_GONE) {
// Clean up as the app is already gone
@@ -1068,6 +1083,83 @@
mPendingCallbackSelfLocked.delete(uid);
}
}
+
+ private void onUidImportanceFlagged(int uid, int importance) {
+ final boolean cached = (importance >= IMPORTANCE_CACHED);
+ List<CallbackRecord> readyCallbackRecords = null;
+ synchronized (mSyncRoot) {
+ final SparseArray<CallbackRecord> procs = mCallbackRecordByPidByUid.get(uid);
+ if (procs == null) {
+ return;
+ }
+ if (cached) {
+ setCachedLocked(procs);
+ } else {
+ readyCallbackRecords = setUncachedLocked(procs);
+ }
+ }
+ if (readyCallbackRecords != null) {
+ // Attempt to dispatch pending events if the UID is coming out of cached state.
+ for (int i = 0; i < readyCallbackRecords.size(); i++) {
+ readyCallbackRecords.get(i).dispatchPending();
+ }
+ }
+ }
+
+ // Set all processes in the list to cached.
+ @GuardedBy("mSyncRoot")
+ private void setCachedLocked(SparseArray<CallbackRecord> procs) {
+ for (int i = 0; i < procs.size(); i++) {
+ final CallbackRecord cb = procs.valueAt(i);
+ if (cb != null) {
+ cb.setCached(true);
+ }
+ }
+ }
+
+ // Set all processes to uncached and return the list of processes that were modified.
+ @GuardedBy("mSyncRoot")
+ private List<CallbackRecord> setUncachedLocked(SparseArray<CallbackRecord> procs) {
+ ArrayList<CallbackRecord> ready = null;
+ for (int i = 0; i < procs.size(); i++) {
+ final CallbackRecord cb = procs.valueAt(i);
+ if (cb != null) {
+ if (cb.setCached(false)) {
+ if (ready == null) ready = new ArrayList<>();
+ ready.add(cb);
+ }
+ }
+ }
+ return ready;
+ }
+ }
+
+ private class DisplayFrozenProcessListener
+ implements ActivityManagerInternal.FrozenProcessListener {
+ public void onProcessFrozen(int pid) {
+ synchronized (mSyncRoot) {
+ CallbackRecord callback = mCallbacks.get(pid);
+ if (callback == null) {
+ return;
+ }
+ callback.setFrozen(true);
+ }
+ }
+
+ public void onProcessUnfrozen(int pid) {
+ // First, see if there is a callback associated with this pid. If there's no
+ // callback, then there is nothing to do.
+ CallbackRecord callback;
+ synchronized (mSyncRoot) {
+ callback = mCallbacks.get(pid);
+ if (callback == null) {
+ return;
+ }
+ callback.setFrozen(false);
+ }
+ // Attempt to dispatch pending events if the process is coming out of frozen.
+ callback.dispatchPending();
+ }
}
private class SettingsObserver extends ContentObserver {
@@ -1314,12 +1406,29 @@
}
mCallbacks.put(callingPid, record);
+ if (deferDisplayEventsWhenFrozen()) {
+ SparseArray<CallbackRecord> uidPeers = mCallbackRecordByPidByUid.get(record.mUid);
+ if (uidPeers == null) {
+ uidPeers = new SparseArray<CallbackRecord>();
+ mCallbackRecordByPidByUid.put(record.mUid, uidPeers);
+ }
+ uidPeers.put(record.mPid, record);
+ }
}
}
private void onCallbackDied(CallbackRecord record) {
synchronized (mSyncRoot) {
mCallbacks.remove(record.mPid);
+ if (deferDisplayEventsWhenFrozen()) {
+ SparseArray<CallbackRecord> uidPeers = mCallbackRecordByPidByUid.get(record.mUid);
+ if (uidPeers != null) {
+ uidPeers.remove(record.mPid);
+ if (uidPeers.size() == 0) {
+ mCallbackRecordByPidByUid.remove(record.mUid);
+ }
+ }
+ }
stopWifiDisplayScanLocked(record);
}
}
@@ -3296,12 +3405,16 @@
// After releasing the lock, send the notifications out.
for (int i = 0; i < mTempCallbacks.size(); i++) {
CallbackRecord callbackRecord = mTempCallbacks.get(i);
- deliverEventInternal(callbackRecord, displayId, event);
+ if (deferDisplayEventsWhenFrozen()) {
+ deliverEventFlagged(callbackRecord, displayId, event);
+ } else {
+ deliverEventUnflagged(callbackRecord, displayId, event);
+ }
}
mTempCallbacks.clear();
}
- private void deliverEventInternal(CallbackRecord callbackRecord, int displayId, int event) {
+ private void deliverEventUnflagged(CallbackRecord callbackRecord, int displayId, int event) {
final int uid = callbackRecord.mUid;
final int pid = callbackRecord.mPid;
if (isUidCached(uid)) {
@@ -3330,6 +3443,10 @@
}
}
+ private void deliverEventFlagged(CallbackRecord callbackRecord, int displayId, int event) {
+ callbackRecord.notifyDisplayEventAsync(displayId, event);
+ }
+
private boolean extraLogging(String packageName) {
return mExtraDisplayEventLogging && mExtraDisplayLoggingPackageName.equals(packageName);
}
@@ -3454,9 +3571,7 @@
pw.println("Callbacks: size=" + callbackCount);
pw.println("-----------------");
for (int i = 0; i < callbackCount; i++) {
- CallbackRecord callback = mCallbacks.valueAt(i);
- pw.println(" " + i + ": mPid=" + callback.mPid
- + ", mWifiDisplayScanRequested=" + callback.mWifiDisplayScanRequested);
+ pw.println(" " + i + ": " + mCallbacks.valueAt(i).dump());
}
pw.println();
@@ -3855,12 +3970,43 @@
public boolean mWifiDisplayScanRequested;
+ // A single pending event.
+ private record Event(int displayId, @DisplayEvent int event) { };
+
+ // The list of pending events. This is null until there is a pending event to be saved.
+ // This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
+ @GuardedBy("mCallback")
+ private ArrayList<Event> mPendingEvents;
+
+ // Process states: a process is ready to receive events if it is neither cached nor
+ // frozen.
+ @GuardedBy("mCallback")
+ private boolean mCached;
+ @GuardedBy("mCallback")
+ private boolean mFrozen;
+
CallbackRecord(int pid, int uid, @NonNull IDisplayManagerCallback callback,
@EventsMask long eventsMask) {
mPid = pid;
mUid = uid;
mCallback = callback;
mEventsMask = new AtomicLong(eventsMask);
+ mCached = false;
+ mFrozen = false;
+
+ if (deferDisplayEventsWhenFrozen()) {
+ // Some CallbackRecords are registered very early in system boot, before
+ // mActivityManagerInternal is initialized. If mActivityManagerInternal is null,
+ // do not register the frozen process listener. However, do verify that all such
+ // registrations are for the self pid (which can never be frozen, so the frozen
+ // process listener does not matter).
+ if (mActivityManagerInternal != null) {
+ mActivityManagerInternal.addFrozenProcessListener(pid, mHandlerExecutor,
+ mDisplayFrozenProcessListener);
+ } else if (Process.myPid() != pid) {
+ Slog.e(TAG, "DisplayListener registered too early");
+ }
+ }
String[] packageNames = mContext.getPackageManager().getPackagesForUid(uid);
mPackageName = packageNames == null ? null : packageNames[0];
@@ -3870,6 +4016,46 @@
mEventsMask.set(eventsMask);
}
+ /**
+ * Return true if the process can accept events.
+ */
+ @GuardedBy("mCallback")
+ private boolean isReadyLocked() {
+ return !mCached && !mFrozen;
+ }
+
+ /**
+ * Return true if the process is now ready and has pending events to be delivered.
+ */
+ @GuardedBy("mCallback")
+ private boolean hasPendingAndIsReadyLocked() {
+ return isReadyLocked() && mPendingEvents != null && !mPendingEvents.isEmpty();
+ }
+
+ /**
+ * Set the frozen flag for this process. Return true if the process is now ready to
+ * receive events and there are pending events to be delivered.
+ * This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
+ */
+ public boolean setFrozen(boolean frozen) {
+ synchronized (mCallback) {
+ mFrozen = frozen;
+ return hasPendingAndIsReadyLocked();
+ }
+ }
+
+ /**
+ * Set the cached flag for this process. Return true if the process is now ready to
+ * receive events and there are pending events to be delivered.
+ * This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
+ */
+ public boolean setCached(boolean cached) {
+ synchronized (mCallback) {
+ mCached = cached;
+ return hasPendingAndIsReadyLocked();
+ }
+ }
+
@Override
public void binderDied() {
if (DEBUG || extraLogging(mPackageName)) {
@@ -3885,7 +4071,7 @@
/**
* @return {@code false} if RemoteException happens; otherwise {@code true} for
* success. This returns true even if the event was deferred because the remote client is
- * cached.
+ * cached or frozen.
*/
public boolean notifyDisplayEventAsync(int displayId, @DisplayEvent int event) {
if (!shouldSendEvent(event)) {
@@ -3902,6 +4088,22 @@
return true;
}
+ if (deferDisplayEventsWhenFrozen()) {
+ synchronized (mCallback) {
+ // Add the new event to the pending list if the client frozen or cached (not
+ // ready) or if there are existing pending events. The latter condition
+ // occurs as the client is transitioning to ready but pending events have not
+ // been dispatched. The new event must be added to the pending list to
+ // preserve event ordering.
+ if (!isReadyLocked() || (mPendingEvents != null && !mPendingEvents.isEmpty())) {
+ // The client is interested in the event but is not ready to receive it.
+ // Put the event on the pending list.
+ addDisplayEvent(displayId, event);
+ return true;
+ }
+ }
+ }
+
return transmitDisplayEvent(displayId, event);
}
@@ -3948,8 +4150,81 @@
return true;
}
}
+
+ // Add a single event to the pending list, possibly combining or collapsing events in the
+ // list.
+ // This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
+ @GuardedBy("mCallback")
+ private void addDisplayEvent(int displayId, int event) {
+ if (mPendingEvents == null) {
+ mPendingEvents = new ArrayList<>();
+ }
+ if (!mPendingEvents.isEmpty()) {
+ // Ignore redundant events. Further optimization is possible by merging adjacent
+ // events.
+ Event last = mPendingEvents.get(mPendingEvents.size() - 1);
+ if (last.displayId == displayId && last.event == event) {
+ if (DEBUG) {
+ Slog.d(TAG, "Ignore redundant display event " + displayId + "/" + event
+ + " to " + mUid + "/" + mPid);
+ }
+ return;
+ }
+ }
+ mPendingEvents.add(new Event(displayId, event));
+ }
+
+ // Send all pending events. This can safely be called if the process is not ready, but it
+ // would be unusual to do so. The method returns true on success.
+ // This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
+ public boolean dispatchPending() {
+ synchronized (mCallback) {
+ if (mPendingEvents == null || mPendingEvents.isEmpty()) {
+ return true;
+ }
+ if (!isReadyLocked()) {
+ return false;
+ }
+ for (int i = 0; i < mPendingEvents.size(); i++) {
+ Event displayEvent = mPendingEvents.get(i);
+ if (DEBUG) {
+ Slog.d(TAG, "Send pending display event #" + i + " "
+ + displayEvent.displayId + "/"
+ + displayEvent.event + " to " + mUid + "/" + mPid);
+ }
+ if (!transmitDisplayEvent(displayEvent.displayId, displayEvent.event)) {
+ Slog.d(TAG, "Drop pending events for dead process " + mPid);
+ break;
+ }
+ }
+ mPendingEvents.clear();
+ return true;
+ }
+ }
+
+ // Return a string suitable for dumpsys.
+ private String dump() {
+ if (deferDisplayEventsWhenFrozen()) {
+ final String fmt =
+ "mPid=%d mUid=%d mWifiDisplayScanRequested=%s"
+ + " cached=%s frozen=%s pending=%d";
+ synchronized (mCallback) {
+ return formatSimple(fmt,
+ mPid, mUid, mWifiDisplayScanRequested, mCached, mFrozen,
+ (mPendingEvents == null) ? 0 : mPendingEvents.size());
+ }
+ } else {
+ final String fmt =
+ "mPid=%d mUid=%d mWifiDisplayScanRequested=%s";
+ return formatSimple(fmt,
+ mPid, mUid, mWifiDisplayScanRequested);
+ }
+ }
}
+ /**
+ * This is only used if {@link deferDisplayEventsWhenFrozen()} is false.
+ */
private static final class PendingCallback {
private final CallbackRecord mCallbackRecord;
private final ArrayList<Pair<Integer, Integer>> mDisplayEvents;
@@ -5504,4 +5779,11 @@
return mExternalDisplayStatsService;
}
}
+
+ /**
+ * Return the value of the pause
+ */
+ private static boolean deferDisplayEventsWhenFrozen() {
+ return com.android.server.am.Flags.deferDisplayEventsWhenFrozen();
+ }
}
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 62d8761..03fec011 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1480,29 +1480,24 @@
brightnessState = clampScreenBrightness(brightnessState);
}
- if (useDozeBrightness) {
- // TODO(b/329676661): Introduce a config property to choose between this brightness
- // strategy and DOZE_DEFAULT
- // On some devices, when auto-brightness is disabled and the device is dozing, we use
- // the current brightness setting scaled by the doze scale factor
- if ((Float.isNaN(brightnessState)
- || displayBrightnessState.getDisplayBrightnessStrategyName()
- .equals(DisplayBrightnessStrategyConstants.FALLBACK_BRIGHTNESS_STRATEGY_NAME))
- && mFlags.isDisplayOffloadEnabled()
- && mDisplayOffloadSession != null
+ if (useDozeBrightness && (Float.isNaN(brightnessState)
+ || displayBrightnessState.getDisplayBrightnessStrategyName()
+ .equals(DisplayBrightnessStrategyConstants.FALLBACK_BRIGHTNESS_STRATEGY_NAME))) {
+ if (mFlags.isDisplayOffloadEnabled() && mDisplayOffloadSession != null
&& (mAutomaticBrightnessController == null
|| !mAutomaticBrightnessStrategy.shouldUseAutoBrightness())) {
+ // TODO(b/329676661): Introduce a config property to choose between this brightness
+ // strategy and DOZE_DEFAULT
+ // On some devices, when auto-brightness is disabled and the device is dozing, we
+ // use the current brightness setting scaled by the doze scale factor
rawBrightnessState = getDozeBrightnessForOffload();
brightnessState = clampScreenBrightness(rawBrightnessState);
updateScreenBrightnessSetting = false;
mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_MANUAL);
mTempBrightnessEvent.setFlags(
mTempBrightnessEvent.getFlags() | BrightnessEvent.FLAG_DOZE_SCALE);
- }
-
- // Use default brightness when dozing unless overridden.
- if (Float.isNaN(brightnessState)
- && !mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig()) {
+ } else if (!mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig()) {
+ // Use default brightness when dozing unless overridden.
rawBrightnessState = mScreenBrightnessDozeConfig;
brightnessState = clampScreenBrightness(rawBrightnessState);
mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_DEFAULT);
diff --git a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
index f78c448..d206c66 100644
--- a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
+++ b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
@@ -99,6 +99,7 @@
// True if needing to roll back only rebootless apexes when native crash happens
private boolean mTwoPhaseRollbackEnabled;
+ /** @hide */
@VisibleForTesting
public RollbackPackageHealthObserver(Context context, ApexManager apexManager) {
mContext = context;
@@ -123,7 +124,7 @@
}
}
- RollbackPackageHealthObserver(Context context) {
+ public RollbackPackageHealthObserver(@NonNull Context context) {
this(context, ApexManager.getInstance());
}
@@ -239,8 +240,8 @@
return false;
}
-
@Override
+ @NonNull
public String getUniqueIdentifier() {
return NAME;
}
@@ -251,7 +252,7 @@
}
@Override
- public boolean mayObservePackage(String packageName) {
+ public boolean mayObservePackage(@NonNull String packageName) {
if (getAvailableRollbacks().isEmpty()) {
return false;
}
@@ -281,12 +282,14 @@
* This may cause {@code packages} to be rolled back if they crash too freqeuntly.
*/
@AnyThread
- void startObservingHealth(List<String> packages, long durationMs) {
+ @NonNull
+ public void startObservingHealth(@NonNull List<String> packages, @NonNull long durationMs) {
PackageWatchdog.getInstance(mContext).startObservingHealth(this, packages, durationMs);
}
@AnyThread
- void notifyRollbackAvailable(RollbackInfo rollback) {
+ @NonNull
+ public void notifyRollbackAvailable(@NonNull RollbackInfo rollback) {
mHandler.post(() -> {
// Enable two-phase rollback when a rebootless apex rollback is made available.
// We assume the rebootless apex is stable and is less likely to be the cause
@@ -314,7 +317,7 @@
* to check for native crashes and mitigate them if needed.
*/
@AnyThread
- void onBootCompletedAsync() {
+ public void onBootCompletedAsync() {
mHandler.post(()->onBootCompleted());
}
diff --git a/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java b/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java
index 79560ce..9cfed02 100644
--- a/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java
+++ b/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java
@@ -51,6 +51,7 @@
/**
* This class handles the logic for logging Watchdog-triggered rollback events.
+ * @hide
*/
public final class WatchdogRollbackLogger {
private static final String TAG = "WatchdogRollbackLogger";
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 946b61a..2d75f35 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -20,6 +20,7 @@
import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
import static android.Manifest.permission.READ_WALLPAPER_INTERNAL;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
+import static android.app.Flags.fixWallpaperChanged;
import static android.app.Flags.removeNextWallpaperComponent;
import static android.app.WallpaperManager.COMMAND_REAPPLY;
import static android.app.WallpaperManager.FLAG_LOCK;
@@ -349,7 +350,10 @@
if (DEBUG) {
Slog.d(TAG, "publish system wallpaper changed!");
}
- notifyWallpaperChanged(wallpaper);
+ notifyWallpaperComplete(wallpaper);
+ if (fixWallpaperChanged()) {
+ notifyWallpaperChanged(wallpaper);
+ }
}
};
@@ -369,7 +373,10 @@
if (DEBUG) {
Slog.d(TAG, "publish lock wallpaper changed!");
}
- notifyWallpaperChanged(wallpaper);
+ notifyWallpaperComplete(wallpaper);
+ if (fixWallpaperChanged()) {
+ notifyWallpaperChanged(wallpaper);
+ }
}
};
@@ -403,8 +410,11 @@
}
}
- private void notifyWallpaperChanged(WallpaperData wallpaper) {
- // Publish completion *after* we've persisted the changes
+ /*
+ * Calls wallpaper setComplete methods. Called for static wallpapers after the wallpaper is set
+ * and changes are persisted.
+ */
+ private void notifyWallpaperComplete(WallpaperData wallpaper) {
if (wallpaper.setComplete != null) {
try {
wallpaper.setComplete.onWallpaperChanged();
@@ -1787,6 +1797,9 @@
switchWallpaper(systemWallpaper, null);
// TODO(b/278261563): call notifyCallbacksLocked inside switchWallpaper
notifyCallbacksLocked(systemWallpaper);
+ if (fixWallpaperChanged()) {
+ notifyWallpaperChanged(systemWallpaper);
+ }
}
if (mLockWallpaperWaitingForUnlock) {
final WallpaperData lockWallpaper =
@@ -1794,6 +1807,9 @@
lockWallpaper.mBindSource = BindSource.SWITCH_WALLPAPER_UNLOCK_USER;
switchWallpaper(lockWallpaper, null);
notifyCallbacksLocked(lockWallpaper);
+ if (fixWallpaperChanged()) {
+ notifyWallpaperChanged(lockWallpaper);
+ }
}
// Make sure that the SELinux labeling of all the relevant files is correct.
@@ -3248,6 +3264,9 @@
}
newWallpaper.wallpaperId = makeWallpaperIdLocked();
notifyCallbacksLocked(newWallpaper);
+ if (fixWallpaperChanged()) {
+ notifyWallpaperChanged(newWallpaper);
+ }
shouldNotifyColors = true;
if (offloadColorExtraction()) {
shouldNotifyColors = false;
@@ -3316,6 +3335,21 @@
return false;
}
+ /*
+ * Attempt to bind the wallpaper given by `componentName`, returning true on success otherwise
+ * false.
+ *
+ * When called, `wallpaper` is in a deliberately inconsistent state. Most fields have been
+ * updated to describe the desired wallpaper, but the ComponentName is not updated until
+ * binding is successful. This is required for maybeDetachWallpapers() to work correctly.
+ *
+ * The late update of the component field should cause multi-threading headaches with
+ * WallpaperConnection#onServiceConnected, but doesn't because onServiceConnected required
+ * `mLock` and `bindWallpaperComponentLocked` is always called with that lock, which prevents a
+ * race condition.
+ *
+ * This is a major motivation for making WallpaperData immutable per b/267170056.
+ */
boolean bindWallpaperComponentLocked(ComponentName componentName, boolean force,
boolean fromUser, WallpaperData wallpaper, IRemoteCallback reply) {
if (DEBUG_LIVE) {
@@ -3610,8 +3644,18 @@
}
wallpaper.callbacks.finishBroadcast();
+ if (!fixWallpaperChanged()) {
+ final Intent intent = new Intent(Intent.ACTION_WALLPAPER_CHANGED);
+ intent.putExtra(WallpaperManager.EXTRA_FROM_FOREGROUND_APP,
+ wallpaper.fromForegroundApp);
+ mContext.sendBroadcastAsUser(intent, new UserHandle(mCurrentUserId));
+ }
+ }
+
+ private void notifyWallpaperChanged(WallpaperData wallpaper) {
final Intent intent = new Intent(Intent.ACTION_WALLPAPER_CHANGED);
intent.putExtra(WallpaperManager.EXTRA_FROM_FOREGROUND_APP, wallpaper.fromForegroundApp);
+ intent.putExtra(WallpaperManager.EXTRA_WHICH_WALLPAPER_CHANGED, wallpaper.mWhich);
mContext.sendBroadcastAsUser(intent, new UserHandle(mCurrentUserId));
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 14e9180..ca93075 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -6409,7 +6409,11 @@
// and the token could be null.
return;
}
- r.mDisplayContent.mAppCompatCameraPolicy.onActivityRefreshed(r);
+ final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy
+ .getAppCompatCameraPolicy(r);
+ if (cameraPolicy != null) {
+ cameraPolicy.onActivityRefreshed(r);
+ }
}
static void splashScreenAttachedLocked(IBinder token) {
@@ -9451,8 +9455,12 @@
if (!shouldBeResumed(/* activeActivity */ null)) {
return;
}
- mDisplayContent.mAppCompatCameraPolicy.onActivityConfigurationChanging(
- this, newConfig, lastReportedConfig);
+
+ final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
+ this);
+ if (cameraPolicy != null) {
+ cameraPolicy.onActivityConfigurationChanging(this, newConfig, lastReportedConfig);
+ }
}
/** Get process configuration, or global config if the process is not set. */
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
index f245efd..0e66629 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
@@ -255,8 +255,8 @@
mActivityRecord.getOverrideOrientation());
final AppCompatCameraOverrides cameraOverrides =
mActivityRecord.mAppCompatController.getAppCompatCameraOverrides();
- final AppCompatCameraPolicy cameraPolicy =
- mActivityRecord.mAppCompatController.getAppCompatCameraPolicy();
+ final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
+ mActivityRecord);
// Don't resize to split screen size when in book mode if letterbox position is centered
return (isBookMode && isNotCenteredHorizontally || isTabletopMode && isLandscape)
|| cameraOverrides.isCameraCompatSplitScreenAspectRatioAllowed()
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
index 6946b6a..3b023fe 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
@@ -72,6 +72,15 @@
float getDesiredAspectRatio(@NonNull Configuration newParentConfig,
@NonNull Rect parentBounds) {
+ // If in camera compat mode, aspect ratio from the camera compat policy has priority over
+ // default letterbox aspect ratio.
+ final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
+ mActivityRecord);
+ if (cameraPolicy != null && cameraPolicy.shouldCameraCompatControlAspectRatio(
+ mActivityRecord)) {
+ return cameraPolicy.getCameraCompatAspectRatio(mActivityRecord);
+ }
+
final float letterboxAspectRatioOverride =
mAppCompatOverrides.getAppCompatAspectRatioOverrides()
.getFixedOrientationLetterboxAspectRatio(newParentConfig);
@@ -119,10 +128,10 @@
if (aspectRatioOverrides.shouldApplyUserMinAspectRatioOverride()) {
return aspectRatioOverrides.getUserMinAspectRatio();
}
- final DisplayContent displayContent = mActivityRecord.getDisplayContent();
- final boolean shouldOverrideMinAspectRatioForCamera = displayContent != null
- && displayContent.mAppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera(
- mActivityRecord);
+ final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
+ mActivityRecord);
+ final boolean shouldOverrideMinAspectRatioForCamera = cameraPolicy != null
+ && cameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord);
if (!aspectRatioOverrides.shouldOverrideMinAspectRatio()
&& !shouldOverrideMinAspectRatioForCamera) {
if (mActivityRecord.isUniversalResizeable()) {
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
index 5338c01..f6090eb 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
@@ -18,6 +18,8 @@
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.pm.ActivityInfo.ScreenOrientation;
@@ -74,6 +76,12 @@
}
}
+ @Nullable
+ static AppCompatCameraPolicy getAppCompatCameraPolicy(@NonNull ActivityRecord activityRecord) {
+ return activityRecord.mDisplayContent != null
+ ? activityRecord.mDisplayContent.mAppCompatCameraPolicy : null;
+ }
+
/**
* "Refreshes" activity by going through "stopped -> resumed" or "paused -> resumed" cycle.
* This allows to clear cached values in apps (e.g. display or camera rotation) that influence
@@ -167,12 +175,37 @@
: SCREEN_ORIENTATION_UNSPECIFIED;
}
+ // TODO(b/369070416): have policies implement the same interface.
+ boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) {
+ return (mDisplayRotationCompatPolicy != null
+ && mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(
+ activity))
+ || (mCameraCompatFreeformPolicy != null
+ && mCameraCompatFreeformPolicy.shouldCameraCompatControlOrientation(
+ activity));
+ }
+
+ // TODO(b/369070416): have policies implement the same interface.
+ boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord activity) {
+ return (mDisplayRotationCompatPolicy != null
+ && mDisplayRotationCompatPolicy.shouldCameraCompatControlAspectRatio(
+ activity))
+ || (mCameraCompatFreeformPolicy != null
+ && mCameraCompatFreeformPolicy.shouldCameraCompatControlAspectRatio(
+ activity));
+ }
+
+ // TODO(b/369070416): have policies implement the same interface.
/**
- * @return {@code true} if the Camera is active for the provided {@link ActivityRecord}.
+ * @return {@code true} if the Camera is active for the provided {@link ActivityRecord} and
+ * any camera compat treatment could be triggered for the current windowing mode.
*/
- boolean isCameraActive(@NonNull ActivityRecord activity, boolean mustBeFullscreen) {
- return mDisplayRotationCompatPolicy != null
- && mDisplayRotationCompatPolicy.isCameraActive(activity, mustBeFullscreen);
+ private boolean isCameraRunningAndWindowingModeEligible(@NonNull ActivityRecord activity) {
+ return (mDisplayRotationCompatPolicy != null
+ && mDisplayRotationCompatPolicy.isCameraRunningAndWindowingModeEligible(activity,
+ /* mustBeFullscreen */ true))
+ || (mCameraCompatFreeformPolicy != null && mCameraCompatFreeformPolicy
+ .isCameraRunningAndWindowingModeEligible(activity));
}
@Nullable
@@ -183,12 +216,24 @@
return null;
}
+ // TODO(b/369070416): have policies implement the same interface.
+ float getCameraCompatAspectRatio(@NonNull ActivityRecord activity) {
+ float displayRotationCompatPolicyAspectRatio = mDisplayRotationCompatPolicy != null
+ ? mDisplayRotationCompatPolicy.getCameraCompatAspectRatio(activity)
+ : MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+ float cameraCompatFreeformPolicyAspectRatio = mCameraCompatFreeformPolicy != null
+ ? mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(activity)
+ : MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+ return Math.max(displayRotationCompatPolicyAspectRatio,
+ cameraCompatFreeformPolicyAspectRatio);
+ }
+
/**
* Whether we should apply the min aspect ratio per-app override only when an app is connected
* to the camera.
*/
boolean shouldOverrideMinAspectRatioForCamera(@NonNull ActivityRecord activityRecord) {
- return isCameraActive(activityRecord, /* mustBeFullscreen= */ true)
+ return isCameraRunningAndWindowingModeEligible(activityRecord)
&& activityRecord.mAppCompatController.getAppCompatCameraOverrides()
.isOverrideMinAspectRatioForCameraEnabled();
}
diff --git a/services/core/java/com/android/server/wm/AppCompatConfiguration.java b/services/core/java/com/android/server/wm/AppCompatConfiguration.java
index 42378aa..38c6de1 100644
--- a/services/core/java/com/android/server/wm/AppCompatConfiguration.java
+++ b/services/core/java/com/android/server/wm/AppCompatConfiguration.java
@@ -290,6 +290,10 @@
// is enabled and activity is connected to the camera in fullscreen.
private final boolean mIsCameraCompatSplitScreenAspectRatioEnabled;
+ // Which aspect ratio to use when camera compat treatment is enabled and an activity eligible
+ // for treatment is connected to the camera.
+ private float mCameraCompatAspectRatio;
+
// Whether activity "refresh" in camera compatibility treatment is enabled.
// See RefreshCallbackItem for context.
private boolean mIsCameraCompatTreatmentRefreshEnabled = true;
@@ -363,6 +367,8 @@
.config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled);
mIsCameraCompatSplitScreenAspectRatioEnabled = mContext.getResources().getBoolean(
R.bool.config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled);
+ mCameraCompatAspectRatio = mContext.getResources().getFloat(
+ R.dimen.config_windowManagerCameraCompatAspectRatio);
mIsPolicyForIgnoringRequestedOrientationEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled);
@@ -1320,6 +1326,31 @@
}
/**
+ * Overrides aspect ratio to use when camera compat treatment is enabled and an activity
+ * eligible for treatment is connected to the camera.
+ */
+ void setCameraCompatAspectRatio(float aspectRatio) {
+ mCameraCompatAspectRatio = aspectRatio;
+ }
+
+ /**
+ * Which aspect ratio to use when camera compat treatment is enabled and an activity eligible
+ * for treatment is connected to the camera.
+ */
+ float getCameraCompatAspectRatio() {
+ return mCameraCompatAspectRatio;
+ }
+
+ /**
+ * Resets aspect ratio to use when camera compat treatment is enabled and an activity eligible
+ * for treatment is connected to the camera.
+ */
+ void resetCameraCompatAspectRatio() {
+ mCameraCompatAspectRatio = mContext.getResources().getFloat(R.dimen
+ .config_windowManagerCameraCompatAspectRatio);
+ }
+
+ /**
* Checks whether rotation compat policy for immersive apps that prevents auto rotation
* into non-optimal screen orientation while in fullscreen is enabled at build time. This is
* used when we need to safely initialize a component before the {@link DeviceConfig} flag
diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java
index 173362c..6c344c6 100644
--- a/services/core/java/com/android/server/wm/AppCompatController.java
+++ b/services/core/java/com/android/server/wm/AppCompatController.java
@@ -16,7 +16,6 @@
package com.android.server.wm;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.content.pm.PackageManager;
import com.android.server.wm.utils.OptPropFactory;
@@ -118,14 +117,6 @@
return mAppCompatOverrides.getAppCompatResizeOverrides();
}
- @Nullable
- AppCompatCameraPolicy getAppCompatCameraPolicy() {
- if (mActivityRecord.mDisplayContent != null) {
- return mActivityRecord.mDisplayContent.mAppCompatCameraPolicy;
- }
- return null;
- }
-
@NonNull
AppCompatReachabilityPolicy getAppCompatReachabilityPolicy() {
return mAppCompatReachabilityPolicy;
diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
index 7477c62..5bd4aeb 100644
--- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
@@ -58,16 +58,17 @@
&& displayContent.getIgnoreOrientationRequest();
final boolean shouldApplyUserFullscreenOverride = mAppCompatOverrides
.getAppCompatAspectRatioOverrides().shouldApplyUserFullscreenOverride();
- final boolean isCameraActive = displayContent != null
- && displayContent.mAppCompatCameraPolicy.isCameraActive(mActivityRecord,
- /* mustBeFullscreen */ true);
+ final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy
+ .getAppCompatCameraPolicy(mActivityRecord);
+ final boolean shouldCameraCompatControlOrientation = cameraPolicy != null
+ && cameraPolicy.shouldCameraCompatControlOrientation(mActivityRecord);
if (shouldApplyUserFullscreenOverride && isIgnoreOrientationRequestEnabled
// Do not override orientation to fullscreen for camera activities.
// Fixed-orientation activities are rarely tested in other orientations, and it
// often results in sideways or stretched previews. As the camera compat treatment
// targets fixed-orientation activities, overriding the orientation disables the
// treatment.
- && !isCameraActive) {
+ && !shouldCameraCompatControlOrientation) {
Slog.v(TAG, "Requested orientation " + screenOrientationToString(candidate)
+ " for " + mActivityRecord + " is overridden to "
+ screenOrientationToString(SCREEN_ORIENTATION_USER)
@@ -113,7 +114,7 @@
// often results in sideways or stretched previews. As the camera compat treatment
// targets fixed-orientation activities, overriding the orientation disables the
// treatment.
- && !isCameraActive) {
+ && !shouldCameraCompatControlOrientation) {
Slog.v(TAG, "Requested orientation " + screenOrientationToString(candidate)
+ " for " + mActivityRecord + " is overridden to "
+ screenOrientationToString(SCREEN_ORIENTATION_USER));
@@ -192,8 +193,9 @@
+ mActivityRecord);
return true;
}
- final AppCompatCameraPolicy cameraPolicy = mActivityRecord.mAppCompatController
- .getAppCompatCameraPolicy();
+
+ final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy
+ .getAppCompatCameraPolicy(mActivityRecord);
if (cameraPolicy != null
&& cameraPolicy.isTreatmentEnabledForActivity(mActivityRecord)) {
Slog.w(TAG, "Ignoring orientation update to "
diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
index d6caa1a..290e71d 100644
--- a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
+++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
@@ -29,6 +29,7 @@
import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_180;
+import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
@@ -123,7 +124,7 @@
* </ul>
*/
@VisibleForTesting
- boolean shouldApplyFreeformTreatmentForCameraCompat(@NonNull ActivityRecord activity) {
+ boolean isCameraCompatForFreeformEnabledForActivity(@NonNull ActivityRecord activity) {
return Flags.enableCameraCompatForDesktopWindowing() && !activity.info.isChangeEnabled(
ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT);
}
@@ -170,6 +171,36 @@
return true;
}
+ boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) {
+ return isCameraRunningAndWindowingModeEligible(activity);
+ }
+
+ boolean isCameraRunningAndWindowingModeEligible(@NonNull ActivityRecord activity) {
+ return activity.inFreeformWindowingMode()
+ && mCameraStateMonitor.isCameraRunningForActivity(activity);
+ }
+
+ boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord activity) {
+ // Camera compat should direct aspect ratio when in camera compat mode, unless an app has a
+ // different camera compat aspect ratio set: this allows per-app camera compat override
+ // aspect ratio to be smaller than the default.
+ return isInCameraCompatMode(activity) && !activity.mAppCompatController
+ .getAppCompatCameraOverrides().isOverrideMinAspectRatioForCameraEnabled();
+ }
+
+ private boolean isInCameraCompatMode(@NonNull ActivityRecord activity) {
+ return activity.mAppCompatController.getAppCompatCameraOverrides()
+ .getFreeformCameraCompatMode() != CAMERA_COMPAT_FREEFORM_NONE;
+ }
+
+ float getCameraCompatAspectRatio(@NonNull ActivityRecord activityRecord) {
+ if (shouldCameraCompatControlAspectRatio(activityRecord)) {
+ return activityRecord.mWmService.mAppCompatConfiguration.getCameraCompatAspectRatio();
+ }
+
+ return MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+ }
+
private void forceUpdateActivityAndTask(ActivityRecord cameraActivity) {
cameraActivity.recomputeConfiguration();
cameraActivity.updateReportedConfigurationAndSend();
@@ -225,7 +256,7 @@
*/
private boolean isTreatmentEnabledForActivity(@NonNull ActivityRecord activity) {
int orientation = activity.getRequestedConfigurationOrientation();
- return shouldApplyFreeformTreatmentForCameraCompat(activity)
+ return isCameraCompatForFreeformEnabledForActivity(activity)
&& mCameraStateMonitor.isCameraRunningForActivity(activity)
&& orientation != ORIENTATION_UNDEFINED
&& activity.inFreeformWindowingMode()
diff --git a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java
index 6e6f76a..3b2f723 100644
--- a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java
+++ b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java
@@ -194,9 +194,10 @@
return aspectRatioOverrides.getUserMinAspectRatio();
}
- final DisplayContent dc = task.mDisplayContent;
- final boolean shouldOverrideMinAspectRatioForCamera = dc != null
- && dc.mAppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord);
+ final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
+ mActivityRecord);
+ final boolean shouldOverrideMinAspectRatioForCamera = cameraPolicy != null
+ && cameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord);
if (!aspectRatioOverrides.shouldOverrideMinAspectRatio()
&& !shouldOverrideMinAspectRatioForCamera) {
if (mActivityRecord.isUniversalResizeable()) {
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index efc3843..90f8b49 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -30,6 +30,7 @@
import static android.view.Display.TYPE_INTERNAL;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
+import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
import static com.android.server.wm.DisplayRotationReversionController.REVERSION_TYPE_CAMERA_COMPAT;
import android.annotation.NonNull;
@@ -133,6 +134,11 @@
return mLastReportedOrientation;
}
+ float getCameraCompatAspectRatio(@NonNull ActivityRecord unusedActivity) {
+ // This policy does not apply camera compat aspect ratio by default, only via overrides.
+ return MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+ }
+
@ScreenOrientation
private synchronized int getOrientationInternal() {
if (!isTreatmentEnabledForDisplay()) {
@@ -271,7 +277,7 @@
boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) {
return isTreatmentEnabledForDisplay()
- && isCameraActive(activity, /* mustBeFullscreen */ true)
+ && isCameraRunningAndWindowingModeEligible(activity, /* mustBeFullscreen */ true)
&& activity.mAppCompatController.getAppCompatCameraOverrides()
.shouldForceRotateForCameraCompat();
}
@@ -290,7 +296,17 @@
return isTreatmentEnabledForActivity(activity, /* mustBeFullscreen */ true);
}
- boolean isCameraActive(@NonNull ActivityRecord activity, boolean mustBeFullscreen) {
+ boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) {
+ return isCameraRunningAndWindowingModeEligible(activity, /* mustBeFullscreen= */ true);
+ }
+
+ boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord unusedActivity) {
+ // This policy does not apply camera compat aspect ratio by default, only via overrides.
+ return false;
+ }
+
+ boolean isCameraRunningAndWindowingModeEligible(@NonNull ActivityRecord activity,
+ boolean mustBeFullscreen) {
// Checking windowing mode on activity level because we don't want to
// apply treatment in case of activity embedding.
return (!mustBeFullscreen || !activity.inMultiWindowMode())
@@ -299,7 +315,8 @@
private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity,
boolean mustBeFullscreen) {
- return activity != null && isCameraActive(activity, mustBeFullscreen)
+ return activity != null
+ && isCameraRunningAndWindowingModeEligible(activity, mustBeFullscreen)
&& activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED
// "locked" and "nosensor" values are often used by camera apps that can't
// handle dynamic changes so we shouldn't force rotate them.
@@ -428,6 +445,7 @@
private boolean shouldOverrideMinAspectRatio(@NonNull ActivityRecord activityRecord) {
return activityRecord.mAppCompatController.getAppCompatCameraOverrides()
.isOverrideMinAspectRatioForCameraEnabled()
- && isCameraActive(activityRecord, /* mustBeFullscreen= */ true);
+ && isCameraRunningAndWindowingModeEligible(activityRecord,
+ /* mustBeFullscreen= */ true);
}
}
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index db0374e..e4fd523 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -815,8 +815,7 @@
if (mDisplayContent.getRotationAnimation() == ScreenRotationAnimation.this) {
// It also invokes kill().
mDisplayContent.setRotationAnimation(null);
- mDisplayContent.mAppCompatCameraPolicy
- .onScreenRotationAnimationFinished();
+ mDisplayContent.mAppCompatCameraPolicy.onScreenRotationAnimationFinished();
} else {
kill();
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 06d8c37..6d7396f 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -1038,6 +1038,25 @@
return 0;
}
+ private int runSetCameraCompatAspectRatio(PrintWriter pw) throws RemoteException {
+ final float aspectRatio;
+ try {
+ String arg = getNextArgRequired();
+ aspectRatio = Float.parseFloat(arg);
+ } catch (NumberFormatException e) {
+ getErrPrintWriter().println("Error: bad aspect ratio format " + e);
+ return -1;
+ } catch (IllegalArgumentException e) {
+ getErrPrintWriter().println(
+ "Error: aspect ratio should be provided as an argument " + e);
+ return -1;
+ }
+ synchronized (mInternal.mGlobalLock) {
+ mAppCompatConfiguration.setCameraCompatAspectRatio(aspectRatio);
+ }
+ return 0;
+ }
+
private int runSetLetterboxStyle(PrintWriter pw) throws RemoteException {
if (peekNextArg() == null) {
getErrPrintWriter().println("Error: No arguments provided.");
@@ -1129,6 +1148,9 @@
runSetBooleanFlag(pw,
mAppCompatConfiguration::setCameraCompatRefreshCycleThroughStopEnabled);
break;
+ case "--cameraCompatAspectRatio":
+ runSetCameraCompatAspectRatio(pw);
+ break;
default:
getErrPrintWriter().println(
"Error: Unrecognized letterbox style option: " + arg);
@@ -1220,6 +1242,9 @@
mAppCompatConfiguration
.resetCameraCompatRefreshCycleThroughStopEnabled();
break;
+ case "cameraCompatAspectRatio":
+ mAppCompatConfiguration.resetCameraCompatAspectRatio();
+ break;
default:
getErrPrintWriter().println(
"Error: Unrecognized letterbox style option: " + arg);
@@ -1330,6 +1355,7 @@
mAppCompatConfiguration.resetUserAppAspectRatioFullscreenEnabled();
mAppCompatConfiguration.resetCameraCompatRefreshEnabled();
mAppCompatConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled();
+ mAppCompatConfiguration.resetCameraCompatAspectRatio();
}
}
@@ -1619,6 +1645,11 @@
pw.println(" Whether activity \"refresh\" in camera compatibility treatment should");
pw.println(" happen using the \"stopped -> resumed\" cycle rather than");
pw.println(" \"paused -> resumed\" cycle.");
+ pw.println(" --cameraCompatAspectRatio aspectRatio");
+ pw.println(" Aspect ratio of letterbox for fixed-orientation camera apps, during ");
+ pw.println(" freeform camera compat mode. If aspectRatio <= "
+ + AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO);
+ pw.println(" it will be ignored.");
pw.println(" reset-letterbox-style [aspectRatio|cornerRadius|backgroundType");
pw.println(" |backgroundColor|wallpaperBlurRadius|wallpaperDarkScrimAlpha");
pw.println(" |horizontalPositionMultiplier|verticalPositionMultiplier");
@@ -1627,7 +1658,8 @@
pw.println(" |isTranslucentLetterboxingEnabled|isUserAppAspectRatioSettingsEnabled");
pw.println(" |persistentPositionMultiplierForHorizontalReachability");
pw.println(" |persistentPositionMultiplierForVerticalReachability");
- pw.println(" |defaultPositionMultiplierForVerticalReachability]");
+ pw.println(" |defaultPositionMultiplierForVerticalReachability");
+ pw.println(" |cameraCompatAspectRatio]");
pw.println(" Resets overrides to default values for specified properties separated");
pw.println(" by space, e.g. 'reset-letterbox-style aspectRatio cornerRadius'.");
pw.println(" If no arguments provided, all values will be reset.");
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java
index 90f6257..d00e2c6 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java
@@ -25,6 +25,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
import android.app.ActivityManager;
import android.app.Instrumentation;
@@ -38,6 +39,9 @@
import android.os.Message;
import android.os.Messenger;
import android.platform.test.annotations.AppModeSdkSandbox;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.Log;
import android.util.SparseArray;
@@ -46,15 +50,19 @@
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.compatibility.common.util.SystemUtil;
+import com.android.compatibility.common.util.TestUtils;
+import com.android.server.am.Flags;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
+import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
@@ -68,6 +76,10 @@
public class DisplayEventDeliveryTest {
private static final String TAG = "DisplayEventDeliveryTest";
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
+
private static final String NAME = TAG;
private static final int WIDTH = 720;
private static final int HEIGHT = 480;
@@ -149,7 +161,6 @@
mExpectations.offer(event);
}
-
/**
* Assert that there isn't any unexpected display event from the test activity
*/
@@ -189,19 +200,9 @@
@Parameter(0)
public int mDisplayCount;
- /**
- * True if running the test activity in cached mode
- * False if running it in non-cached mode
- */
- @Parameter(1)
- public boolean mCached;
-
- @Parameters(name = "#{index}: {0} {1}")
+ @Parameters(name = "#{index}: {0}")
public static Iterable<? extends Object> data() {
- return Arrays.asList(new Object[][]{
- {1, false}, {2, false}, {3, false}, {10, false},
- {1, true}, {2, true}, {3, true}, {10, true}
- });
+ return Arrays.asList(new Object[][]{ {1}, {2}, {3}, {10} });
}
private class TestHandler extends Handler {
@@ -289,20 +290,51 @@
}
/**
- * Create virtual displays, change their configurations and release them
- * mDisplays: the amount of virtual displays to be created
- * mCached: true to run the test activity in cached mode; false in non-cached mode
+ * Return true if the freezer is enabled on this platform.
*/
- @Test
- public void testDisplayEvents() {
- Log.d(TAG, "Start test testDisplayEvents " + mDisplayCount + " " + mCached);
- // Launch DisplayEventActivity and start listening to display events
- launchTestActivity();
+ private boolean isAppFreezerEnabled() {
+ try {
+ return mActivityManager.getService().isAppFreezerEnabled();
+ } catch (Exception e) {
+ Log.e(TAG, "isAppFreezerEnabled() failed: " + e);
+ return false;
+ }
+ }
- if (mCached) {
- // The test activity in cached mode won't receive the pending display events
+ private void waitForProcessFreeze(int pid, long timeoutMs) {
+ // TODO: Add a listener to monitor freezer state changes.
+ SystemUtil.runWithShellPermissionIdentity(() -> {
+ TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid,
+ (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs),
+ () -> mActivityManager.isProcessFrozen(pid));
+ });
+ }
+
+ private void waitForProcessUnfreeze(int pid, long timeoutMs) {
+ // TODO: Add a listener to monitor freezer state changes.
+ SystemUtil.runWithShellPermissionIdentity(() -> {
+ TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid,
+ (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs),
+ () -> !mActivityManager.isProcessFrozen(pid));
+ });
+ }
+
+ /**
+ * Create virtual displays, change their configurations and release them. The number of
+ * displays is set by the {@link #mDisplays} variable.
+ */
+ private void testDisplayEventsInternal(boolean cached, boolean frozen) {
+ Log.d(TAG, "Start test testDisplayEvents " + mDisplayCount + " " + cached + " " + frozen);
+ // Launch DisplayEventActivity and start listening to display events
+ int pid = launchTestActivity();
+
+ // The test activity in cached or frozen mode won't receive the pending display events.
+ if (cached) {
makeTestActivityCached();
}
+ if (frozen) {
+ makeTestActivityFrozen(pid);
+ }
// Create new virtual displays
for (int i = 0; i < mDisplayCount; i++) {
@@ -315,8 +347,8 @@
}
for (int i = 0; i < mDisplayCount; i++) {
- if (mCached) {
- // DISPLAY_ADDED should be deferred for cached process
+ if (cached || frozen) {
+ // DISPLAY_ADDED should be deferred for a cached or frozen process.
displayBundleAt(i).assertNoDisplayEvents();
} else {
// DISPLAY_ADDED should arrive immediately for non-cached process
@@ -331,8 +363,8 @@
}
for (int i = 0; i < mDisplayCount; i++) {
- if (mCached) {
- // DISPLAY_CHANGED should be deferred for cached process
+ if (cached || frozen) {
+ // DISPLAY_CHANGED should be deferred for cached or frozen process.
displayBundleAt(i).assertNoDisplayEvents();
} else {
// DISPLAY_CHANGED should arrive immediately for non-cached process
@@ -340,10 +372,16 @@
}
}
- if (mCached) {
- // The test activity becomes non-cached and should receive the pending display events
+ // Unfreeze the test activity, if it was frozen.
+ if (frozen) {
+ makeTestActivityUnfrozen(pid);
+ }
+
+ if (cached || frozen) {
+ // Always ensure the test activity is not cached.
bringTestActivityTop();
+ // The test activity becomes non-cached and should receive the pending display events
for (int i = 0; i < mDisplayCount; i++) {
// The pending DISPLAY_ADDED & DISPLAY_CHANGED should arrive now
displayBundleAt(i).waitDisplayEvent(DISPLAY_ADDED);
@@ -363,9 +401,48 @@
}
/**
- * Launch the test activity that would listen to display events
+ * Create virtual displays, change their configurations and release them.
*/
- private void launchTestActivity() {
+ @Test
+ public void testDisplayEvents() {
+ testDisplayEventsInternal(false, false);
+ }
+
+ /**
+ * Create virtual displays, change their configurations and release them. The display app is
+ * moved to cached and the test verifies that no events are delivered to the cached app.
+ */
+ @Test
+ public void testDisplayEventsCached() {
+ testDisplayEventsInternal(true, false);
+ }
+
+ /**
+ * Create virtual displays, change their configurations and release them. The display app is
+ * frozen and the test verifies that no events are delivered to the frozen app.
+ */
+ @RequiresFlagsEnabled(Flags.FLAG_DEFER_DISPLAY_EVENTS_WHEN_FROZEN)
+ @Test
+ public void testDisplayEventsFrozen() {
+ assumeTrue(isAppFreezerEnabled());
+ testDisplayEventsInternal(false, true);
+ }
+
+ /**
+ * Create virtual displays, change their configurations and release them. The display app is
+ * cached and frozen and the test verifies that no events are delivered to the app.
+ */
+ @RequiresFlagsEnabled(Flags.FLAG_DEFER_DISPLAY_EVENTS_WHEN_FROZEN)
+ @Test
+ public void testDisplayEventsCachedFrozen() {
+ assumeTrue(isAppFreezerEnabled());
+ testDisplayEventsInternal(true, true);
+ }
+
+ /**
+ * Launch the test activity that would listen to display events. Return its process ID.
+ */
+ private int launchTestActivity() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setClassName(TEST_PACKAGE, TEST_ACTIVITY);
intent.putExtra(TEST_MESSENGER, mMessenger);
@@ -377,6 +454,18 @@
},
android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
waitLatch(mLatchActivityLaunch);
+
+ try {
+ String cmd = "pidof " + TEST_PACKAGE;
+ String result = SystemUtil.runShellCommand(mInstrumentation, cmd);
+ return Integer.parseInt(result.trim());
+ } catch (IOException e) {
+ fail("failed to get pid of test package");
+ return 0;
+ } catch (NumberFormatException e) {
+ fail("failed to parse pid " + e);
+ return 0;
+ }
}
/**
@@ -415,6 +504,45 @@
waitLatch(mLatchActivityCached);
}
+ // Sleep, ignoring interrupts.
+ private void pause(int s) {
+ try { Thread.sleep(s * 1000); } catch (Exception e) { }
+ }
+
+ /**
+ * Freeze the test activity.
+ */
+ private void makeTestActivityFrozen(int pid) {
+ // The delay here is meant to allow pending binder transactions to drain. A process
+ // cannot be frozen if it has pending binder transactions, and attempting to freeze such a
+ // process more than a few times will result in the system killing the process.
+ pause(5);
+ try {
+ String cmd = "am freeze --sticky ";
+ SystemUtil.runShellCommand(mInstrumentation, cmd + TEST_PACKAGE);
+ } catch (IOException e) {
+ fail(e.toString());
+ }
+ // Wait for the freeze to complete in the kernel and for the frozen process
+ // notification to settle out.
+ waitForProcessFreeze(pid, 5 * 1000);
+ }
+
+ /**
+ * Freeze the test activity.
+ */
+ private void makeTestActivityUnfrozen(int pid) {
+ try {
+ String cmd = "am unfreeze --sticky ";
+ SystemUtil.runShellCommand(mInstrumentation, cmd + TEST_PACKAGE);
+ } catch (IOException e) {
+ fail(e.toString());
+ }
+ // Wait for the freeze to complete in the kernel and for the frozen process
+ // notification to settle out.
+ waitForProcessUnfreeze(pid, 5 * 1000);
+ }
+
/**
* Create a virtual display
*
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index 84c4f62..5709d88 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -1010,7 +1010,39 @@
@Test
@EnableFlags(Flags.FLAG_MODES_UI)
- public void testConfigXml_manualRule_upgradeWhenExisting() throws Exception {
+ public void testConfigXml_manualRuleWithoutCondition_upgradeWhenExisting() throws Exception {
+ // prior to modes_ui, it's possible to have a non-null manual rule that doesn't have much
+ // data on it because it's meant to indicate that the manual rule is on by merely existing.
+ ZenModeConfig config = new ZenModeConfig();
+ config.manualRule = new ZenModeConfig.ZenRule();
+ config.manualRule.enabled = true;
+ config.manualRule.pkg = "android";
+ config.manualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+ config.manualRule.conditionId = null;
+ config.manualRule.enabler = "test";
+
+ // write out entire config xml
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ writeConfigXml(config, XML_VERSION_MODES_API, /* forBackup= */ false, baos);
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ ZenModeConfig fromXml = readConfigXml(bais);
+
+
+ // The result should be valid and contain a manual rule; the rule should have a non-null
+ // ZenPolicy and a condition whose state is true. The conditionId should be default.
+ assertThat(fromXml.isValid()).isTrue();
+ assertThat(fromXml.manualRule).isNotNull();
+ assertThat(fromXml.manualRule.zenPolicy).isNotNull();
+ assertThat(fromXml.manualRule.condition).isNotNull();
+ assertThat(fromXml.manualRule.condition.state).isEqualTo(STATE_TRUE);
+ assertThat(fromXml.manualRule.conditionId).isEqualTo(Uri.EMPTY);
+ assertThat(fromXml.manualRule.enabler).isEqualTo("test");
+ assertThat(fromXml.isManualActive()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_MODES_UI)
+ public void testConfigXml_manualRuleWithCondition_upgradeWhenExisting() throws Exception {
// prior to modes_ui, it's possible to have a non-null manual rule that doesn't have much
// data on it because it's meant to indicate that the manual rule is on by merely existing.
ZenModeConfig config = new ZenModeConfig();
@@ -1029,6 +1061,7 @@
// The result should have a manual rule; it should have a non-null ZenPolicy and a condition
// whose state is true. The conditionId and enabler data should also be preserved.
+ assertThat(fromXml.isValid()).isTrue();
assertThat(fromXml.manualRule).isNotNull();
assertThat(fromXml.manualRule.zenPolicy).isNotNull();
assertThat(fromXml.manualRule.condition).isNotNull();
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 92205f39..65736cb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -186,7 +186,8 @@
void setTopActivityCameraActive(boolean enabled) {
doReturn(enabled).when(getTopDisplayRotationCompatPolicy())
- .isCameraActive(eq(mActivityStack.top()), /* mustBeFullscreen= */ eq(true));
+ .isCameraRunningAndWindowingModeEligible(eq(mActivityStack.top()),
+ /* mustBeFullscreen= */ eq(true));
}
void setTopActivityEligibleForOrientationOverride(boolean enabled) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
index dbcef10..a8ccf95 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -26,6 +26,7 @@
import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_USER;
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;
@@ -35,6 +36,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import static org.junit.Assert.assertEquals;
@@ -247,8 +249,8 @@
assertTrue(mActivity.info
.isChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT));
- assertFalse(mCameraCompatFreeformPolicy
- .shouldApplyFreeformTreatmentForCameraCompat(mActivity));
+ assertFalse(mCameraCompatFreeformPolicy.isCameraCompatForFreeformEnabledForActivity(
+ mActivity));
}
@Test
@@ -256,8 +258,8 @@
public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- assertTrue(mCameraCompatFreeformPolicy
- .shouldApplyFreeformTreatmentForCameraCompat(mActivity));
+ assertTrue(mCameraCompatFreeformPolicy.isCameraCompatForFreeformEnabledForActivity(
+ mActivity));
}
@Test
@@ -303,6 +305,49 @@
assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
}
+ @Test
+ public void testGetCameraCompatAspectRatio_activityNotInCameraCompat_returnsDefaultAspRatio() {
+ configureActivity(SCREEN_ORIENTATION_FULL_USER);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity);
+
+ assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO,
+ mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity),
+ /* delta= */ 0.001);
+ }
+
+ @Test
+ public void testGetCameraCompatAspectRatio_activityInCameraCompat_returnsConfigAspectRatio() {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ final float configAspectRatio = 1.5f;
+ mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity);
+
+ assertEquals(configAspectRatio,
+ mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity),
+ /* delta= */ 0.001);
+ }
+
+
+ @Test
+ public void testGetCameraCompatAspectRatio_inCameraCompatPerAppOverride_returnDefAspectRatio() {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ final float configAspectRatio = 1.5f;
+ mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio);
+ doReturn(true).when(mActivity.mAppCompatController.getAppCompatCameraOverrides())
+ .isOverrideMinAspectRatioForCameraEnabled();
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity);
+
+ assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO,
+ mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity),
+ /* delta= */ 0.001);
+ }
+
private void configureActivity(@ScreenOrientation int activityOrientation) {
configureActivity(activityOrientation, WINDOWING_MODE_FREEFORM);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
index 8cf593f..35c9e3f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -544,39 +544,35 @@
}
@Test
- public void testIsCameraActiveWhenCallbackInvokedNoMultiWindow_returnTrue() {
+ public void testShouldCameraCompatControlOrientationWhenInvokedNoMultiWindow_returnTrue() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(
- mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true));
+ assertTrue(mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(mActivity));
}
@Test
- public void testIsCameraActiveWhenCallbackNotInvokedNoMultiWindow_returnFalse() {
+ public void testShouldCameraCompatControlOrientationWhenNotInvokedNoMultiWindow_returnFalse() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- assertFalse(
- mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true));
+ assertFalse(mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(mActivity));
}
@Test
- public void testIsCameraActiveWhenCallbackNotInvokedMultiWindow_returnFalse() {
+ public void testShouldCameraCompatControlOrientationWhenNotInvokedMultiWindow_returnFalse() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
when(mActivity.inMultiWindowMode()).thenReturn(true);
- assertFalse(
- mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true));
+ assertFalse(mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(mActivity));
}
@Test
- public void testIsCameraActiveWhenCallbackInvokedMultiWindow_returnFalse() {
+ public void testShouldCameraCompatControlOrientationWhenInvokedMultiWindow_returnFalse() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
when(mActivity.inMultiWindowMode()).thenReturn(true);
mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(
- mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true));
+ assertFalse(mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(mActivity));
}
private void configureActivity(@ScreenOrientation int activityOrientation) {