Merge "Use adapter's sensistivity" into main
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 0298c1e6..251776e 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -2995,6 +2995,8 @@
pw.print(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS,
Flags.startUserBeforeScheduledAlarms());
pw.println();
+ pw.print(Flags.FLAG_ACQUIRE_WAKELOCK_BEFORE_SEND, Flags.acquireWakelockBeforeSend());
+ pw.println();
pw.decreaseIndent();
pw.println();
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 458c171..248f191 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1651,9 +1651,65 @@
/** @hide Similar to {@link OP_CONTROL_AUDIO}, but doesn't require capabilities. */
public static final int OP_CONTROL_AUDIO_PARTIAL = AppOpEnums.APP_OP_CONTROL_AUDIO_PARTIAL;
+ /**
+ * Access coarse eye tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_EYE_TRACKING_COARSE =
+ AppOpEnums.APP_OP_EYE_TRACKING_COARSE;
+
+ /**
+ * Access fine eye tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_EYE_TRACKING_FINE =
+ AppOpEnums.APP_OP_EYE_TRACKING_FINE;
+
+ /**
+ * Access face tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_FACE_TRACKING =
+ AppOpEnums.APP_OP_FACE_TRACKING;
+
+ /**
+ * Access hand tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_HAND_TRACKING =
+ AppOpEnums.APP_OP_HAND_TRACKING;
+
+ /**
+ * Access head tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_HEAD_TRACKING =
+ AppOpEnums.APP_OP_HEAD_TRACKING;
+
+ /**
+ * Access coarse scene tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_SCENE_UNDERSTANDING_COARSE =
+ AppOpEnums.APP_OP_SCENE_UNDERSTANDING_COARSE;
+
+ /**
+ * Access fine scene tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_SCENE_UNDERSTANDING_FINE =
+ AppOpEnums.APP_OP_SCENE_UNDERSTANDING_FINE;
+
/** @hide */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- public static final int _NUM_OP = 156;
+ public static final int _NUM_OP = 163;
/**
* All app ops represented as strings.
@@ -1813,6 +1869,13 @@
OPSTR_WRITE_SYSTEM_PREFERENCES,
OPSTR_CONTROL_AUDIO,
OPSTR_CONTROL_AUDIO_PARTIAL,
+ OPSTR_EYE_TRACKING_COARSE,
+ OPSTR_EYE_TRACKING_FINE,
+ OPSTR_FACE_TRACKING,
+ OPSTR_HAND_TRACKING,
+ OPSTR_HEAD_TRACKING,
+ OPSTR_SCENE_UNDERSTANDING_COARSE,
+ OPSTR_SCENE_UNDERSTANDING_FINE,
})
public @interface AppOpString {}
@@ -2579,6 +2642,36 @@
/** @hide Access to a audio playback and control APIs without capability requirements */
public static final String OPSTR_CONTROL_AUDIO_PARTIAL = "android:control_audio_partial";
+ /** @hide Access coarse eye tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_EYE_TRACKING_COARSE = "android:eye_tracking_coarse";
+
+ /** @hide Access fine eye tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_EYE_TRACKING_FINE = "android:eye_tracking_fine";
+
+ /** @hide Access face tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_FACE_TRACKING = "android:face_tracking";
+
+ /** @hide Access hand tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_HAND_TRACKING = "android:hand_tracking";
+
+ /** @hide Access head tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_HEAD_TRACKING = "android:head_tracking";
+
+ /** @hide Access coarse scene tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_SCENE_UNDERSTANDING_COARSE =
+ "android:scene_understanding_coarse";
+
+ /** @hide Access fine scene tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_SCENE_UNDERSTANDING_FINE =
+ "android:scene_understanding_fine";
+
/** {@link #sAppOpsToNote} not initialized yet for this op */
private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0;
/** Should not collect noting of this app-op in {@link #sAppOpsToNote} */
@@ -2657,6 +2750,14 @@
Flags.replaceBodySensorPermissionEnabled() ? OP_READ_HEART_RATE : OP_NONE,
Flags.replaceBodySensorPermissionEnabled() ? OP_READ_SKIN_TEMPERATURE : OP_NONE,
Flags.replaceBodySensorPermissionEnabled() ? OP_READ_OXYGEN_SATURATION : OP_NONE,
+ // Android XR
+ android.xr.Flags.xrManifestEntries() ? OP_EYE_TRACKING_COARSE : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_EYE_TRACKING_FINE : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_FACE_TRACKING : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_HAND_TRACKING : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_HEAD_TRACKING : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_SCENE_UNDERSTANDING_COARSE : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_SCENE_UNDERSTANDING_FINE : OP_NONE,
};
/**
@@ -3192,6 +3293,41 @@
"CONTROL_AUDIO").setDefaultMode(AppOpsManager.MODE_FOREGROUND).build(),
new AppOpInfo.Builder(OP_CONTROL_AUDIO_PARTIAL, OPSTR_CONTROL_AUDIO_PARTIAL,
"CONTROL_AUDIO_PARTIAL").setDefaultMode(AppOpsManager.MODE_FOREGROUND).build(),
+ new AppOpInfo.Builder(OP_EYE_TRACKING_COARSE, OPSTR_EYE_TRACKING_COARSE,
+ "EYE_TRACKING_COARSE")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.EYE_TRACKING_COARSE : null)
+ .build(),
+ new AppOpInfo.Builder(OP_EYE_TRACKING_FINE, OPSTR_EYE_TRACKING_FINE,
+ "EYE_TRACKING_FINE")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.EYE_TRACKING_FINE : null)
+ .build(),
+ new AppOpInfo.Builder(OP_FACE_TRACKING, OPSTR_FACE_TRACKING,
+ "FACE_TRACKING")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.FACE_TRACKING : null)
+ .build(),
+ new AppOpInfo.Builder(OP_HAND_TRACKING, OPSTR_HAND_TRACKING,
+ "HAND_TRACKING")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.HAND_TRACKING : null)
+ .build(),
+ new AppOpInfo.Builder(OP_HEAD_TRACKING, OPSTR_HEAD_TRACKING,
+ "HEAD_TRACKING")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.HEAD_TRACKING : null)
+ .build(),
+ new AppOpInfo.Builder(OP_SCENE_UNDERSTANDING_COARSE, OPSTR_SCENE_UNDERSTANDING_COARSE,
+ "SCENE_UNDERSTANDING_COARSE")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.SCENE_UNDERSTANDING_COARSE : null)
+ .build(),
+ new AppOpInfo.Builder(OP_SCENE_UNDERSTANDING_FINE, OPSTR_SCENE_UNDERSTANDING_FINE,
+ "SCENE_UNDERSTANDING_FINE")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.SCENE_UNDERSTANDING_FINE : null)
+ .build(),
};
// The number of longs needed to form a full bitmask of app ops
@@ -3301,6 +3437,15 @@
}
/**
+ * Returns whether the provided {@code op} is a valid op code or not.
+ *
+ * @hide
+ */
+ public static boolean isValidOp(int op) {
+ return op >= 0 && op < sAppOpInfos.length;
+ }
+
+ /**
* @hide
*/
public static int strDebugOpToOp(String op) {
diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java
index fa977c9..2daa52b 100644
--- a/core/java/android/app/AutomaticZenRule.java
+++ b/core/java/android/app/AutomaticZenRule.java
@@ -228,7 +228,7 @@
public AutomaticZenRule(Parcel source) {
enabled = source.readInt() == ENABLED;
if (source.readInt() == ENABLED) {
- name = getTrimmedString(source.readString());
+ name = getTrimmedString(source.readString8());
}
interruptionFilter = source.readInt();
conditionId = getTrimmedUri(source.readParcelable(null, android.net.Uri.class));
@@ -238,11 +238,11 @@
source.readParcelable(null, android.content.ComponentName.class));
creationTime = source.readLong();
mZenPolicy = source.readParcelable(null, ZenPolicy.class);
- mPkg = source.readString();
+ mPkg = source.readString8();
mDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
mAllowManualInvocation = source.readBoolean();
mIconResId = source.readInt();
- mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH);
+ mTriggerDescription = getTrimmedString(source.readString8(), MAX_DESC_LENGTH);
mType = source.readInt();
}
@@ -514,7 +514,7 @@
dest.writeInt(enabled ? ENABLED : DISABLED);
if (name != null) {
dest.writeInt(1);
- dest.writeString(name);
+ dest.writeString8(name);
} else {
dest.writeInt(0);
}
@@ -524,11 +524,11 @@
dest.writeParcelable(configurationActivity, 0);
dest.writeLong(creationTime);
dest.writeParcelable(mZenPolicy, 0);
- dest.writeString(mPkg);
+ dest.writeString8(mPkg);
dest.writeParcelable(mDeviceEffects, 0);
dest.writeBoolean(mAllowManualInvocation);
dest.writeInt(mIconResId);
- dest.writeString(mTriggerDescription);
+ dest.writeString8(mTriggerDescription);
dest.writeInt(mType);
}
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index eb9feb9..8af5b1b 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -189,6 +189,7 @@
* @param arguments Any additional arguments that were supplied when the
* instrumentation was started.
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public void onCreate(Bundle arguments) {
}
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 719e438..1b71e73 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -25,6 +25,9 @@
import static android.app.admin.DevicePolicyResources.UNDEFINED;
import static android.graphics.drawable.Icon.TYPE_URI;
import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP;
+import static android.util.TypedValue.COMPLEX_UNIT_PX;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static java.util.Objects.requireNonNull;
@@ -6001,6 +6004,8 @@
contentView.setViewVisibility(p.mTextViewId, View.GONE);
contentView.setTextViewText(p.mTextViewId, null);
}
+
+ updateExpanderAlignment(contentView, p, hasSecondLine);
setHeaderlessVerticalMargins(contentView, p, hasSecondLine);
// Update margins to leave space for the top line (but not for headerless views like
@@ -6010,12 +6015,29 @@
int margin = getContentMarginTop(mContext,
R.dimen.notification_2025_content_margin_top);
contentView.setViewLayoutMargin(R.id.notification_main_column,
- RemoteViews.MARGIN_TOP, margin, TypedValue.COMPLEX_UNIT_PX);
+ RemoteViews.MARGIN_TOP, margin, COMPLEX_UNIT_PX);
}
return contentView;
}
+ private static void updateExpanderAlignment(RemoteViews contentView,
+ StandardTemplateParams p, boolean hasSecondLine) {
+ if (notificationsRedesignTemplates() && p.mHeaderless) {
+ if (!hasSecondLine) {
+ // If there's no text, let's center the expand button vertically to align things
+ // more nicely. This is handled separately for notifications that use a
+ // NotificationHeaderView, see NotificationHeaderView#centerTopLine.
+ contentView.setViewLayoutHeight(R.id.expand_button, MATCH_PARENT,
+ COMPLEX_UNIT_PX);
+ } else {
+ // Otherwise, just use the default height for the button to keep it top-aligned.
+ contentView.setViewLayoutHeight(R.id.expand_button, WRAP_CONTENT,
+ COMPLEX_UNIT_PX);
+ }
+ }
+ }
+
private static void setHeaderlessVerticalMargins(RemoteViews contentView,
StandardTemplateParams p, boolean hasSecondLine) {
if (Flags.notificationsRedesignTemplates() || !p.mHeaderless) {
@@ -9560,7 +9582,7 @@
int marginStart = res.getDimensionPixelSize(
R.dimen.notification_2025_content_margin_start);
contentView.setViewLayoutMargin(R.id.title,
- RemoteViews.MARGIN_START, marginStart, TypedValue.COMPLEX_UNIT_PX);
+ RemoteViews.MARGIN_START, marginStart, COMPLEX_UNIT_PX);
}
if (isLegacyHeaderless) {
// Collapsed legacy messaging style has a 1-line limit.
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index 7a811a1..5b0cf115 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -132,7 +132,7 @@
per-file ConfigurationController.java = file:/services/core/java/com/android/server/wm/OWNERS
per-file *ScreenCapture* = file:/services/core/java/com/android/server/wm/OWNERS
per-file ComponentOptions.java = file:/services/core/java/com/android/server/wm/OWNERS
-
+per-file Presentation.java = file:/services/core/java/com/android/server/wm/OWNERS
# Multitasking
per-file multitasking.aconfig = file:/services/core/java/com/android/server/wm/OWNERS
diff --git a/core/java/android/app/Presentation.java b/core/java/android/app/Presentation.java
index bdab39d..f39e2dd 100644
--- a/core/java/android/app/Presentation.java
+++ b/core/java/android/app/Presentation.java
@@ -20,6 +20,8 @@
import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;
+import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
+
import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
@@ -34,6 +36,8 @@
import android.view.Display;
import android.view.Gravity;
import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams.WindowType;
@@ -277,6 +281,11 @@
@Override
public void show() {
super.show();
+
+ WindowInsetsController controller = getWindow().getInsetsController();
+ if (controller != null && enablePresentationForConnectedDisplays()) {
+ controller.hide(WindowInsets.Type.systemBars());
+ }
}
/**
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 67ade79..0085e4f 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -143,3 +143,10 @@
is_fixed_read_only: true
bug: "370928384"
}
+
+flag {
+ name: "device_aware_settings_override"
+ namespace: "virtual_devices"
+ description: "Settings override for virtual devices"
+ bug: "371801645"
+}
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index c4af871..bebca57 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -1499,7 +1499,7 @@
}
@VisibleForTesting
- static final class DisplayListenerDelegate {
+ public static final class DisplayListenerDelegate {
public final DisplayListener mListener;
public volatile long mInternalEventFlagsMask;
@@ -1536,7 +1536,7 @@
}
@VisibleForTesting
- boolean isEventFilterExplicit() {
+ public boolean isEventFilterExplicit() {
return mIsEventFilterExplicit;
}
@@ -1892,7 +1892,7 @@
}
@VisibleForTesting
- CopyOnWriteArrayList<DisplayListenerDelegate> getDisplayListeners() {
+ public CopyOnWriteArrayList<DisplayListenerDelegate> getDisplayListeners() {
return mDisplayListeners;
}
}
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 953ee08..5b5360e 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -485,6 +485,9 @@
/**
* Returns the list of ContextHubInfo objects describing the available Context Hubs.
*
+ * To find the list of hubs that include all Hubs (including both Context Hubs and Vendor Hubs),
+ * use the {@link #getHubs()} method instead.
+ *
* @return the list of ContextHubInfo objects
*
* @see ContextHubInfo
@@ -499,8 +502,8 @@
}
/**
- * Returns the list of HubInfo objects describing the available hubs (including ContextHub and
- * VendorHub). This method is primarily used for debugging purposes as most clients care about
+ * Returns the list of HubInfo objects describing the available hubs (including Context Hubs and
+ * Vendor Hubs). This method is primarily used for debugging purposes as most clients care about
* endpoints and services more than hubs.
*
* @return the list of HubInfo objects
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 4cbd5be..1cf43d4 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -2636,7 +2636,7 @@
enabled = source.readInt() == 1;
snoozing = source.readInt() == 1;
if (source.readInt() == 1) {
- name = source.readString();
+ name = source.readString8();
}
zenMode = source.readInt();
conditionId = source.readParcelable(null, android.net.Uri.class);
@@ -2644,18 +2644,18 @@
component = source.readParcelable(null, android.content.ComponentName.class);
configurationActivity = source.readParcelable(null, android.content.ComponentName.class);
if (source.readInt() == 1) {
- id = source.readString();
+ id = source.readString8();
}
creationTime = source.readLong();
if (source.readInt() == 1) {
- enabler = source.readString();
+ enabler = source.readString8();
}
zenPolicy = source.readParcelable(null, android.service.notification.ZenPolicy.class);
zenDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
- pkg = source.readString();
+ pkg = source.readString8();
allowManualInvocation = source.readBoolean();
- iconResName = source.readString();
- triggerDescription = source.readString();
+ iconResName = source.readString8();
+ triggerDescription = source.readString8();
type = source.readInt();
userModifiedFields = source.readInt();
zenPolicyUserModifiedFields = source.readInt();
@@ -2703,7 +2703,7 @@
dest.writeInt(snoozing ? 1 : 0);
if (name != null) {
dest.writeInt(1);
- dest.writeString(name);
+ dest.writeString8(name);
} else {
dest.writeInt(0);
}
@@ -2714,23 +2714,23 @@
dest.writeParcelable(configurationActivity, 0);
if (id != null) {
dest.writeInt(1);
- dest.writeString(id);
+ dest.writeString8(id);
} else {
dest.writeInt(0);
}
dest.writeLong(creationTime);
if (enabler != null) {
dest.writeInt(1);
- dest.writeString(enabler);
+ dest.writeString8(enabler);
} else {
dest.writeInt(0);
}
dest.writeParcelable(zenPolicy, 0);
dest.writeParcelable(zenDeviceEffects, 0);
- dest.writeString(pkg);
+ dest.writeString8(pkg);
dest.writeBoolean(allowManualInvocation);
- dest.writeString(iconResName);
- dest.writeString(triggerDescription);
+ dest.writeString8(iconResName);
+ dest.writeString8(triggerDescription);
dest.writeInt(type);
dest.writeInt(userModifiedFields);
dest.writeInt(zenPolicyUserModifiedFields);
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index f58baff..4fc894c 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -789,6 +789,12 @@
in @nullable ImeTracker.Token statsToken);
/**
+ * Updates the currently animating insets types of a remote process.
+ */
+ @EnforcePermission("MANAGE_APP_TOKENS")
+ void updateDisplayWindowAnimatingTypes(int displayId, int animatingTypes);
+
+ /**
* Called to get the expected window insets.
*
* @return {@code true} if system bars are always consumed.
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 1f8f0820..7d6d5a2 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -272,6 +272,15 @@
in @nullable ImeTracker.Token imeStatsToken);
/**
+ * Notifies WindowState what insets types are currently running within the Window.
+ * see {@link com.android.server.wm.WindowState#mInsetsAnimationRunning).
+ *
+ * @param window The window that is insets animaiton is running.
+ * @param animatingTypes Indicates the currently animating insets types.
+ */
+ oneway void updateAnimatingTypes(IWindow window, int animatingTypes);
+
+ /**
* Called when the system gesture exclusion has changed.
*/
oneway void reportSystemGestureExclusionChanged(IWindow window, in List<Rect> exclusionRects);
@@ -372,14 +381,4 @@
*/
oneway void notifyImeWindowVisibilityChangedFromClient(IWindow window, boolean visible,
in ImeTracker.Token statsToken);
-
- /**
- * Notifies WindowState whether inset animations are currently running within the Window.
- * This value is used by the server to vote for refresh rate.
- * see {@link com.android.server.wm.WindowState#mInsetsAnimationRunning).
- *
- * @param window The window that is insets animaiton is running.
- * @param running Indicates the insets animation state.
- */
- oneway void notifyInsetsAnimationRunningStateChanged(IWindow window, boolean running);
}
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 0d82acd..462c5c6 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -211,12 +211,12 @@
}
/**
- * Notifies when the state of running animation is changed. The state is either "running" or
- * "idle".
+ * Notifies when the insets types of running animation have changed. The animatingTypes
+ * contain all types, which have an ongoing animation.
*
- * @param running {@code true} if there is any animation running; {@code false} otherwise.
+ * @param animatingTypes the {@link InsetsType}s that are currently animating
*/
- default void notifyAnimationRunningStateChanged(boolean running) {}
+ default void updateAnimatingTypes(@InsetsType int animatingTypes) {}
/** @see ViewRootImpl#isHandlingPointerEvent */
default boolean isHandlingPointerEvent() {
@@ -665,6 +665,9 @@
/** Set of inset types which are requested visible which are reported to the host */
private @InsetsType int mReportedRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
+ /** Set of insets types which are currently animating */
+ private @InsetsType int mAnimatingTypes = 0;
+
/** Set of inset types that we have controls of */
private @InsetsType int mControllableTypes;
@@ -745,9 +748,10 @@
mFrame, mFromState, mToState, RESIZE_INTERPOLATOR,
ANIMATION_DURATION_RESIZE, mTypes, InsetsController.this);
if (mRunningAnimations.isEmpty()) {
- mHost.notifyAnimationRunningStateChanged(true);
+ mHost.updateAnimatingTypes(runner.getTypes());
}
mRunningAnimations.add(new RunningAnimation(runner, runner.getAnimationType()));
+ mAnimatingTypes |= runner.getTypes();
}
};
@@ -1564,9 +1568,8 @@
}
}
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_ANIMATION_RUNNING);
- if (mRunningAnimations.isEmpty()) {
- mHost.notifyAnimationRunningStateChanged(true);
- }
+ mAnimatingTypes |= runner.getTypes();
+ mHost.updateAnimatingTypes(mAnimatingTypes);
mRunningAnimations.add(new RunningAnimation(runner, animationType));
if (DEBUG) Log.d(TAG, "Animation added to runner. useInsetsAnimationThread: "
+ useInsetsAnimationThread);
@@ -1827,7 +1830,7 @@
dispatchAnimationEnd(runningAnimation.runner.getAnimation());
} else {
if (Flags.refactorInsetsController()) {
- if (removedTypes == ime()
+ if ((removedTypes & ime()) != 0
&& control.getAnimationType() == ANIMATION_TYPE_HIDE) {
if (mHost != null) {
// if the (hide) animation is cancelled, the
@@ -1842,9 +1845,11 @@
break;
}
}
- if (mRunningAnimations.isEmpty()) {
- mHost.notifyAnimationRunningStateChanged(false);
+ if (removedTypes > 0) {
+ mAnimatingTypes &= ~removedTypes;
+ mHost.updateAnimatingTypes(mAnimatingTypes);
}
+
onAnimationStateChanged(removedTypes, false /* running */);
}
@@ -1969,14 +1974,6 @@
return animatingTypes;
}
- private @InsetsType int computeAnimatingTypes() {
- int animatingTypes = 0;
- for (int i = 0; i < mRunningAnimations.size(); i++) {
- animatingTypes |= mRunningAnimations.get(i).runner.getTypes();
- }
- return animatingTypes;
- }
-
/**
* Called when finishing setting requested visible types or finishing setting controls.
*
@@ -1989,7 +1986,7 @@
// report its requested visibility at the end of the animation, otherwise we would
// lose the leash, and it would disappear during the animation
// TODO(b/326377046) revisit this part and see if we can make it more general
- typesToReport = mRequestedVisibleTypes | (computeAnimatingTypes() & ime());
+ typesToReport = mRequestedVisibleTypes | (mAnimatingTypes & ime());
} else {
typesToReport = mRequestedVisibleTypes;
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 9498407..7fd7be8 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2540,11 +2540,12 @@
}
/**
- * Notify the when the running state of a insets animation changed.
+ * Notify the when the animating insets types have changed.
*/
@VisibleForTesting
- public void notifyInsetsAnimationRunningStateChanged(boolean running) {
+ public void updateAnimatingTypes(@InsetsType int animatingTypes) {
if (sToolkitSetFrameRateReadOnlyFlagValue) {
+ boolean running = animatingTypes != 0;
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.instant(Trace.TRACE_TAG_VIEW,
TextUtils.formatSimple("notifyInsetsAnimationRunningStateChanged(%s)",
@@ -2552,7 +2553,7 @@
}
mInsetsAnimationRunning = running;
try {
- mWindowSession.notifyInsetsAnimationRunningStateChanged(mWindow, running);
+ mWindowSession.updateAnimatingTypes(mWindow, animatingTypes);
} catch (RemoteException e) {
}
}
diff --git a/core/java/android/view/ViewRootInsetsControllerHost.java b/core/java/android/view/ViewRootInsetsControllerHost.java
index 889acca4..8954df6 100644
--- a/core/java/android/view/ViewRootInsetsControllerHost.java
+++ b/core/java/android/view/ViewRootInsetsControllerHost.java
@@ -171,6 +171,13 @@
}
@Override
+ public void updateAnimatingTypes(@WindowInsets.Type.InsetsType int animatingTypes) {
+ if (mViewRoot != null) {
+ mViewRoot.updateAnimatingTypes(animatingTypes);
+ }
+ }
+
+ @Override
public boolean hasAnimationCallbacks() {
if (mViewRoot.mView == null) {
return false;
@@ -275,13 +282,6 @@
}
@Override
- public void notifyAnimationRunningStateChanged(boolean running) {
- if (mViewRoot != null) {
- mViewRoot.notifyInsetsAnimationRunningStateChanged(running);
- }
- }
-
- @Override
public boolean isHandlingPointerEvent() {
return mViewRoot != null && mViewRoot.isHandlingPointerEvent();
}
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 24647f4..196ae5e 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -625,6 +625,12 @@
int TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH = (1 << 14); // 0x4000
/**
+ * Transition flag: Indicates that aod is showing hidden by entering doze
+ * @hide
+ */
+ int TRANSIT_FLAG_AOD_APPEARING = (1 << 15); // 0x8000
+
+ /**
* @hide
*/
@IntDef(flag = true, prefix = { "TRANSIT_FLAG_" }, value = {
@@ -643,6 +649,7 @@
TRANSIT_FLAG_KEYGUARD_OCCLUDING,
TRANSIT_FLAG_KEYGUARD_UNOCCLUDING,
TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH,
+ TRANSIT_FLAG_AOD_APPEARING,
})
@Retention(RetentionPolicy.SOURCE)
@interface TransitionFlags {}
@@ -659,7 +666,8 @@
(TRANSIT_FLAG_KEYGUARD_GOING_AWAY
| TRANSIT_FLAG_KEYGUARD_APPEARING
| TRANSIT_FLAG_KEYGUARD_OCCLUDING
- | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING);
+ | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING
+ | TRANSIT_FLAG_AOD_APPEARING);
/**
* Remove content mode: Indicates remove content mode is currently not defined.
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index 72a595d..0a86ff8 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -597,6 +597,11 @@
}
@Override
+ public void updateAnimatingTypes(IWindow window, @InsetsType int animatingTypes) {
+ // NO-OP
+ }
+
+ @Override
public void reportSystemGestureExclusionChanged(android.view.IWindow window,
List<Rect> exclusionRects) {
}
@@ -679,11 +684,6 @@
@NonNull ImeTracker.Token statsToken) {
}
- @Override
- public void notifyInsetsAnimationRunningStateChanged(IWindow window, boolean running) {
- // NO-OP
- }
-
void setParentInterface(@Nullable ISurfaceControlViewHostParent parentInterface) {
IBinder oldInterface = mParentInterface == null ? null : mParentInterface.asBinder();
IBinder newInterface = parentInterface == null ? null : parentInterface.asBinder();
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 49a11ca..80a9cbc 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -235,6 +235,14 @@
}
flag {
+ name: "request_rectangle_with_source"
+ namespace: "accessibility"
+ description: "Request rectangle on screen with source parameter"
+ bug: "391877896"
+ is_exported: true
+}
+
+flag {
name: "restore_a11y_secure_settings_on_hsum_device"
namespace: "accessibility"
description: "Grab the a11y settings and send the settings restored broadcast for current visible foreground user"
diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java
index aa0111a..60178cd 100644
--- a/core/java/android/view/inputmethod/ImeTracker.java
+++ b/core/java/android/view/inputmethod/ImeTracker.java
@@ -225,6 +225,7 @@
PHASE_SERVER_UPDATE_CLIENT_VISIBILITY,
PHASE_WM_DISPLAY_IME_CONTROLLER_SET_IME_REQUESTED_VISIBLE,
PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES,
+ PHASE_WM_REQUESTED_VISIBLE_TYPES_NOT_CHANGED,
})
@Retention(RetentionPolicy.SOURCE)
@interface Phase {}
@@ -445,6 +446,9 @@
/** The control target reported its requestedVisibleTypes back to WindowManagerService. */
int PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES =
ImeProtoEnums.PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES;
+ /** The requestedVisibleTypes have not been changed, so this request is not continued. */
+ int PHASE_WM_REQUESTED_VISIBLE_TYPES_NOT_CHANGED =
+ ImeProtoEnums.PHASE_WM_REQUESTED_VISIBLE_TYPES_NOT_CHANGED;
/**
* Called when an IME request is started.
diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index 16f4114..c81c2bb 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -196,3 +196,10 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "report_animating_insets_types"
+ namespace: "input_method"
+ description: "Adding animating insets types and report IME visibility at the beginning of hiding"
+ bug: "393049691"
+}
diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java
index 118edc2..fa7b74f 100644
--- a/core/java/android/widget/RemoteViewsAdapter.java
+++ b/core/java/android/widget/RemoteViewsAdapter.java
@@ -242,7 +242,7 @@
@Override
public void onNullBinding(ComponentName name) {
- enqueueDeferredUnbindServiceMessage();
+ unbindNow();
}
@Override
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 99fe0cb..5e828ba 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -5211,7 +5211,11 @@
*/
@Nullable
public String getFontVariationSettings() {
- return mTextPaint.getFontVariationSettings();
+ if (Flags.typefaceRedesignReadonly()) {
+ return mTextPaint.getFontVariationOverride();
+ } else {
+ return mTextPaint.getFontVariationSettings();
+ }
}
/**
@@ -5567,10 +5571,10 @@
Math.clamp(400 + mFontWeightAdjustment,
FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX)));
}
- mTextPaint.setFontVariationSettings(
+ mTextPaint.setFontVariationOverride(
FontVariationAxis.toFontVariationSettings(axes));
} else {
- mTextPaint.setFontVariationSettings(fontVariationSettings);
+ mTextPaint.setFontVariationOverride(fontVariationSettings);
}
effective = true;
} else {
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 4aeedbb..42bf6d1 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -97,6 +97,8 @@
ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity,
true),
ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, false),
+ ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX(
+ Flags::enableDragToDesktopIncomingTransitionsBugfix, false),
ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true),
ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true),
ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true),
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 2e36e9a..684f320 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -811,4 +811,13 @@
metadata {
purpose: PURPOSE_BUGFIX
}
-}
\ No newline at end of file
+}
+flag {
+ name: "enable_drag_to_desktop_incoming_transitions_bugfix"
+ namespace: "lse_desktop_experience"
+ description: "Enables bugfix handling incoming transitions during the DragToDesktop transition."
+ bug: "397135730"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index 85794d4..056a0e8 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -181,6 +181,11 @@
*/
public interface BatteryHistoryStore {
/**
+ * Returns the maximum amount of storage that can be occupied by the battery history story.
+ */
+ int getMaxHistorySize();
+
+ /**
* Returns the table of contents, in the chronological order.
*/
List<BatteryHistoryFragment> getFragments();
@@ -516,6 +521,22 @@
}
/**
+ * Returns a high estimate of how many items are currently included in the battery history.
+ */
+ public int getEstimatedItemCount() {
+ int estimatedBytes = mHistoryBuffer.dataSize();
+ if (mStore != null) {
+ estimatedBytes += mStore.getMaxHistorySize() * 10; // account for the compression ratio
+ }
+ if (mHistoryParcels != null) {
+ for (int i = mHistoryParcels.size() - 1; i >= 0; i--) {
+ estimatedBytes += mHistoryParcels.get(i).dataSize();
+ }
+ }
+ return estimatedBytes / 4; // Minimum size of a history item is 4 bytes
+ }
+
+ /**
* Creates a read-only copy of the battery history. Does not copy the files stored
* in the system directory, so it is not safe while actively writing history.
*/
diff --git a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
index 0d5d876..38398b4 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
@@ -47,6 +47,8 @@
private boolean mClosed;
private long mBaseMonotonicTime;
private long mBaseTimeUtc;
+ private int mItemIndex = 0;
+ private int mMaxHistoryItems;
public BatteryStatsHistoryIterator(@NonNull BatteryStatsHistory history, long startTimeMs,
long endTimeMs) {
@@ -54,6 +56,7 @@
mStartTimeMs = startTimeMs;
mEndTimeMs = (endTimeMs != MonotonicClock.UNDEFINED) ? endTimeMs : Long.MAX_VALUE;
mHistoryItem.clear();
+ mMaxHistoryItems = history.getEstimatedItemCount();
}
@Override
@@ -80,6 +83,11 @@
private void advance() {
while (true) {
+ if (mItemIndex > mMaxHistoryItems) {
+ Slog.wtfStack(TAG, "Number of battery history items is too large: " + mItemIndex);
+ break;
+ }
+
Parcel p = mBatteryStatsHistory.getNextParcel(mStartTimeMs, mEndTimeMs);
if (p == null) {
break;
@@ -109,6 +117,7 @@
break;
}
if (mHistoryItem.time >= mStartTimeMs) {
+ mItemIndex++;
mNextItemReady = true;
return;
}
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java b/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java
index 69c0480..7ee22f3 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java
@@ -157,7 +157,7 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
- sForInternedString.parcel(this.name, dest, flags);
+ dest.writeString(this.name);
dest.writeInt(this.getIcon());
dest.writeInt(this.getLabelRes());
dest.writeCharSequence(this.getNonLocalizedLabel());
@@ -175,7 +175,7 @@
// We use the boot classloader for all classes that we load.
final ClassLoader boot = Object.class.getClassLoader();
//noinspection ConstantConditions
- this.name = sForInternedString.unparcel(in);
+ this.name = in.readString();
this.icon = in.readInt();
this.labelRes = in.readInt();
this.nonLocalizedLabel = in.readCharSequence();
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 7018ebc..5a180d7 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -82,7 +82,7 @@
* Notify system UI the immersive mode changed. This shall be removed when client immersive is
* enabled.
*/
- void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode);
+ void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode, int windowType);
void dismissKeyboardShortcutsMenu();
void toggleKeyboardShortcutsMenu(int deviceId);
diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java
index c0fe0d1..3472d68 100644
--- a/core/java/com/android/internal/widget/NotificationProgressBar.java
+++ b/core/java/com/android/internal/widget/NotificationProgressBar.java
@@ -555,6 +555,18 @@
mNotificationProgressDrawable.setParts(p.first);
mAdjustedProgressFraction =
(p.second - mTrackerDrawWidth / 2F) / (width - mTrackerDrawWidth);
+
+ mNotificationProgressDrawable.updateEndDotColor(getEndDotColor(fallbackSegments));
+ }
+
+ private int getEndDotColor(List<ProgressStyle.Segment> fallbackSegments) {
+ if (!mProgressModel.isStyledByProgress()) return Color.TRANSPARENT;
+ if (mProgressModel.getProgress() == mProgressModel.getProgressMax()) {
+ return Color.TRANSPARENT;
+ }
+
+ return fallbackSegments == null ? mProgressModel.getSegments().getLast().getColor()
+ : fallbackSegments.getLast().getColor();
}
private void updateTrackerAndBarPos(int w, int h) {
diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
index 30dcc67..b109610 100644
--- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java
+++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
@@ -21,6 +21,7 @@
import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
import android.graphics.Canvas;
+import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
@@ -74,6 +75,8 @@
mFillPaint.setStyle(Paint.Style.FILL);
}
+ private @ColorInt int mEndDotColor = Color.TRANSPARENT;
+
private int mAlpha;
public NotificationProgressDrawable() {
@@ -125,6 +128,16 @@
setParts(Arrays.asList(parts));
}
+ /**
+ * Update the color of the end dot. If TRANSPARENT, the dot is not drawn.
+ */
+ public void updateEndDotColor(@ColorInt int endDotColor) {
+ if (mEndDotColor != endDotColor) {
+ mEndDotColor = endDotColor;
+ invalidateSelf();
+ }
+ }
+
@Override
public void draw(@NonNull Canvas canvas) {
final float pointRadius = mState.mPointRadius;
@@ -164,6 +177,18 @@
canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint);
}
}
+
+ if (mEndDotColor != Color.TRANSPARENT) {
+ final float right = (float) getBounds().right;
+ final float dotRadius = mState.mFadedSegmentHeight / 2F;
+ mFillPaint.setColor(mEndDotColor);
+ // Use drawRoundRect instead of drawCircle to ensure alignment with the segment below.
+ mSegRectF.set(
+ Math.round(right - mState.mFadedSegmentHeight), Math.round(centerY - dotRadius),
+ Math.round(right), Math.round(centerY + dotRadius));
+ canvas.drawRoundRect(mSegRectF, mState.mSegmentCornerRadius,
+ mState.mSegmentCornerRadius, mFillPaint);
+ }
}
@Override
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 9acb242..a1961ae 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -268,6 +268,9 @@
72dp (content margin) - 12dp (action padding) - 4dp (button inset) -->
<dimen name="notification_2025_actions_margin_start">56dp</dimen>
+ <!-- Notification action button text size -->
+ <dimen name="notification_2025_action_text_size">16sp</dimen>
+
<!-- The margin on the end of most content views (ignores the expander) -->
<dimen name="notification_content_margin_end">16dp</dimen>
diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
index 8ef105f..de5f0ff 100644
--- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
+++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
@@ -177,8 +177,10 @@
@RequiresFlagsEnabled(Flags.FLAG_DELAY_IMPLICIT_RR_REGISTRATION_UNTIL_RR_ACCESSED)
public void test_refreshRateRegistration_implicitRRCallbacksEnabled()
throws RemoteException {
+ DisplayManager.DisplayListener displayListener1 =
+ Mockito.mock(DisplayManager.DisplayListener.class);
// Subscription without supplied events doesn't subscribe to RR events
- mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler,
+ mDisplayManagerGlobal.registerDisplayListener(displayListener1, mHandler,
ALL_DISPLAY_EVENTS, /* packageName= */ null,
/* isEventFilterExplicit */ false);
Mockito.verify(mDisplayManager)
@@ -187,7 +189,9 @@
// After registering to refresh rate changes, subscription without supplied events subscribe
// to RR events
mDisplayManagerGlobal.registerForRefreshRateChanges();
- mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler,
+ DisplayManager.DisplayListener displayListener2 =
+ Mockito.mock(DisplayManager.DisplayListener.class);
+ mDisplayManagerGlobal.registerDisplayListener(displayListener2, mHandler,
ALL_DISPLAY_EVENTS, /* packageName= */ null,
/* isEventFilterExplicit */ false);
Mockito.verify(mDisplayManager)
@@ -203,7 +207,9 @@
}
// Subscription to RR when events are supplied doesn't happen
- mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler,
+ DisplayManager.DisplayListener displayListener3 =
+ Mockito.mock(DisplayManager.DisplayListener.class);
+ mDisplayManagerGlobal.registerDisplayListener(displayListener3, mHandler,
ALL_DISPLAY_EVENTS, /* packageName= */ null,
/* isEventFilterExplicit */ true);
Mockito.verify(mDisplayManager)
@@ -214,7 +220,6 @@
int subscribedListenersCount = 0;
int nonSubscribedListenersCount = 0;
for (DisplayManagerGlobal.DisplayListenerDelegate delegate: delegates) {
-
if (delegate.isEventFilterExplicit()) {
assertEquals(ALL_DISPLAY_EVENTS, delegate.mInternalEventFlagsMask);
nonSubscribedListenersCount++;
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index 4516e9c..af87af0 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -1195,6 +1195,23 @@
});
}
+ @Test
+ public void testAnimatingTypes() throws Exception {
+ prepareControls();
+
+ final int types = navigationBars() | statusBars();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ clearInvocations(mTestHost);
+ mController.hide(types);
+ // quickly jump to final state by cancelling it.
+ mController.cancelExistingAnimations();
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ verify(mTestHost, times(1)).updateAnimatingTypes(eq(types));
+ verify(mTestHost, times(1)).updateAnimatingTypes(eq(0) /* animatingTypes */);
+ }
+
private void waitUntilNextFrame() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
Choreographer.getMainThreadInstance().postCallback(Choreographer.CALLBACK_COMMIT,
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index c40137f..f5d1e7a 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -1054,7 +1054,7 @@
ViewRootImpl viewRootImpl = mView.getViewRootImpl();
sInstrumentation.runOnMainSync(() -> {
mView.invalidate();
- viewRootImpl.notifyInsetsAnimationRunningStateChanged(true);
+ viewRootImpl.updateAnimatingTypes(Type.systemBars());
mView.invalidate();
});
sInstrumentation.waitForIdleSync();
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
index 3aefcd5..9087da3 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
@@ -552,7 +552,9 @@
private fun createAppBubble(usePendingIntent: Boolean = false): Bubble {
val target = Intent(context, TestActivity::class.java)
+ val component = ComponentName(context, TestActivity::class.java)
target.setPackage(context.packageName)
+ target.setComponent(component)
if (usePendingIntent) {
// Robolectric doesn't seem to play nice with PendingIntents, have to mock it.
val pendingIntent = mock<PendingIntent>()
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
index 7b583137..14c152102 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
@@ -19,7 +19,9 @@
import android.animation.AnimatorTestRule
import android.content.Context
import android.content.pm.LauncherApps
+import android.graphics.Insets
import android.graphics.PointF
+import android.graphics.Rect
import android.os.Handler
import android.os.UserManager
import android.view.IWindowManager
@@ -61,6 +63,7 @@
import com.android.wm.shell.shared.TransactionPool
import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+import com.android.wm.shell.shared.bubbles.DeviceConfig
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
@@ -80,6 +83,10 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
class BubbleBarLayerViewTest {
+ companion object {
+ const val SCREEN_WIDTH = 2000
+ const val SCREEN_HEIGHT = 1000
+ }
@get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this)
@@ -111,6 +118,16 @@
bubblePositioner = BubblePositioner(context, windowManager)
bubblePositioner.setShowingInBubbleBar(true)
+ val deviceConfig =
+ DeviceConfig(
+ windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT),
+ isLargeScreen = true,
+ isSmallTablet = false,
+ isLandscape = true,
+ isRtl = false,
+ insets = Insets.of(10, 20, 30, 40)
+ )
+ bubblePositioner.update(deviceConfig)
testBubblesList = mutableListOf()
val bubbleData = mock<BubbleData>()
@@ -313,6 +330,48 @@
assertThat(uiEventLoggerFake.logs[0]).hasBubbleInfo(bubble)
}
+ @Test
+ fun testUpdateExpandedView_updateLocation() {
+ bubblePositioner.bubbleBarLocation = BubbleBarLocation.RIGHT
+ val bubble = createBubble("first")
+
+ getInstrumentation().runOnMainSync {
+ bubbleBarLayerView.showExpandedView(bubble)
+ }
+ waitForExpandedViewAnimation()
+
+ val previousX = bubble.bubbleBarExpandedView!!.x
+
+ bubblePositioner.bubbleBarLocation = BubbleBarLocation.LEFT
+ getInstrumentation().runOnMainSync {
+ bubbleBarLayerView.updateExpandedView()
+ }
+
+ assertThat(bubble.bubbleBarExpandedView!!.x).isNotEqualTo(previousX)
+ }
+
+ @Test
+ fun testUpdatedExpandedView_updateLocation_skipWhileAnimating() {
+ bubblePositioner.bubbleBarLocation = BubbleBarLocation.RIGHT
+ val bubble = createBubble("first")
+
+ getInstrumentation().runOnMainSync {
+ bubbleBarLayerView.showExpandedView(bubble)
+ }
+ waitForExpandedViewAnimation()
+
+ val previousX = bubble.bubbleBarExpandedView!!.x
+ bubble.bubbleBarExpandedView!!.isAnimating = true
+
+ bubblePositioner.bubbleBarLocation = BubbleBarLocation.LEFT
+ getInstrumentation().runOnMainSync {
+ bubbleBarLayerView.updateExpandedView()
+ }
+
+ // Expanded view is not updated while animating
+ assertThat(bubble.bubbleBarExpandedView!!.x).isEqualTo(previousX)
+ }
+
private fun createBubble(key: String): Bubble {
val bubbleTaskView = FakeBubbleTaskViewFactory(context, mainExecutor).create()
val bubbleBarExpandedView =
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index d50a14c..c2aa146 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -79,7 +79,7 @@
android:layout_marginEnd="4dp">
<Button
- android:layout_width="94dp"
+ android:layout_width="108dp"
android:layout_height="60dp"
android:id="@+id/maximize_menu_size_toggle_button"
style="?android:attr/buttonBarButtonStyle"
@@ -126,7 +126,7 @@
<Button
android:id="@+id/maximize_menu_snap_left_button"
style="?android:attr/buttonBarButtonStyle"
- android:layout_width="41dp"
+ android:layout_width="48dp"
android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
android:layout_marginEnd="4dp"
android:background="@drawable/desktop_mode_maximize_menu_button_background"
@@ -137,7 +137,7 @@
<Button
android:id="@+id/maximize_menu_snap_right_button"
style="?android:attr/buttonBarButtonStyle"
- android:layout_width="41dp"
+ android:layout_width="48dp"
android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
android:background="@drawable/desktop_mode_maximize_menu_button_background"
android:importantForAccessibility="yes"
diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml
index d754243..8d18f95 100644
--- a/libs/WindowManager/Shell/res/values/colors.xml
+++ b/libs/WindowManager/Shell/res/values/colors.xml
@@ -68,4 +68,8 @@
<color name="desktop_mode_caption_button_on_hover_light">#11000000</color>
<color name="desktop_mode_caption_button_on_hover_dark">#11FFFFFF</color>
<color name="desktop_mode_caption_button">#00000000</color>
+ <color name="tiling_divider_background_light">#C9C7B6</color>
+ <color name="tiling_divider_background_dark">#4A4739</color>
+ <color name="tiling_handle_background_light">#000000</color>
+ <color name="tiling_handle_background_dark">#FFFFFF</color>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index a0c68ad..32660e8 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -618,6 +618,15 @@
<!-- The vertical inset to apply to the app chip's ripple drawable -->
<dimen name="desktop_mode_header_app_chip_ripple_inset_vertical">4dp</dimen>
+ <!-- The corner radius of the windowing actions pill buttons's ripple drawable -->
+ <dimen name="desktop_mode_handle_menu_windowing_action_ripple_radius">24dp</dimen>
+ <!-- The horizontal/vertical inset to apply to the ripple drawable effect of windowing
+ actions pill central buttons -->
+ <dimen name="desktop_mode_handle_menu_windowing_action_ripple_inset_base">2dp</dimen>
+ <!-- The horizontal/vertical vertical inset to apply to the ripple drawable effect of windowing
+ actions pill edge buttons -->
+ <dimen name="desktop_mode_handle_menu_windowing_action_ripple_inset_shift">4dp</dimen>
+
<!-- The corner radius of the minimize button's ripple drawable -->
<dimen name="desktop_mode_header_minimize_ripple_radius">18dp</dimen>
<!-- The vertical inset to apply to the minimize button's ripple drawable -->
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 00c446c..2e33253 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -22,6 +22,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.TaskInfo;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.os.SystemProperties;
@@ -287,6 +288,16 @@
}
/**
+ * @return If {@code true} we set opaque background for all freeform tasks to prevent freeform
+ * tasks below from being visible if freeform task window above is translucent.
+ * Otherwise if fluid resize is enabled, add a background to freeform tasks.
+ */
+ public static boolean shouldSetBackground(@NonNull TaskInfo taskInfo) {
+ return taskInfo.isFreeform() && (!DesktopModeStatus.isVeiledResizeEnabled()
+ || DesktopModeFlags.ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS.isTrue());
+ }
+
+ /**
* @return {@code true} if the app handle should be shown because desktop mode is enabled or
* the device has a large screen
*/
@@ -374,7 +385,7 @@
* of the display's root [TaskDisplayArea] is set to WINDOWING_MODE_FREEFORM.
*/
public static boolean enterDesktopByDefaultOnFreeformDisplay(@NonNull Context context) {
- if (!Flags.enterDesktopByDefaultOnFreeformDisplays()) {
+ if (!DesktopExperienceFlags.ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS.isTrue()) {
return false;
}
return SystemProperties.getBoolean(ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP,
@@ -387,7 +398,7 @@
* screen.
*/
public static boolean shouldMaximizeWhenDragToTopEdge(@NonNull Context context) {
- if (!Flags.enableDragToMaximize()) {
+ if (!DesktopExperienceFlags.ENABLE_DRAG_TO_MAXIMIZE.isTrue()) {
return false;
}
return SystemProperties.getBoolean(ENABLE_DRAG_TO_MAXIMIZE_SYS_PROP,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
index 5355138..26c3626 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -147,10 +147,9 @@
/** To be overridden by subclasses to adjust the animation surface change. */
void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
// Update the surface position and alpha.
- if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()
- && mAnimation.getExtensionEdges() != 0x0
+ if (mAnimation.getExtensionEdges() != 0x0
&& !(mChange.hasFlags(FLAG_TRANSLUCENT)
- && mChange.getActivityComponent() != null)) {
+ && mChange.getActivityComponent() != null)) {
// Extend non-translucent activities
t.setEdgeExtensionEffect(mLeash, mAnimation.getExtensionEdges());
}
@@ -189,8 +188,7 @@
@CallSuper
void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
onAnimationUpdate(t, mAnimation.getDuration());
- if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()
- && mAnimation.getExtensionEdges() != 0x0) {
+ if (mAnimation.getExtensionEdges() != 0x0) {
t.setEdgeExtensionEffect(mLeash, /* edge */ 0);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index c3e783d..85b7ac2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -20,11 +20,9 @@
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
-import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationSpec.createShowSnapshotForClosingAnimation;
import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
-import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE;
@@ -143,10 +141,6 @@
// ending states.
prepareForJumpCut(info, startTransaction);
} else {
- if (!com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) {
- addEdgeExtensionIfNeeded(startTransaction, finishTransaction,
- postStartTransactionCallbacks, adapters);
- }
addBackgroundColorIfNeeded(info, startTransaction, finishTransaction, adapters);
for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
duration = Math.max(duration, adapter.getDurationHint());
@@ -329,34 +323,6 @@
}
}
- /** Adds edge extension to the surfaces that have such an animation property. */
- private void addEdgeExtensionIfNeeded(@NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks,
- @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) {
- for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
- final Animation animation = adapter.mAnimation;
- if (animation.getExtensionEdges() == 0) {
- continue;
- }
- if (adapter.mChange.hasFlags(FLAG_TRANSLUCENT)
- && adapter.mChange.getActivityComponent() != null) {
- // Skip edge extension for translucent activity.
- continue;
- }
- final TransitionInfo.Change change = adapter.mChange;
- if (TransitionUtil.isOpeningType(adapter.mChange.getMode())) {
- // Need to screenshot after startTransaction is applied otherwise activity
- // may not be visible or ready yet.
- postStartTransactionCallbacks.add(
- t -> edgeExtendWindow(change, animation, t, finishTransaction));
- } else {
- // Can screenshot now (before startTransaction is applied)
- edgeExtendWindow(change, animation, startTransaction, finishTransaction);
- }
- }
- }
-
/** Adds background color to the transition if any animation has such a property. */
private void addBackgroundColorIfNeeded(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 313d151..d948928 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -364,7 +364,7 @@
@ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
return new Bubble(intent,
user,
- /* key= */ getAppBubbleKeyForApp(intent.getIntent().getPackage(), user),
+ /* key= */ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user),
mainExecutor, bgExecutor);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 29837dc..677c21c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -473,7 +473,7 @@
/** Updates the expanded view size and position. */
public void updateExpandedView() {
- if (mExpandedView == null || mExpandedBubble == null) return;
+ if (mExpandedView == null || mExpandedBubble == null || mExpandedView.isAnimating()) return;
boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY);
mPositioner.getBubbleBarExpandedViewBounds(mPositioner.isBubbleBarOnLeft(),
isOverflowExpanded, mTempRect);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index df82091..dd2050a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -461,6 +461,14 @@
}
}
+ private void setAnimating(boolean imeAnimationOngoing) {
+ int animatingTypes = imeAnimationOngoing ? WindowInsets.Type.ime() : 0;
+ try {
+ mWmService.updateDisplayWindowAnimatingTypes(mDisplayId, animatingTypes);
+ } catch (RemoteException e) {
+ }
+ }
+
private int imeTop(float surfaceOffset, float surfacePositionY) {
// surfaceOffset is already offset by the surface's top inset, so we need to subtract
// the top inset so that the return value is in screen coordinates.
@@ -619,6 +627,9 @@
+ imeTop(hiddenY, defaultY) + "->" + imeTop(shownY, defaultY)
+ " showing:" + (mAnimationDirection == DIRECTION_SHOW));
}
+ if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+ setAnimating(true);
+ }
int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY, defaultY),
imeTop(shownY, defaultY), mAnimationDirection == DIRECTION_SHOW,
isFloating, t);
@@ -666,6 +677,8 @@
}
if (!android.view.inputmethod.Flags.refactorInsetsController()) {
dispatchEndPositioning(mDisplayId, mCancelled, t);
+ } else if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+ setAnimating(false);
}
if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) {
ImeTracker.forLogging().onProgress(mStatsToken,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
index 6c04e2a..d304e20 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
@@ -80,7 +80,19 @@
private int mHoveringWidth;
private int mHoveringHeight;
private boolean mIsLeftRightSplit;
+ private boolean mIsSplitScreen;
+ /**
+ * Notifies the divider of ui mode change.
+ *
+ * @param isDarkMode Whether the mode is ui dark mode.
+ */
+ public void onUiModeChange(boolean isDarkMode) {
+ if (!mIsSplitScreen) {
+ mPaint.setColor(getTilingHandleColor(isDarkMode));
+ invalidate();
+ }
+ }
public DividerHandleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint.setColor(getResources().getColor(R.color.docked_divider_handle, null));
@@ -103,6 +115,27 @@
mHoveringHeight = mHeight > mWidth ? ((int) (mHeight * 1.5f)) : mHeight;
}
+ /**
+ * Used by tiling infrastructure to specify display light/dark mode and
+ * whether handle colors should be overridden on display mode change in case
+ * of non split screen.
+ * @param isSplitScreen Whether the divider is used by split screen or tiling.
+ * @param isDarkMode Whether the mode is ui dark mode.
+ */
+ public void setup(boolean isSplitScreen, boolean isDarkMode) {
+ mIsSplitScreen = isSplitScreen;
+ if (!mIsSplitScreen) {
+ mPaint.setColor(getTilingHandleColor(isDarkMode));
+ setAlpha(.9f);
+ }
+ }
+
+ private int getTilingHandleColor(Boolean isDarkMode) {
+ return isDarkMode ? getResources().getColor(
+ R.color.tiling_handle_background_dark, null /* theme */) : getResources().getColor(
+ R.color.tiling_handle_background_light, null /* theme */);
+ }
+
/** sets whether it's a left/right or top/bottom split */
public void setIsLeftRightSplit(boolean isLeftRightSplit) {
mIsLeftRightSplit = isLeftRightSplit;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java
index d5aaf75..cf0ecae 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java
@@ -47,15 +47,17 @@
private InvertedRoundedCornerDrawInfo mBottomLeftCorner;
private InvertedRoundedCornerDrawInfo mBottomRightCorner;
private boolean mIsLeftRightSplit;
+ private boolean mIsSplitScreen;
public DividerRoundedCorner(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mDividerWidth = getResources().getDimensionPixelSize(R.dimen.split_divider_bar_width);
mDividerBarBackground = new Paint();
mDividerBarBackground.setColor(
- getResources().getColor(R.color.split_divider_background, null));
+ getResources().getColor(R.color.split_divider_background, null /* theme */));
mDividerBarBackground.setFlags(Paint.ANTI_ALIAS_FLAG);
mDividerBarBackground.setStyle(Paint.Style.FILL);
+ mIsSplitScreen = false;
}
@Override
@@ -99,7 +101,41 @@
}
/**
+ * Used by tiling infrastructure to specify display light/dark mode and
+ * whether handle colors should be overridden on display mode change in case
+ * of non split screen.
+ *
+ * @param isSplitScreen Whether the divider is used by split screen or tiling.
+ * @param isDarkMode Whether the mode is ui dark mode.
+ */
+ public void setup(boolean isSplitScreen, boolean isDarkMode) {
+ mIsSplitScreen = isSplitScreen;
+ if (!isSplitScreen) {
+ mDividerBarBackground.setColor(getTilingHandleColor(isDarkMode));
+ }
+ }
+
+ /**
+ * Notifies the divider of ui mode change.
+ *
+ * @param isDarkMode Whether the mode is ui dark mode.
+ */
+ public void onUiModeChange(boolean isDarkMode) {
+ if (!mIsSplitScreen) {
+ mDividerBarBackground.setColor(getTilingHandleColor(isDarkMode));
+ invalidate();
+ }
+ }
+
+ private int getTilingHandleColor(boolean isDarkMode) {
+ return isDarkMode ? getResources().getColor(
+ R.color.tiling_divider_background_dark, null /* theme */) : getResources().getColor(
+ R.color.tiling_divider_background_light, null /* theme */);
+ }
+
+ /**
* Set whether the rounded corner is for a left/right split.
+ *
* @param isLeftRightSplit whether it's a left/right split or top/bottom split.
*/
public void setIsLeftRightSplit(boolean isLeftRightSplit) {
@@ -123,7 +159,16 @@
mCornerPosition = cornerPosition;
final RoundedCorner roundedCorner = getDisplay().getRoundedCorner(cornerPosition);
- mRadius = roundedCorner == null ? 0 : roundedCorner.getRadius();
+ if (mIsSplitScreen) {
+ mRadius = roundedCorner == null ? 0 : roundedCorner.getRadius();
+ } else {
+ mRadius = mContext
+ .getResources()
+ .getDimensionPixelSize(
+ com.android.wm.shell.shared.R.dimen
+ .desktop_windowing_freeform_rounded_corner_radius);
+ }
+
// Starts with a filled square, and then subtracting out a circle from the appropriate
// corner.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 59acdc5..48fadc0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -100,6 +100,7 @@
import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler;
+import com.android.wm.shell.desktopmode.DragToDisplayTransitionHandler;
import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.OverviewToDesktopTransitionObserver;
@@ -770,7 +771,8 @@
DesksOrganizer desksOrganizer,
DesksTransitionObserver desksTransitionObserver,
UserProfileContexts userProfileContexts,
- DesktopModeCompatPolicy desktopModeCompatPolicy) {
+ DesktopModeCompatPolicy desktopModeCompatPolicy,
+ DragToDisplayTransitionHandler dragToDisplayTransitionHandler) {
return new DesktopTasksController(
context,
shellInit,
@@ -808,7 +810,8 @@
desksOrganizer,
desksTransitionObserver,
userProfileContexts,
- desktopModeCompatPolicy);
+ desktopModeCompatPolicy,
+ dragToDisplayTransitionHandler);
}
@WMSingleton
@@ -934,6 +937,12 @@
@WMSingleton
@Provides
+ static DragToDisplayTransitionHandler provideDragToDisplayTransitionHandler() {
+ return new DragToDisplayTransitionHandler();
+ }
+
+ @WMSingleton
+ @Provides
static Optional<DesktopModeKeyGestureHandler> provideDesktopModeKeyGestureHandler(
Context context,
Optional<DesktopModeWindowDecorViewModel> desktopModeWindowDecorViewModel,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
index c9a63ff..e89aafe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
@@ -27,9 +27,9 @@
import android.view.Display.DEFAULT_DISPLAY
import android.view.IWindowManager
import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.DesktopExperienceFlags
import android.window.WindowContainerTransaction
import com.android.internal.protolog.ProtoLog
-import com.android.window.flags.Flags
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
@@ -47,31 +47,9 @@
) {
fun refreshDisplayWindowingMode() {
- if (!Flags.enableDisplayWindowingModeSwitching()) return
- // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available.
- val isExtendedDisplayEnabled =
- 0 !=
- Settings.Global.getInt(
- context.contentResolver,
- DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
- 0,
- )
- if (!isExtendedDisplayEnabled) {
- // No action needed in mirror or projected mode.
- return
- }
+ if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return
- val hasNonDefaultDisplay =
- rootTaskDisplayAreaOrganizer.getDisplayIds().any { displayId ->
- displayId != DEFAULT_DISPLAY
- }
- val targetDisplayWindowingMode =
- if (hasNonDefaultDisplay) {
- WINDOWING_MODE_FREEFORM
- } else {
- // Use the default display windowing mode when no non-default display.
- windowManager.getWindowingMode(DEFAULT_DISPLAY)
- }
+ val targetDisplayWindowingMode = getTargetWindowingModeForDefaultDisplay()
val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)
requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." }
val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
@@ -111,6 +89,25 @@
transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
}
+ private fun getTargetWindowingModeForDefaultDisplay(): Int {
+ if (isExtendedDisplayEnabled() && hasExternalDisplay()) {
+ return WINDOWING_MODE_FREEFORM
+ }
+ return windowManager.getWindowingMode(DEFAULT_DISPLAY)
+ }
+
+ // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available.
+ private fun isExtendedDisplayEnabled() =
+ 0 !=
+ Settings.Global.getInt(
+ context.contentResolver,
+ DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
+ 0,
+ )
+
+ private fun hasExternalDisplay() =
+ rootTaskDisplayAreaOrganizer.getDisplayIds().any { it != DEFAULT_DISPLAY }
+
private fun logV(msg: String, vararg arguments: Any?) {
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index 04e609e..03423ba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -1007,6 +1007,21 @@
fun saveBoundsBeforeFullImmersive(taskId: Int, bounds: Rect) =
boundsBeforeFullImmersiveByTaskId.set(taskId, Rect(bounds))
+ /** Returns the current state of the desktop, formatted for usage by remote clients. */
+ fun getDeskDisplayStateForRemote(): Array<DisplayDeskState> =
+ desktopData
+ .desksSequence()
+ .groupBy { it.displayId }
+ .map { (displayId, desks) ->
+ val activeDeskId = desktopData.getActiveDesk(displayId)?.deskId
+ DisplayDeskState().apply {
+ this.displayId = displayId
+ this.activeDeskId = activeDeskId ?: INVALID_DESK_ID
+ this.deskIds = desks.map { it.deskId }.toIntArray()
+ }
+ }
+ .toTypedArray()
+
/** TODO: b/389960283 - consider updating only the changing desks. */
private fun updatePersistentRepository(displayId: Int) {
val desks = desktopData.desksSequence(displayId).map { desk -> desk.deepCopy() }.toList()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 180d069..fca5084 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -205,6 +205,7 @@
private val desksTransitionObserver: DesksTransitionObserver,
private val userProfileContexts: UserProfileContexts,
private val desktopModeCompatPolicy: DesktopModeCompatPolicy,
+ private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler,
) :
RemoteCallable<DesktopTasksController>,
Transitions.TransitionHandler,
@@ -811,7 +812,7 @@
willExitDesktop(
triggerTaskId = taskInfo.taskId,
displayId = displayId,
- forceToFullscreen = false,
+ forceExitDesktop = false,
)
taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true)
val desktopExitRunnable =
@@ -884,7 +885,7 @@
snapEventHandler.removeTaskIfTiled(displayId, taskId)
taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true)
- val willExitDesktop = willExitDesktop(taskId, displayId, forceToFullscreen = false)
+ val willExitDesktop = willExitDesktop(taskId, displayId, forceExitDesktop = false)
val desktopExitRunnable =
performDesktopExitCleanUp(
wct = wct,
@@ -977,7 +978,7 @@
) {
logV("moveToFullscreenWithAnimation taskId=%d", task.taskId)
val wct = WindowContainerTransaction()
- val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceToFullscreen = true)
+ val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceExitDesktop = true)
val deactivationRunnable = addMoveToFullscreenChanges(wct, task, willExitDesktop)
// We are moving a freeform task to fullscreen, put the home task under the fullscreen task.
@@ -996,7 +997,14 @@
deactivationRunnable?.invoke(transition)
// handles case where we are moving to full screen without closing all DW tasks.
- if (!taskRepository.isOnlyVisibleNonClosingTask(task.taskId)) {
+ if (
+ !taskRepository.isOnlyVisibleNonClosingTask(task.taskId)
+ // This callback is already invoked by |addMoveToFullscreenChanges| when one of these
+ // flags is enabled.
+ &&
+ !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue &&
+ !Flags.enableDesktopWindowingPip()
+ ) {
desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted(
FULLSCREEN_ANIMATION_DURATION
)
@@ -1893,16 +1901,24 @@
private fun willExitDesktop(
triggerTaskId: Int,
displayId: Int,
- forceToFullscreen: Boolean,
+ forceExitDesktop: Boolean,
): Boolean {
+ if (
+ forceExitDesktop &&
+ (Flags.enableDesktopWindowingPip() ||
+ DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue)
+ ) {
+ // |forceExitDesktop| is true when the callers knows we'll exit desktop, such as when
+ // explicitly going fullscreen, so there's no point in checking the desktop state.
+ return true
+ }
if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId, displayId)) {
return false
}
} else if (
Flags.enableDesktopWindowingPip() &&
- taskRepository.isMinimizedPipPresentInDisplay(displayId) &&
- !forceToFullscreen
+ taskRepository.isMinimizedPipPresentInDisplay(displayId)
) {
return false
} else {
@@ -2295,7 +2311,7 @@
willExitDesktop(
triggerTaskId = task.taskId,
displayId = task.displayId,
- forceToFullscreen = true,
+ forceExitDesktop = true,
),
)
wct.reorder(task.token, true)
@@ -2328,7 +2344,7 @@
willExitDesktop(
triggerTaskId = task.taskId,
displayId = task.displayId,
- forceToFullscreen = true,
+ forceExitDesktop = true,
),
)
return wct
@@ -2433,7 +2449,7 @@
willExitDesktop(
triggerTaskId = task.taskId,
displayId = task.displayId,
- forceToFullscreen = true,
+ forceExitDesktop = true,
),
)
}
@@ -2471,7 +2487,7 @@
willExitDesktop(
triggerTaskId = task.taskId,
displayId = task.displayId,
- forceToFullscreen = true,
+ forceExitDesktop = true,
),
)
}
@@ -3173,25 +3189,24 @@
val wct = WindowContainerTransaction()
wct.setBounds(taskInfo.token, destinationBounds)
- // TODO: b/362720497 - reparent to a specific desk within the target display.
- // Reparent task if it has been moved to a new display.
- if (Flags.enableConnectedDisplaysWindowDrag()) {
- val newDisplayId = motionEvent.getDisplayId()
- if (newDisplayId != taskInfo.getDisplayId()) {
- val displayAreaInfo =
- rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(newDisplayId)
- if (displayAreaInfo == null) {
- logW(
- "Task reparent cannot find DisplayAreaInfo for displayId=%d",
- newDisplayId,
- )
- } else {
- wct.reparent(taskInfo.token, displayAreaInfo.token, /* onTop= */ true)
- }
+ val newDisplayId = motionEvent.getDisplayId()
+ val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(newDisplayId)
+ val isCrossDisplayDrag =
+ Flags.enableConnectedDisplaysWindowDrag() &&
+ newDisplayId != taskInfo.getDisplayId() &&
+ displayAreaInfo != null
+ val handler =
+ if (isCrossDisplayDrag) {
+ dragToDisplayTransitionHandler
+ } else {
+ null
}
+ if (isCrossDisplayDrag) {
+ // TODO: b/362720497 - reparent to a specific desk within the target display.
+ wct.reparent(taskInfo.token, displayAreaInfo.token, /* onTop= */ true)
}
- transitions.startTransition(TRANSIT_CHANGE, wct, null)
+ transitions.startTransition(TRANSIT_CHANGE, wct, handler)
releaseVisualIndicator()
}
@@ -3613,27 +3628,11 @@
controller,
{ c ->
run {
- c.taskRepository.addDeskChangeListener(
- deskChangeListener,
- c.mainExecutor,
- )
- c.taskRepository.addVisibleTasksListener(
- visibleTasksListener,
- c.mainExecutor,
- )
- c.taskbarDesktopTaskListener = taskbarDesktopTaskListener
- c.desktopModeEnterExitTransitionListener =
- desktopModeEntryExitTransitionListener
+ syncInitialState(c)
+ registerListeners(c)
}
},
- { c ->
- run {
- c.taskRepository.removeDeskChangeListener(deskChangeListener)
- c.taskRepository.removeVisibleTasksListener(visibleTasksListener)
- c.taskbarDesktopTaskListener = null
- c.desktopModeEnterExitTransitionListener = null
- }
- },
+ { c -> run { unregisterListeners(c) } },
)
}
@@ -3729,6 +3728,31 @@
c.startLaunchIntentTransition(intent, options, displayId)
}
}
+
+ private fun syncInitialState(c: DesktopTasksController) {
+ remoteListener.call { l ->
+ // TODO: b/393962589 - implement desks limit.
+ val canCreateDesks = true
+ l.onListenerConnected(
+ c.taskRepository.getDeskDisplayStateForRemote(),
+ canCreateDesks,
+ )
+ }
+ }
+
+ private fun registerListeners(c: DesktopTasksController) {
+ c.taskRepository.addDeskChangeListener(deskChangeListener, c.mainExecutor)
+ c.taskRepository.addVisibleTasksListener(visibleTasksListener, c.mainExecutor)
+ c.taskbarDesktopTaskListener = taskbarDesktopTaskListener
+ c.desktopModeEnterExitTransitionListener = desktopModeEntryExitTransitionListener
+ }
+
+ private fun unregisterListeners(c: DesktopTasksController) {
+ c.taskRepository.removeDeskChangeListener(deskChangeListener)
+ c.taskRepository.removeVisibleTasksListener(visibleTasksListener)
+ c.taskbarDesktopTaskListener = null
+ c.desktopModeEnterExitTransitionListener = null
+ }
}
private fun logV(msg: String, vararg arguments: Any?) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.kt
new file mode 100644
index 0000000..d51576a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.desktopmode
+
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import com.android.wm.shell.transition.Transitions
+
+/** Handles the transition to drag a window to another display by dragging the caption. */
+class DragToDisplayTransitionHandler : Transitions.TransitionHandler {
+ override fun handleRequest(
+ transition: IBinder,
+ request: TransitionRequestInfo,
+ ): WindowContainerTransaction? {
+ return null
+ }
+
+ override fun startAnimation(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction,
+ finishCallback: Transitions.TransitionFinishCallback,
+ ): Boolean {
+ for (change in info.changes) {
+ val sc = change.leash
+ val endBounds = change.endAbsBounds
+ val endPosition = change.endRelOffset
+ startTransaction
+ .setWindowCrop(sc, endBounds.width(), endBounds.height())
+ .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat())
+ finishTransaction
+ .setWindowCrop(sc, endBounds.width(), endBounds.height())
+ .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat())
+ }
+
+ startTransaction.apply()
+ finishCallback.onTransitionFinished(null)
+ return true
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index e9c6ade..3652a16 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -67,7 +67,6 @@
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN;
import static com.android.wm.shell.transition.DefaultSurfaceAnimator.buildSurfaceAnimation;
-import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionTypeFromInfo;
import static com.android.wm.shell.transition.TransitionAnimationHelper.isCoveredByOpaqueFullscreenChange;
@@ -543,21 +542,9 @@
backgroundColorForTransition);
if (!isTask && a.getExtensionEdges() != 0x0) {
- if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) {
- startTransaction.setEdgeExtensionEffect(
- change.getLeash(), a.getExtensionEdges());
- finishTransaction.setEdgeExtensionEffect(change.getLeash(), /* edge */ 0);
- } else {
- if (!TransitionUtil.isOpeningType(mode)) {
- // Can screenshot now (before startTransaction is applied)
- edgeExtendWindow(change, a, startTransaction, finishTransaction);
- } else {
- // Need to screenshot after startTransaction is applied otherwise
- // activity may not be visible or ready yet.
- postStartTransactionCallbacks
- .add(t -> edgeExtendWindow(change, a, t, finishTransaction));
- }
- }
+ startTransaction.setEdgeExtensionEffect(
+ change.getLeash(), a.getExtensionEdges());
+ finishTransaction.setEdgeExtensionEffect(change.getLeash(), /* edge */ 0);
}
final Rect clipRect = TransitionUtil.isClosingType(mode)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
index 7984bce..edfb560 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
@@ -26,7 +26,6 @@
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.TransitionInfo.FLAGS_IS_NON_APP_WINDOW;
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
-import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
@@ -39,20 +38,10 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.WindowConfiguration;
-import android.graphics.BitmapShader;
-import android.graphics.Canvas;
import android.graphics.Color;
-import android.graphics.Insets;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.graphics.Shader;
-import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.view.animation.Animation;
-import android.view.animation.Transformation;
-import android.window.ScreenCapture;
import android.window.TransitionInfo;
import com.android.internal.R;
@@ -317,129 +306,6 @@
}
/**
- * Adds edge extension surface to the given {@code change} for edge extension animation.
- */
- public static void edgeExtendWindow(@NonNull TransitionInfo.Change change,
- @NonNull Animation a, @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction) {
- // Do not create edge extension surface for transfer starting window change.
- // The app surface could be empty thus nothing can draw on the hardware renderer, which will
- // block this thread when calling Surface#unlockCanvasAndPost.
- if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
- return;
- }
- final Transformation transformationAtStart = new Transformation();
- a.getTransformationAt(0, transformationAtStart);
- final Transformation transformationAtEnd = new Transformation();
- a.getTransformationAt(1, transformationAtEnd);
-
- // We want to create an extension surface that is the maximal size and the animation will
- // take care of cropping any part that overflows.
- final Insets maxExtensionInsets = Insets.min(
- transformationAtStart.getInsets(), transformationAtEnd.getInsets());
-
- final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(),
- change.getEndAbsBounds().height());
- final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(),
- change.getEndAbsBounds().width());
- if (maxExtensionInsets.left < 0) {
- final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight);
- final Rect extensionRect = new Rect(0, 0,
- -maxExtensionInsets.left, targetSurfaceHeight);
- final int xPos = maxExtensionInsets.left;
- final int yPos = 0;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Left Edge Extension", startTransaction, finishTransaction);
- }
-
- if (maxExtensionInsets.top < 0) {
- final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1);
- final Rect extensionRect = new Rect(0, 0,
- targetSurfaceWidth, -maxExtensionInsets.top);
- final int xPos = 0;
- final int yPos = maxExtensionInsets.top;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Top Edge Extension", startTransaction, finishTransaction);
- }
-
- if (maxExtensionInsets.right < 0) {
- final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0,
- targetSurfaceWidth, targetSurfaceHeight);
- final Rect extensionRect = new Rect(0, 0,
- -maxExtensionInsets.right, targetSurfaceHeight);
- final int xPos = targetSurfaceWidth;
- final int yPos = 0;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Right Edge Extension", startTransaction, finishTransaction);
- }
-
- if (maxExtensionInsets.bottom < 0) {
- final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1,
- targetSurfaceWidth, targetSurfaceHeight);
- final Rect extensionRect = new Rect(0, 0,
- targetSurfaceWidth, -maxExtensionInsets.bottom);
- final int xPos = maxExtensionInsets.left;
- final int yPos = targetSurfaceHeight;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Bottom Edge Extension", startTransaction, finishTransaction);
- }
- }
-
- /**
- * Takes a screenshot of {@code surfaceToExtend}'s edge and extends it for edge extension
- * animation.
- */
- private static SurfaceControl createExtensionSurface(@NonNull SurfaceControl surfaceToExtend,
- @NonNull Rect edgeBounds, @NonNull Rect extensionRect, int xPos, int yPos,
- @NonNull String layerName, @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction) {
- final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder()
- .setName(layerName)
- .setParent(surfaceToExtend)
- .setHidden(true)
- .setCallsite("TransitionAnimationHelper#createExtensionSurface")
- .setOpaque(true)
- .setBufferSize(extensionRect.width(), extensionRect.height())
- .build();
-
- final ScreenCapture.LayerCaptureArgs captureArgs =
- new ScreenCapture.LayerCaptureArgs.Builder(surfaceToExtend)
- .setSourceCrop(edgeBounds)
- .setFrameScale(1)
- .setPixelFormat(PixelFormat.RGBA_8888)
- .setChildrenOnly(true)
- .setAllowProtected(false)
- .setCaptureSecureLayers(true)
- .build();
- final ScreenCapture.ScreenshotHardwareBuffer edgeBuffer =
- ScreenCapture.captureLayers(captureArgs);
-
- if (edgeBuffer == null) {
- ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
- "Failed to capture edge of window.");
- return null;
- }
-
- final BitmapShader shader = new BitmapShader(edgeBuffer.asBitmap(),
- Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
- final Paint paint = new Paint();
- paint.setShader(shader);
-
- final Surface surface = new Surface(edgeExtensionLayer);
- final Canvas c = surface.lockHardwareCanvas();
- c.drawRect(extensionRect, paint);
- surface.unlockCanvasAndPost(c);
- surface.release();
-
- startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE);
- startTransaction.setPosition(edgeExtensionLayer, xPos, yPos);
- startTransaction.setVisibility(edgeExtensionLayer, true);
- finishTransaction.remove(edgeExtensionLayer);
-
- return edgeExtensionLayer;
- }
-
- /**
* Returns whether there is an opaque fullscreen Change positioned in front of the given Change
* in the given TransitionInfo.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 49510c8..5e8c1fe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -61,6 +61,7 @@
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier;
import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
@@ -247,6 +248,10 @@
relayoutParams.mOccludingCaptionElements.add(controlsElement);
relayoutParams.mCaptionTopPadding = getTopPadding(relayoutParams,
taskInfo.getConfiguration().windowConfiguration.getBounds(), displayInsetsState);
+ // Set opaque background for all freeform tasks to prevent freeform tasks below
+ // from being visible if freeform task window above is translucent.
+ // Otherwise if fluid resize is enabled, add a background to freeform tasks.
+ relayoutParams.mShouldSetBackground = DesktopModeStatus.shouldSetBackground(taskInfo);
}
@SuppressLint("MissingPermission")
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 0d773ec..7ef1a93 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -558,6 +558,7 @@
} else {
decoration.relayout(taskInfo, taskInfo.isFocused, decoration.mExclusionRegion);
}
+ mDesktopTilingDecorViewModel.onTaskInfoChange(taskInfo);
mActivityOrientationChangeHandler.ifPresent(handler ->
handler.handleActivityOrientationChange(oldTaskInfo, taskInfo));
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 6165dbf..30e5c2a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -1064,6 +1064,10 @@
relayoutParams.mCornerRadius = shouldIgnoreCornerRadius ? INVALID_CORNER_RADIUS :
getCornerRadius(context, relayoutParams.mLayoutResId);
}
+ // Set opaque background for all freeform tasks to prevent freeform tasks below
+ // from being visible if freeform task window above is translucent.
+ // Otherwise if fluid resize is enabled, add a background to freeform tasks.
+ relayoutParams.mShouldSetBackground = DesktopModeStatus.shouldSetBackground(taskInfo);
}
private static int getCornerRadius(@NonNull Context context, int layoutResId) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index ff50672..ad2e23c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -50,6 +50,7 @@
import androidx.core.view.isGone
import com.android.window.flags.Flags
import com.android.wm.shell.R
+import com.android.wm.shell.bubbles.ContextUtils.isRtl
import com.android.wm.shell.shared.annotations.ShellBackgroundThread
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
@@ -60,6 +61,8 @@
import com.android.wm.shell.windowdecor.common.DecorThemeUtil
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader
import com.android.wm.shell.windowdecor.common.calculateMenuPosition
+import com.android.wm.shell.windowdecor.common.DrawableInsets
+import com.android.wm.shell.windowdecor.common.createRippleDrawable
import com.android.wm.shell.windowdecor.extension.isFullscreen
import com.android.wm.shell.windowdecor.extension.isMultiWindow
import com.android.wm.shell.windowdecor.extension.isPinned
@@ -71,6 +74,7 @@
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+
/**
* Handle menu opened when the appropriate button is clicked on.
*
@@ -467,6 +471,33 @@
val rootView = LayoutInflater.from(context)
.inflate(R.layout.desktop_mode_window_decor_handle_menu, null /* root */) as View
+ private val windowingButtonRippleRadius = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_handle_menu_windowing_action_ripple_radius)
+ private val windowingButtonDrawableInsets = DrawableInsets(
+ vertical = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base),
+ horizontal = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base)
+ )
+ private val windowingButtonDrawableInsetsLeft = DrawableInsets(
+ vertical = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base),
+ horizontalLeft = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_shift),
+ )
+ private val windowingButtonDrawableInsetsRight = DrawableInsets(
+ vertical = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base),
+ horizontalRight = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_shift)
+ )
+
// App Info Pill.
private val appInfoPill = rootView.requireViewById<View>(R.id.app_info_pill)
private val collapseMenuButton = appInfoPill.requireViewById<HandleMenuImageButton>(
@@ -708,6 +739,49 @@
desktopBtn.isSelected = taskInfo.isFreeform
desktopBtn.isEnabled = !taskInfo.isFreeform
desktopBtn.imageTintList = style.windowingButtonColor
+
+ val startInsets = if (context.isRtl) {
+ windowingButtonDrawableInsetsRight
+ } else {
+ windowingButtonDrawableInsetsLeft
+ }
+ val endInsets = if (context.isRtl) {
+ windowingButtonDrawableInsetsLeft
+ } else {
+ windowingButtonDrawableInsetsRight
+ }
+
+ fullscreenBtn.apply {
+ background = createRippleDrawable(
+ color = style.textColor,
+ cornerRadius = windowingButtonRippleRadius,
+ drawableInsets = startInsets
+ )
+ }
+
+ splitscreenBtn.apply {
+ background = createRippleDrawable(
+ color = style.textColor,
+ cornerRadius = windowingButtonRippleRadius,
+ drawableInsets = windowingButtonDrawableInsets
+ )
+ }
+
+ floatingBtn.apply {
+ background = createRippleDrawable(
+ color = style.textColor,
+ cornerRadius = windowingButtonRippleRadius,
+ drawableInsets = windowingButtonDrawableInsets
+ )
+ }
+
+ desktopBtn.apply {
+ background = createRippleDrawable(
+ color = style.textColor,
+ cornerRadius = windowingButtonRippleRadius,
+ drawableInsets = endInsets
+ )
+ }
}
private fun bindMoreActionsPill(style: MenuStyle) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt
index c6cb62d..1b0e0f70 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt
@@ -363,10 +363,11 @@
dragEventListeners.remove(dragEventListener)
}
- override fun onTopologyChanged(topology: DisplayTopology) {
+ override fun onTopologyChanged(topology: DisplayTopology?) {
// TODO: b/383069173 - Cancel window drag when topology changes happen during drag.
displayIds.clear()
+ if (topology == null) return
val displayBounds = topology.getAbsoluteBounds()
displayIds.addAll(List(displayBounds.size()) { displayBounds.keyAt(it) })
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
index 7af6b8e..5bd4228 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
@@ -225,7 +225,7 @@
val veilAnimT = surfaceControlTransactionSupplier.get()
val iconAnimT = surfaceControlTransactionSupplier.get()
veilAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
- duration = RESIZE_ALPHA_DURATION
+ duration = VEIL_ENTRY_ALPHA_ANIMATION_DURATION
addUpdateListener {
veilAnimT.setAlpha(background, animatedValue as Float)
.apply()
@@ -243,7 +243,8 @@
})
}
iconAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
- duration = RESIZE_ALPHA_DURATION
+ duration = ICON_ALPHA_ANIMATION_DURATION
+ startDelay = ICON_ENTRY_DELAY
addUpdateListener {
iconAnimT.setAlpha(icon, animatedValue as Float)
.apply()
@@ -387,23 +388,38 @@
if (background == null || icon == null) return
veilAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
- duration = RESIZE_ALPHA_DURATION
+ duration = VEIL_EXIT_ALPHA_ANIMATION_DURATION
+ startDelay = VEIL_EXIT_DELAY
addUpdateListener {
surfaceControlTransactionSupplier.get()
.setAlpha(background, animatedValue as Float)
- .setAlpha(icon, animatedValue as Float)
.apply()
}
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
surfaceControlTransactionSupplier.get()
.hide(background)
- .hide(icon)
.apply()
}
})
}
+ iconAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = ICON_ALPHA_ANIMATION_DURATION
+ addUpdateListener {
+ surfaceControlTransactionSupplier.get()
+ .setAlpha(icon, animatedValue as Float)
+ .apply()
+ }
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ surfaceControlTransactionSupplier.get()
+ .hide(icon)
+ .apply()
+ }
+ })
+ }
veilAnimator?.start()
+ iconAnimator?.start()
isVisible = false
}
@@ -451,7 +467,11 @@
companion object {
private const val TAG = "ResizeVeil"
- private const val RESIZE_ALPHA_DURATION = 100L
+ private const val ICON_ALPHA_ANIMATION_DURATION = 50L
+ private const val VEIL_ENTRY_ALPHA_ANIMATION_DURATION = 50L
+ private const val VEIL_EXIT_ALPHA_ANIMATION_DURATION = 200L
+ private const val ICON_ENTRY_DELAY = 33L
+ private const val VEIL_EXIT_DELAY = 33L
private const val VEIL_CONTAINER_LAYER = TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL
/** The background is a child of the veil container layer and goes at the bottom. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 4002dc5..7baef2b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -16,7 +16,6 @@
package com.android.wm.shell.windowdecor;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.content.res.Configuration.DENSITY_DPI_UNDEFINED;
import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsets.Type.mandatorySystemGestures;
@@ -57,7 +56,6 @@
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
-import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams.OccludingCaptionElement;
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost;
@@ -504,15 +502,14 @@
startT.show(mTaskSurface);
}
- if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
- && !DesktopModeStatus.isVeiledResizeEnabled()) {
- // When fluid resize is enabled, add a background to freeform tasks
- int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor();
+ if (params.mShouldSetBackground) {
+ final int backgroundColorInt = mTaskInfo.taskDescription != null
+ ? mTaskInfo.taskDescription.getBackgroundColor() : Color.BLACK;
mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f;
mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f;
mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f;
startT.setColor(mTaskSurface, mTmpColor);
- } else if (!DesktopModeStatus.isVeiledResizeEnabled()) {
+ } else {
startT.unsetColor(mTaskSurface);
}
@@ -833,6 +830,7 @@
boolean mSetTaskVisibilityPositionAndCrop;
boolean mHasGlobalFocus;
boolean mShouldSetAppBounds;
+ boolean mShouldSetBackground;
void reset() {
mLayoutResId = Resources.ID_NULL;
@@ -857,6 +855,7 @@
mAsyncViewHost = false;
mHasGlobalFocus = false;
mShouldSetAppBounds = false;
+ mShouldSetBackground = false;
}
boolean hasInputFeatureSpy() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
new file mode 100644
index 0000000..e18239d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.common
+
+import android.annotation.ColorInt
+import android.graphics.Color
+import android.graphics.drawable.LayerDrawable
+import android.graphics.drawable.RippleDrawable
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.RoundRectShape
+import com.android.wm.shell.windowdecor.common.OPACITY_11
+import com.android.wm.shell.windowdecor.common.OPACITY_15
+import android.content.res.ColorStateList
+
+/**
+ * Represents drawable insets, specifying the number of pixels to inset a drawable from its bounds.
+ */
+data class DrawableInsets(val l: Int, val t: Int, val r: Int, val b: Int) {
+ constructor(vertical: Int = 0, horizontal: Int = 0) :
+ this(horizontal, vertical, horizontal, vertical)
+ constructor(vertical: Int = 0, horizontalLeft: Int = 0, horizontalRight: Int = 0) :
+ this(horizontalLeft, vertical, horizontalRight, vertical)
+}
+
+/**
+ * Replaces the alpha component of a color with the given alpha value.
+ */
+@ColorInt
+fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
+ return Color.argb(
+ alpha,
+ Color.red(color),
+ Color.green(color),
+ Color.blue(color)
+ )
+}
+
+/**
+ * Creates a RippleDrawable with specified color, corner radius, and insets.
+ */
+fun createRippleDrawable(
+ @ColorInt color: Int,
+ cornerRadius: Int,
+ drawableInsets: DrawableInsets,
+): RippleDrawable {
+ return RippleDrawable(
+ ColorStateList(
+ arrayOf(
+ intArrayOf(android.R.attr.state_hovered),
+ intArrayOf(android.R.attr.state_pressed),
+ intArrayOf(),
+ ),
+ intArrayOf(
+ replaceColorAlpha(color, OPACITY_11),
+ replaceColorAlpha(color, OPACITY_15),
+ Color.TRANSPARENT,
+ )
+ ),
+ null /* content */,
+ LayerDrawable(arrayOf(
+ ShapeDrawable().apply {
+ shape = RoundRectShape(
+ FloatArray(8) { cornerRadius.toFloat() },
+ null /* inset */,
+ null /* innerRadii */
+ )
+ paint.color = Color.WHITE
+ }
+ )).apply {
+ require(numberOfLayers == 1) { "Must only contain one layer" }
+ setLayerInset(0 /* index */,
+ drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b)
+ }
+ )
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt
index ee5d0e8..e9426d2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt
@@ -137,6 +137,10 @@
}
}
+ fun onTaskInfoChange(taskInfo: RunningTaskInfo) {
+ tilingTransitionHandlerByDisplayId.get(taskInfo.displayId)?.onTaskInfoChange(taskInfo)
+ }
+
override fun onDisplayChange(
displayId: Int,
fromRotation: Int,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt
index fbbf1a5..cb45c17 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt
@@ -57,6 +57,7 @@
private val transactionSupplier: Supplier<SurfaceControl.Transaction>,
private var dividerBounds: Rect,
private val displayContext: Context,
+ private val isDarkMode: Boolean,
) : WindowlessWindowManager(config, leash, null), DividerMoveCallback, View.OnLayoutChangeListener {
private lateinit var viewHost: SurfaceControlViewHost
private var tilingDividerView: TilingDividerView? = null
@@ -153,7 +154,7 @@
surfaceControlViewHost.setView(dividerView, lp)
val tmpDividerBounds = Rect()
getDividerBounds(tmpDividerBounds)
- dividerView.setup(this, tmpDividerBounds, handleRegionSize)
+ dividerView.setup(this, tmpDividerBounds, handleRegionSize, isDarkMode)
t.setRelativeLayer(leash, relativeLeash, 1)
.setPosition(
leash,
@@ -172,6 +173,11 @@
updateTouchRegion()
}
+ /** Changes divider colour if dark/light mode is toggled. */
+ fun onUiModeChange(isDarkMode: Boolean) {
+ tilingDividerView?.onUiModeChange(isDarkMode)
+ }
+
/** Hides the divider bar. */
fun hideDividerBar() {
if (!dividerShown) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
index 9833325..a45df04 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
@@ -103,6 +103,7 @@
@VisibleForTesting
var desktopTilingDividerWindowManager: DesktopTilingDividerWindowManager? = null
private lateinit var dividerBounds: Rect
+ private var isDarkMode = false
private var isResizing = false
private var isTilingFocused = false
@@ -129,6 +130,7 @@
val isTiled = destinationBounds != taskInfo.configuration.windowConfiguration.bounds
initTilingApps(resizeMetadata, position, taskInfo)
+ isDarkMode = isTaskInDarkMode(taskInfo)
// Observe drag resizing to break tiling if a task is drag resized.
desktopModeWindowDecoration.addDragResizeListener(this)
@@ -232,6 +234,7 @@
transactionSupplier,
dividerBounds,
displayContext,
+ isDarkMode,
)
}
// a leash to present the divider on top of, without re-parenting.
@@ -356,6 +359,17 @@
transitions.startTransition(TRANSIT_CHANGE, wct, this)
}
+ fun onTaskInfoChange(taskInfo: RunningTaskInfo) {
+ val isCurrentTaskInDarkMode = isTaskInDarkMode(taskInfo)
+ if (isCurrentTaskInDarkMode == isDarkMode || !isTilingManagerInitialised) return
+ isDarkMode = isCurrentTaskInDarkMode
+ desktopTilingDividerWindowManager?.onUiModeChange(isDarkMode)
+ }
+
+ fun isTaskInDarkMode(taskInfo: RunningTaskInfo): Boolean =
+ (taskInfo.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
+ Configuration.UI_MODE_NIGHT_YES
+
override fun startAnimation(
transition: IBinder,
info: TransitionInfo,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt
index b8e3b0f..54dcd2d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt
@@ -16,6 +16,7 @@
package com.android.wm.shell.windowdecor.tiling
import android.content.Context
+import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
@@ -85,11 +86,14 @@
dividerMoveCallback: DividerMoveCallback,
dividerBounds: Rect,
handleRegionSize: Size,
+ isDarkMode: Boolean,
) {
callback = dividerMoveCallback
this.dividerBounds.set(dividerBounds)
handle.setIsLeftRightSplit(true)
+ handle.setup(/* isSplitScreen= */ false, isDarkMode)
corners.setIsLeftRightSplit(true)
+ corners.setup(/* isSplitScreen= */ false, isDarkMode)
handleRegionHeight = handleRegionSize.height
handleRegionWidth = handleRegionSize.width
cornersRadius =
@@ -103,6 +107,18 @@
)
}
+ fun onUiModeChange(isDarkMode: Boolean) {
+ handle.onUiModeChange(isDarkMode)
+ corners.onUiModeChange(isDarkMode)
+ paint.color =
+ if (isDarkMode) {
+ resources.getColor(R.color.tiling_divider_background_dark, null /* theme */)
+ } else {
+ resources.getColor(R.color.tiling_divider_background_light, null /* theme */)
+ }
+ invalidate()
+ }
+
override fun onFinishInflate() {
super.onFinishInflate()
dividerBar = requireViewById(R.id.divider_bar)
@@ -112,7 +128,15 @@
resources.getDimensionPixelSize(R.dimen.docked_stack_divider_lift_elevation)
setOnTouchListener(this)
setWillNotDraw(false)
- paint.color = resources.getColor(R.color.split_divider_background, null)
+ paint.color =
+ if (
+ context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
+ Configuration.UI_MODE_NIGHT_YES
+ ) {
+ resources.getColor(R.color.tiling_divider_background_dark, /* theme= */null)
+ } else {
+ resources.getColor(R.color.tiling_divider_background_light, /* theme= */ null)
+ }
paint.isAntiAlias = true
paint.style = Paint.Style.FILL
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 870c894..eb8b617 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -61,6 +61,8 @@
import com.android.wm.shell.windowdecor.common.OPACITY_55
import com.android.wm.shell.windowdecor.common.OPACITY_65
import com.android.wm.shell.windowdecor.common.Theme
+import com.android.wm.shell.windowdecor.common.DrawableInsets
+import com.android.wm.shell.windowdecor.common.createRippleDrawable
import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance
import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance
@@ -635,61 +637,10 @@
)
}
- @ColorInt
- private fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
- return Color.argb(
- alpha,
- Color.red(color),
- Color.green(color),
- Color.blue(color)
- )
- }
-
- private fun createRippleDrawable(
- @ColorInt color: Int,
- cornerRadius: Int,
- drawableInsets: DrawableInsets,
- ): RippleDrawable {
- return RippleDrawable(
- ColorStateList(
- arrayOf(
- intArrayOf(android.R.attr.state_hovered),
- intArrayOf(android.R.attr.state_pressed),
- intArrayOf(),
- ),
- intArrayOf(
- replaceColorAlpha(color, OPACITY_11),
- replaceColorAlpha(color, OPACITY_15),
- Color.TRANSPARENT
- )
- ),
- null /* content */,
- LayerDrawable(arrayOf(
- ShapeDrawable().apply {
- shape = RoundRectShape(
- FloatArray(8) { cornerRadius.toFloat() },
- null /* inset */,
- null /* innerRadii */
- )
- paint.color = Color.WHITE
- }
- )).apply {
- require(numberOfLayers == 1) { "Must only contain one layer" }
- setLayerInset(0 /* index */,
- drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b)
- }
- )
- }
-
private enum class SizeToggleDirection {
MAXIMIZE, RESTORE
}
- private data class DrawableInsets(val l: Int, val t: Int, val r: Int, val b: Int) {
- constructor(vertical: Int = 0, horizontal: Int = 0) :
- this(horizontal, vertical, horizontal, vertical)
- }
-
private data class Header(
val type: Type,
val appTheme: Theme,
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
index 2800839..d82c066 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
@@ -18,9 +18,9 @@
import android.tools.NavBar
import android.tools.Rotation
-import com.android.internal.R
import com.android.window.flags.Flags
import com.android.wm.shell.Utils
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import org.junit.After
import org.junit.Assume
import org.junit.Before
@@ -42,8 +42,8 @@
fun setup() {
Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
// Skip the test when the drag-to-maximize is enabled on this device.
- Assume.assumeFalse(Flags.enableDragToMaximize() &&
- instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode))
+ Assume.assumeFalse(
+ DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(instrumentation.context))
tapl.setEnableRotation(true)
tapl.setExpectedRotation(rotation.value)
testApp.enterDesktopMode(wmHelper, device)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt
index 60a0fb5..675b63c 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt
@@ -23,12 +23,12 @@
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
-import com.android.internal.R
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.window.flags.Flags
import com.android.wm.shell.Utils
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import org.junit.After
import org.junit.Assume
import org.junit.Before
@@ -54,8 +54,8 @@
fun setup() {
Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
// Skip the test when the drag-to-maximize is disabled on this device.
- Assume.assumeTrue(Flags.enableDragToMaximize() &&
- instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode))
+ Assume.assumeTrue(
+ DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(instrumentation.context))
tapl.setEnableRotation(true)
tapl.setExpectedRotation(rotation.value)
ChangeDisplayOrientationRule.setRotation(rotation)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt
index 81c46f1..b9a5e4a9 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt
@@ -25,6 +25,7 @@
import android.tools.flicker.rules.ChangeDisplayOrientationRule
import android.tools.traces.parsers.WindowManagerStateHelper
import android.util.DisplayMetrics
+import android.window.DesktopExperienceFlags
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
@@ -64,7 +65,7 @@
@Before
fun setup() {
Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
- Assume.assumeTrue(Flags.enableDisplayWindowingModeSwitching())
+ Assume.assumeTrue(DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue)
tapl.setEnableRotation(true)
tapl.setExpectedRotation(rotation.value)
ChangeDisplayOrientationRule.setRotation(rotation)
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
index 7f48499..e39fa3a 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -22,7 +22,6 @@
import android.tools.flicker.legacy.LegacyFlickerTest
import android.tools.flicker.legacy.LegacyFlickerTestFactory
import android.tools.traces.component.ComponentNameMatcher
-import android.tools.traces.component.EdgeExtensionComponentMatcher
import androidx.test.filters.FlakyTest
import androidx.test.filters.RequiresDevice
import com.android.wm.shell.flicker.splitscreen.benchmark.CopyContentInSplitBenchmark
@@ -99,7 +98,6 @@
ComponentNameMatcher.SPLASH_SCREEN,
ComponentNameMatcher.SNAPSHOT,
ComponentNameMatcher.IME_SNAPSHOT,
- EdgeExtensionComponentMatcher(),
magnifierLayer,
popupWindowLayer
)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
index 0ff7230..f0c97d3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
@@ -101,7 +101,7 @@
private fun testDisplayWindowingModeSwitch(
defaultWindowingMode: Int,
extendedDisplayEnabled: Boolean,
- expectTransition: Boolean,
+ expectToSwitch: Boolean,
) {
defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode
whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(defaultWindowingMode)
@@ -113,10 +113,14 @@
settingsSession.use {
connectExternalDisplay()
- defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+ if (expectToSwitch) {
+ // Assumes [connectExternalDisplay] properly triggered the switching transition.
+ // Will verify the transition later along with [disconnectExternalDisplay].
+ defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+ }
disconnectExternalDisplay()
- if (expectTransition) {
+ if (expectToSwitch) {
val arg = argumentCaptor<WindowContainerTransaction>()
verify(transitions, times(2))
.startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
@@ -139,7 +143,7 @@
testDisplayWindowingModeSwitch(
defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
extendedDisplayEnabled = false,
- expectTransition = false,
+ expectToSwitch = false,
)
}
@@ -148,7 +152,7 @@
testDisplayWindowingModeSwitch(
defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
extendedDisplayEnabled = true,
- expectTransition = true,
+ expectToSwitch = true,
)
}
@@ -157,7 +161,7 @@
testDisplayWindowingModeSwitch(
defaultWindowingMode = WINDOWING_MODE_FREEFORM,
extendedDisplayEnabled = true,
- expectTransition = false,
+ expectToSwitch = false,
)
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index ac1deec..04acaef 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -261,6 +261,7 @@
@Mock private lateinit var desksTransitionsObserver: DesksTransitionObserver
@Mock private lateinit var packageManager: PackageManager
@Mock private lateinit var mockDisplayContext: Context
+ @Mock private lateinit var dragToDisplayTransitionHandler: DragToDisplayTransitionHandler
private lateinit var controller: DesktopTasksController
private lateinit var shellInit: ShellInit
@@ -431,6 +432,7 @@
desksTransitionsObserver,
userProfileContexts,
desktopModeCompatPolicy,
+ dragToDisplayTransitionHandler,
)
@After
@@ -2069,6 +2071,21 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToFullscreen_fromDeskWithMultipleTasks_deactivatesDesk() {
+ val deskId = 1
+ taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+
+ controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN)
+
+ val wct = getLatestExitDesktopWct()
+ verify(desksOrganizer).deactivateDesk(wct, deskId = deskId)
+ }
+
+ @Test
fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() {
val task = setUpFreeformTask()
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
@@ -2278,7 +2295,10 @@
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+ )
fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() {
val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
@@ -2305,29 +2325,6 @@
}
@Test
- @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
- fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() {
- val homeTask = setUpHomeTask()
- val task1 = setUpFreeformTask()
- // Setup task2
- setUpFreeformTask()
-
- val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)
- assertNotNull(tdaInfo).configuration.windowConfiguration.windowingMode =
- WINDOWING_MODE_FULLSCREEN
-
- controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN)
-
- val wct = getLatestExitDesktopWct()
- val task1Change = assertNotNull(wct.changes[task1.token.asBinder()])
- assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
- verify(desktopModeEnterExitTransitionListener)
- .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
- // Does not remove wallpaper activity, as desktop still has a visible desktop task
- wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = false))
- }
-
- @Test
fun moveToFullscreen_nonExistentTask_doesNothing() {
controller.moveToFullscreen(999, transitionSource = UNKNOWN)
verifyExitDesktopWCTNotExecuted()
@@ -4455,7 +4452,10 @@
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+ )
fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() {
val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
@@ -4480,27 +4480,6 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
- fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() {
- val homeTask = setUpHomeTask()
- val task1 = setUpFreeformTask()
- val task2 = setUpFreeformTask()
- val task3 = setUpFreeformTask()
-
- task1.isFocused = false
- task2.isFocused = true
- task3.isFocused = false
- controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
-
- val wct = getLatestExitDesktopWct()
- val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
- assertThat(taskChange.windowingMode)
- .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
- // Does not remove wallpaper activity
- wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = null))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveFocusedTaskToFullscreen_multipleVisibleTasks_fullscreenOverHome_multiDesksEnabled() {
val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
@@ -5031,7 +5010,7 @@
Mockito.argThat { wct ->
return@argThat wct.hierarchyOps[0].isReparent
},
- eq(null),
+ eq(dragToDisplayTransitionHandler),
)
}
@@ -5225,6 +5204,10 @@
}
@Test
+ @DisableFlags(
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+ )
fun enterSplit_multipleVisibleNonMinimizedTasks_removesWallpaperActivity() {
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt
new file mode 100644
index 0000000..51c3029
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.desktopmode
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import com.android.wm.shell.transition.Transitions
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/**
+ * Test class for {@link DragToDisplayTransitionHandler}
+ *
+ * Usage: atest WMShellUnitTests:DragToDisplayTransitionHandlerTest
+ */
+class DragToDisplayTransitionHandlerTest {
+ private lateinit var handler: DragToDisplayTransitionHandler
+ private val mockTransition: IBinder = mock()
+ private val mockRequestInfo: TransitionRequestInfo = mock()
+ private val mockTransitionInfo: TransitionInfo = mock()
+ private val mockStartTransaction: SurfaceControl.Transaction = mock()
+ private val mockFinishTransaction: SurfaceControl.Transaction = mock()
+ private val mockFinishCallback: Transitions.TransitionFinishCallback = mock()
+
+ @Before
+ fun setUp() {
+ handler = DragToDisplayTransitionHandler()
+ whenever(mockStartTransaction.setWindowCrop(any(), any(), any()))
+ .thenReturn(mockStartTransaction)
+ whenever(mockFinishTransaction.setWindowCrop(any(), any(), any()))
+ .thenReturn(mockFinishTransaction)
+ }
+
+ @Test
+ fun handleRequest_anyRequest_returnsNull() {
+ val result = handler.handleRequest(mockTransition, mockRequestInfo)
+ assert(result == null)
+ }
+
+ @Test
+ fun startAnimation_verifyTransformationsApplied() {
+ val mockChange1 = mock<TransitionInfo.Change>()
+ val leash1 = mock<SurfaceControl>()
+ val endBounds1 = Rect(0, 0, 50, 50)
+ val endPosition1 = Point(5, 5)
+
+ whenever(mockChange1.leash).doReturn(leash1)
+ whenever(mockChange1.endAbsBounds).doReturn(endBounds1)
+ whenever(mockChange1.endRelOffset).doReturn(endPosition1)
+
+ val mockChange2 = mock<TransitionInfo.Change>()
+ val leash2 = mock<SurfaceControl>()
+ val endBounds2 = Rect(100, 100, 200, 150)
+ val endPosition2 = Point(15, 25)
+
+ whenever(mockChange2.leash).doReturn(leash2)
+ whenever(mockChange2.endAbsBounds).doReturn(endBounds2)
+ whenever(mockChange2.endRelOffset).doReturn(endPosition2)
+
+ whenever(mockTransitionInfo.changes).doReturn(listOf(mockChange1, mockChange2))
+
+ handler.startAnimation(
+ mockTransition,
+ mockTransitionInfo,
+ mockStartTransaction,
+ mockFinishTransaction,
+ mockFinishCallback,
+ )
+
+ verify(mockStartTransaction).setWindowCrop(leash1, endBounds1.width(), endBounds1.height())
+ verify(mockStartTransaction)
+ .setPosition(leash1, endPosition1.x.toFloat(), endPosition1.y.toFloat())
+ verify(mockStartTransaction).setWindowCrop(leash2, endBounds2.width(), endBounds2.height())
+ verify(mockStartTransaction)
+ .setPosition(leash2, endPosition2.x.toFloat(), endPosition2.y.toFloat())
+ verify(mockStartTransaction).apply()
+ verify(mockFinishCallback).onTransitionFinished(null)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
index 4082ffd..fb62ba7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
@@ -16,6 +16,7 @@
package com.android.wm.shell.shared.desktopmode
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.content.Context
import android.content.res.Resources
import android.platform.test.annotations.DisableFlags
@@ -27,6 +28,7 @@
import com.android.internal.R
import com.android.window.flags.Flags
import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.util.createTaskInfo
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -152,6 +154,70 @@
assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isTrue()
}
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS,
+ )
+ @Test
+ fun shouldSetBackground_BTWFlagEnabled_freeformTask_returnsTrue() {
+ val freeFormTaskInfo = createTaskInfo(deviceWindowingMode = WINDOWING_MODE_FREEFORM)
+ assertThat(DesktopModeStatus.shouldSetBackground(freeFormTaskInfo)).isTrue()
+ }
+
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS,
+ )
+ @Test
+ fun shouldSetBackground_BTWFlagEnabled_notFreeformTask_returnsFalse() {
+ val notFreeFormTaskInfo = createTaskInfo()
+ assertThat(DesktopModeStatus.shouldSetBackground(notFreeFormTaskInfo)).isFalse()
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ @DisableFlags(Flags.FLAG_ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS)
+ @Test
+ fun shouldSetBackground_BTWFlagDisabled_freeformTaskAndFluid_returnsTrue() {
+ val freeFormTaskInfo = createTaskInfo(deviceWindowingMode = WINDOWING_MODE_FREEFORM)
+
+ setIsVeiledResizeEnabled(false)
+ assertThat(DesktopModeStatus.shouldSetBackground(freeFormTaskInfo)).isTrue()
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ @DisableFlags(Flags.FLAG_ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS)
+ @Test
+ fun shouldSetBackground_BTWFlagDisabled_freeformTaskAndVeiled_returnsFalse() {
+ val freeFormTaskInfo = createTaskInfo(deviceWindowingMode = WINDOWING_MODE_FREEFORM)
+
+ setIsVeiledResizeEnabled(true)
+ assertThat(DesktopModeStatus.shouldSetBackground(freeFormTaskInfo)).isFalse()
+ }
+
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS,
+ )
+ @Test
+ fun shouldSetBackground_BTWFlagEnabled_freeformTaskAndFluid_returnsTrue() {
+ val freeFormTaskInfo = createTaskInfo(deviceWindowingMode = WINDOWING_MODE_FREEFORM)
+
+ setIsVeiledResizeEnabled(false)
+ assertThat(DesktopModeStatus.shouldSetBackground(freeFormTaskInfo)).isTrue()
+ }
+
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS,
+ )
+ @Test
+ fun shouldSetBackground_BTWFlagEnabled_windowModesTask_freeformTaskAndVeiled_returnsTrue() {
+ val freeFormTaskInfo = createTaskInfo(deviceWindowingMode = WINDOWING_MODE_FREEFORM)
+
+ setIsVeiledResizeEnabled(true)
+ assertThat(DesktopModeStatus.shouldSetBackground(freeFormTaskInfo)).isTrue()
+ }
+
@Test
fun isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktop_returnsTrue() {
doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported))
@@ -254,4 +320,11 @@
deviceRestrictions.isAccessible = true
deviceRestrictions.setBoolean(/* obj= */ null, /* z= */ !eligible)
}
+
+ private fun setIsVeiledResizeEnabled(enabled: Boolean) {
+ val deviceRestrictions =
+ DesktopModeStatus::class.java.getDeclaredField("IS_VEILED_RESIZE_ENABLED")
+ deviceRestrictions.isAccessible = true
+ deviceRestrictions.setBoolean(/* obj= */ null, /* z= */ enabled)
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index 6f73db0..6773307 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -1316,9 +1316,11 @@
mTransactionPool, createTestDisplayController(), mMainExecutor,
mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
mock(FocusTransitionObserver.class));
+ final RecentTasksController mockRecentsTaskController = mock(RecentTasksController.class);
+ doReturn(mContext).when(mockRecentsTaskController).getContext();
final RecentsTransitionHandler recentsHandler =
new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions,
- mock(RecentTasksController.class), mock(HomeTransitionObserver.class));
+ mockRecentsTaskController, mock(HomeTransitionObserver.class));
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
shellInit.init();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 16c7935..e89a122 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -321,6 +321,19 @@
}
@Test
+ fun testOnTaskInfoChanged_tilingNotified() {
+ val task = createTask(
+ windowingMode = WINDOWING_MODE_FREEFORM
+ )
+ setUpMockDecorationsForTasks(task)
+
+ onTaskOpening(task)
+ desktopModeWindowDecorViewModel.onTaskInfoChanged(task)
+
+ verify(mockTilingWindowDecoration).onTaskInfoChange(task)
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING)
fun testInsetsStateChanged_notifiesAllDecorsInDisplay() {
val task1 = createTask(windowingMode = WINDOWING_MODE_FREEFORM, displayId = 1)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index af01623..a2927fa 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -24,7 +24,6 @@
import static android.view.WindowInsets.Type.mandatorySystemGestures;
import static android.view.WindowInsets.Type.statusBars;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlBuilder;
import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlTransaction;
@@ -51,7 +50,6 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import static org.mockito.quality.Strictness.LENIENT;
import android.annotation.NonNull;
import android.app.ActivityManager;
@@ -79,13 +77,11 @@
import androidx.test.filters.SmallTest;
-import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
-import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.tests.R;
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost;
@@ -564,12 +560,7 @@
}
@Test
- public void testRelayout_fluidResizeEnabled_freeformTask_setTaskSurfaceColor() {
- StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
- DesktopModeStatus.class).strictness(
- LENIENT).startMocking();
- when(DesktopModeStatus.isVeiledResizeEnabled()).thenReturn(false);
-
+ public void testRelayout_shouldSetBackground_freeformTask_setTaskSurfaceColor() {
final Display defaultDisplay = mock(Display.class);
doReturn(defaultDisplay).when(mMockDisplayController)
.getDisplay(Display.DEFAULT_DISPLAY);
@@ -595,11 +586,10 @@
.build();
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+ mRelayoutParams.mShouldSetBackground = true;
windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlStartT).setColor(mMockTaskSurface, new float[]{1.f, 1.f, 0.f});
-
- mockitoSession.finishMocking();
}
@Test
@@ -627,11 +617,7 @@
}
@Test
- public void testRelayout_fluidResizeEnabled_fullscreenTask_clearTaskSurfaceColor() {
- StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
- DesktopModeStatus.class).strictness(LENIENT).startMocking();
- when(DesktopModeStatus.isVeiledResizeEnabled()).thenReturn(false);
-
+ public void testRelayout_shouldNotSetBackground_fullscreenTask_clearTaskSurfaceColor() {
final Display defaultDisplay = mock(Display.class);
doReturn(defaultDisplay).when(mMockDisplayController)
.getDisplay(Display.DEFAULT_DISPLAY);
@@ -655,12 +641,11 @@
.setWindowingMode(WINDOWING_MODE_FULLSCREEN)
.build();
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+ mRelayoutParams.mIsCaptionVisible = false;
windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlStartT).unsetColor(mMockTaskSurface);
-
- mockitoSession.finishMocking();
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt
index 121e0e9..8442056 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt
@@ -81,6 +81,7 @@
transactionSupplierMock,
BOUNDS,
context,
+ /* isDarkMode= */ true
)
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt
index 9a9d05a..9a3d5d8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt
@@ -60,7 +60,7 @@
tilingDividerView =
LayoutInflater.from(mContext).inflate(R.layout.tiling_split_divider, /* root= */ null)
as TilingDividerView
- tilingDividerView.setup(dividerMoveCallbackMock, DIVIDER_BOUNDS, HANDLE_SIZE)
+ tilingDividerView.setup(dividerMoveCallbackMock, DIVIDER_BOUNDS, HANDLE_SIZE, true)
tilingDividerView.handleY = 0..1500
}
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 38ac8ab..a892e88 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -141,6 +141,7 @@
"libsync",
"libui",
"aconfig_text_flags_c_lib",
+ "aconfig_view_accessibility_flags_c_lib",
"server_configurable_flags",
"libaconfig_storage_read_api_cc",
"libgraphicsenv",
diff --git a/libs/hwui/FeatureFlags.h b/libs/hwui/FeatureFlags.h
index 5f84f47..5ceb97c 100644
--- a/libs/hwui/FeatureFlags.h
+++ b/libs/hwui/FeatureFlags.h
@@ -18,6 +18,7 @@
#define ANDROID_HWUI_FEATURE_FLAGS_H
#ifdef __ANDROID__
+#include <android_view_accessibility.h>
#include <com_android_text_flags.h>
#endif // __ANDROID__
@@ -44,6 +45,19 @@
} // namespace text_feature
+namespace view_accessibility_flags {
+
+inline bool force_invert_color() {
+#ifdef __ANDROID__
+ static bool flag = android::view::accessibility::force_invert_color();
+ return flag;
+#else
+ return true;
+#endif // __ANDROID__
+}
+
+} // namespace view_accessibility_flags
+
} // namespace android
#endif // ANDROID_HWUI_FEATURE_FLAGS_H
diff --git a/location/java/android/location/GnssMeasurement.java b/location/java/android/location/GnssMeasurement.java
index 200d4ef..6ae73a2 100644
--- a/location/java/android/location/GnssMeasurement.java
+++ b/location/java/android/location/GnssMeasurement.java
@@ -1484,6 +1484,10 @@
* in an open sky test - the important aspect of this output is that changes in this value are
* indicative of changes on input signal power in the frequency band for this measurement.
*
+ * <p> This field is part of the GnssMeasurement object so it is only reported when the GNSS
+ * measurement is reported. E.g., when a GNSS signal is too weak to be acquired, the AGC value
+ * is not reported.
+ *
* <p> The value is only available if {@link #hasAutomaticGainControlLevelDb()} is {@code true}
*
* @deprecated Use {@link GnssMeasurementsEvent#getGnssAutomaticGainControls()} instead.
diff --git a/location/java/android/location/GnssMeasurementsEvent.java b/location/java/android/location/GnssMeasurementsEvent.java
index 4fc2ee8..8cdfd01 100644
--- a/location/java/android/location/GnssMeasurementsEvent.java
+++ b/location/java/android/location/GnssMeasurementsEvent.java
@@ -158,6 +158,14 @@
/**
* Gets the collection of {@link GnssAutomaticGainControl} associated with the
* current event.
+ *
+ * <p>This field must be reported when the GNSS measurement engine is running, even when the
+ * GnssMeasurement or GnssClock fields are not reported yet. E.g., when a GNSS signal is too
+ * weak to be acquired, the AGC value must still be reported.
+ *
+ * <p>For devices that do not support this field, an empty collection is returned. In that case,
+ * please use {@link GnssMeasurement#hasAutomaticGainControlLevelDb()}
+ * and {@link GnssMeasuremen#getAutomaticGainControlLevelDb()}.
*/
@NonNull
public Collection<GnssAutomaticGainControl> getGnssAutomaticGainControls() {
diff --git a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
index 8b6194f..fb89973 100644
--- a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
+++ b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
@@ -28,7 +28,6 @@
import android.util.Log;
import com.android.internal.annotations.KeepForWeakReference;
-import com.android.internal.telephony.flags.Flags;
import java.util.concurrent.TimeUnit;
@@ -146,17 +145,12 @@
< emergencyExtensionMillis);
boolean isInEmergencyCallback = false;
boolean isInEmergencySmsMode = false;
- if (!Flags.enforceTelephonyFeatureMappingForPublicApis()) {
+ PackageManager pm = mContext.getPackageManager();
+ if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) {
isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
+ }
+ if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) {
isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
- } else {
- PackageManager pm = mContext.getPackageManager();
- if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) {
- isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
- }
- if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) {
- isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
- }
}
return mIsInEmergencyCall || isInEmergencyCallback || isInEmergencyExtension
|| isInEmergencySmsMode;
diff --git a/media/java/android/media/flags/projection.aconfig b/media/java/android/media/flags/projection.aconfig
index 6d4f0b4..846448b 100644
--- a/media/java/android/media/flags/projection.aconfig
+++ b/media/java/android/media/flags/projection.aconfig
@@ -39,3 +39,12 @@
}
is_exported: true
}
+
+flag {
+ namespace: "media_projection"
+ name: "app_content_sharing"
+ description: "Enable apps to share some sub-surface"
+ bug: "379989921"
+ is_exported: true
+}
+
diff --git a/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt
index 9d037e9..806580b 100644
--- a/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt
+++ b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt
@@ -17,20 +17,20 @@
package com.android.settingslib.widget
import android.content.Context
-import android.os.Build
import android.text.TextUtils
import android.util.AttributeSet
import android.view.View
-import androidx.annotation.RequiresApi
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import com.android.settingslib.widget.preference.intro.R
-class IntroPreference @JvmOverloads constructor(
+class IntroPreference
+@JvmOverloads
+constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
- defStyleRes: Int = 0
+ defStyleRes: Int = 0,
) : Preference(context, attrs, defStyleAttr, defStyleRes), GroupSectionDividerMixin {
private var isCollapsable: Boolean = true
@@ -66,9 +66,9 @@
/**
* Sets whether the summary is collapsable.
+ *
* @param collapsable True if the summary should be collapsable, false otherwise.
*/
- @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
fun setCollapsable(collapsable: Boolean) {
isCollapsable = collapsable
minLines = if (isCollapsable) DEFAULT_MIN_LINES else DEFAULT_MAX_LINES
@@ -77,9 +77,9 @@
/**
* Sets the minimum number of lines to display when collapsed.
+ *
* @param lines The minimum number of lines.
*/
- @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
fun setMinLines(lines: Int) {
minLines = lines.coerceIn(1, DEFAULT_MAX_LINES)
notifyChanged()
@@ -87,9 +87,9 @@
/**
* Sets the action when clicking on the hyperlink in the text.
+ *
* @param listener The click listener for hyperlink.
*/
- @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
fun setHyperlinkListener(listener: View.OnClickListener) {
if (hyperlinkListener != listener) {
hyperlinkListener = listener
@@ -99,9 +99,9 @@
/**
* Sets the action when clicking on the learn more view.
+ *
* @param listener The click listener for learn more.
*/
- @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
fun setLearnMoreAction(listener: View.OnClickListener) {
if (learnMoreListener != listener) {
learnMoreListener = listener
@@ -111,9 +111,9 @@
/**
* Sets the text of learn more view.
+ *
* @param text The text of learn more.
*/
- @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
fun setLearnMoreText(text: CharSequence) {
if (!TextUtils.equals(learnMoreText, text)) {
learnMoreText = text
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
index 1cb8005..02bef9f 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -59,8 +59,6 @@
private val preferenceHierarchy: PreferenceHierarchy,
) : KeyedDataObservable<String>() {
- private val mainExecutor = HandlerExecutor.main
-
private val preferenceLifecycleContext =
object : PreferenceLifecycleContext(context) {
override val lifecycleScope: LifecycleCoroutineScope
@@ -88,11 +86,11 @@
private val preferences: ImmutableMap<String, PreferenceHierarchyNode>
private val dependencies: ImmutableMultimap<String, String>
private val lifecycleAwarePreferences: Array<PreferenceLifecycleProvider>
- private val storages = mutableMapOf<String, KeyedObservable<String>>()
+ private val observables = mutableMapOf<String, KeyedObservable<String>>()
private val preferenceObserver: KeyedObserver<String?>
- private val storageObserver =
+ private val observer =
KeyedObserver<String> { key, reason ->
if (DataChangeReason.isDataChange(reason)) {
notifyChange(key, PreferenceChangeReason.VALUE)
@@ -133,15 +131,19 @@
this.dependencies = dependenciesBuilder.build()
this.lifecycleAwarePreferences = lifecycleAwarePreferences.toTypedArray()
+ val executor = HandlerExecutor.main
preferenceObserver = KeyedObserver { key, reason -> onPreferenceChange(key, reason) }
- addObserver(preferenceObserver, mainExecutor)
+ addObserver(preferenceObserver, executor)
preferenceScreen.forEachRecursively {
- it.preferenceDataStore?.findKeyValueStore()?.let { keyValueStore ->
- val key = it.key
- storages[key] = keyValueStore
- keyValueStore.addObserver(key, storageObserver, mainExecutor)
- }
+ val key = it.key ?: return@forEachRecursively
+ @Suppress("UNCHECKED_CAST")
+ val observable =
+ it.preferenceDataStore?.findKeyValueStore()
+ ?: (preferences[key]?.metadata as? KeyedObservable<String>)
+ ?: return@forEachRecursively
+ observables[key] = observable
+ observable.addObserver(key, observer, executor)
}
}
@@ -212,7 +214,7 @@
fun onDestroy() {
removeObserver(preferenceObserver)
- for ((key, storage) in storages) storage.removeObserver(key, storageObserver)
+ for ((key, observable) in observables) observable.removeObserver(key, observer)
for (preference in lifecycleAwarePreferences) {
preference.onDestroy(preferenceLifecycleContext)
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
index ebd5a1d..3625c00 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -29,6 +29,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.os.UserHandle;
+import android.os.UserManager;
import android.telephony.TelephonyManager;
import android.util.Log;
@@ -37,6 +38,8 @@
import androidx.annotation.VisibleForTesting;
import com.android.settingslib.R;
+import com.android.settingslib.flags.Flags;
+import com.android.settingslib.utils.ThreadUtils;
import java.util.Collection;
import java.util.HashMap;
@@ -65,6 +68,7 @@
private final android.os.Handler mReceiverHandler;
private final UserHandle mUserHandle;
private final Context mContext;
+ private boolean mIsWorkProfile = false;
interface Handler {
void onReceive(Context context, Intent intent, BluetoothDevice device);
@@ -140,6 +144,9 @@
addHandler(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED, new AutoOnStateChangedHandler());
registerAdapterIntentReceiver();
+
+ UserManager userManager = context.getSystemService(UserManager.class);
+ mIsWorkProfile = userManager != null && userManager.isManagedProfile();
}
/** Register to start receiving callbacks for Bluetooth events. */
@@ -220,20 +227,32 @@
callback.onProfileConnectionStateChanged(device, state, bluetoothProfile);
}
+ if (mIsWorkProfile) {
+ Log.d(TAG, "Skip profileConnectionStateChanged for audio sharing, work profile");
+ return;
+ }
+
+ LocalBluetoothLeBroadcast broadcast = mBtManager == null ? null
+ : mBtManager.getProfileManager().getLeAudioBroadcastProfile();
+ LocalBluetoothLeBroadcastAssistant assistant = mBtManager == null ? null
+ : mBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
// Trigger updateFallbackActiveDeviceIfNeeded when ASSISTANT profile disconnected when
// audio sharing is enabled.
if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
&& state == BluetoothAdapter.STATE_DISCONNECTED
- && BluetoothUtils.isAudioSharingUIAvailable(mContext)) {
- LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
- if (profileManager != null
- && profileManager.getLeAudioBroadcastProfile() != null
- && profileManager.getLeAudioBroadcastProfile().isProfileReady()
- && profileManager.getLeAudioBroadcastAssistantProfile() != null
- && profileManager.getLeAudioBroadcastAssistantProfile().isProfileReady()) {
- Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, ASSISTANT profile disconnected");
- profileManager.getLeAudioBroadcastProfile().updateFallbackActiveDeviceIfNeeded();
- }
+ && BluetoothUtils.isAudioSharingUIAvailable(mContext)
+ && broadcast != null && assistant != null && broadcast.isProfileReady()
+ && assistant.isProfileReady()) {
+ Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, ASSISTANT profile disconnected");
+ broadcast.updateFallbackActiveDeviceIfNeeded();
+ }
+ // Dispatch handleOnProfileStateChanged to local broadcast profile
+ if (Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()
+ && broadcast != null
+ && state == BluetoothAdapter.STATE_CONNECTED) {
+ Log.d(TAG, "dispatchProfileConnectionStateChanged to local broadcast profile");
+ var unused = ThreadUtils.postOnBackgroundThread(
+ () -> broadcast.handleProfileConnected(device, bluetoothProfile, mBtManager));
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 31948e4..e78a692 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -719,6 +719,30 @@
}
}
+ /** Check if the {@link CachedBluetoothDevice} is a media device */
+ @WorkerThread
+ public static boolean isMediaDevice(@Nullable CachedBluetoothDevice cachedDevice) {
+ if (cachedDevice == null) return false;
+ return cachedDevice.getProfiles().stream()
+ .anyMatch(
+ profile ->
+ profile instanceof A2dpProfile
+ || profile instanceof HearingAidProfile
+ || profile instanceof LeAudioProfile
+ || profile instanceof HeadsetProfile);
+ }
+
+ /** Check if the {@link CachedBluetoothDevice} supports LE Audio profile */
+ @WorkerThread
+ public static boolean isLeAudioSupported(@Nullable CachedBluetoothDevice cachedDevice) {
+ if (cachedDevice == null) return false;
+ return cachedDevice.getProfiles().stream()
+ .anyMatch(
+ profile ->
+ profile instanceof LeAudioProfile
+ && profile.isEnabled(cachedDevice.getDevice()));
+ }
+
/** Returns if the broadcast is on-going. */
@WorkerThread
public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index f18a2da..08f7806 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -54,6 +54,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.annotation.WorkerThread;
import com.android.settingslib.R;
import com.android.settingslib.flags.Flags;
@@ -64,6 +65,7 @@
import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
+import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -107,6 +109,7 @@
private static final String SETTINGS_PKG = "com.android.settings";
private static final String SYSUI_PKG = "com.android.systemui";
private static final String TAG = "LocalBluetoothLeBroadcast";
+ private static final String AUTO_REJOIN_BROADCAST_TAG = "REJOIN_LE_BROADCAST_ID";
private static final boolean DEBUG = BluetoothUtils.D;
private static final String VALID_PASSWORD_CHARACTERS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,"
@@ -120,6 +123,7 @@
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
static final int UNKNOWN_VALUE_PLACEHOLDER = -1;
+ private static final int JUST_BOND_MILLIS_THRESHOLD = 30000; // 30s
private static final Uri[] SETTINGS_URIS =
new Uri[] {
Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_NAME),
@@ -1283,4 +1287,87 @@
UserManager userManager = context.getSystemService(UserManager.class);
return userManager != null && userManager.isManagedProfile();
}
+
+ /** Handle profile connected for {@link CachedBluetoothDevice}. */
+ @WorkerThread
+ public void handleProfileConnected(@NonNull CachedBluetoothDevice cachedDevice,
+ int bluetoothProfile, @Nullable LocalBluetoothManager btManager) {
+ if (!Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()) {
+ Log.d(TAG, "Skip handleProfileConnected, flag off");
+ return;
+ }
+ if (!SYSUI_PKG.equals(mContext.getPackageName())) {
+ Log.d(TAG, "Skip handleProfileConnected, not a valid caller");
+ return;
+ }
+ if (!BluetoothUtils.isMediaDevice(cachedDevice)) {
+ Log.d(TAG, "Skip handleProfileConnected, not a media device");
+ return;
+ }
+ Timestamp bondTimestamp = cachedDevice.getBondTimestamp();
+ if (bondTimestamp != null) {
+ long diff = System.currentTimeMillis() - bondTimestamp.getTime();
+ if (diff <= JUST_BOND_MILLIS_THRESHOLD) {
+ Log.d(TAG, "Skip handleProfileConnected, just bond within " + diff);
+ return;
+ }
+ }
+ if (!isEnabled(null)) {
+ Log.d(TAG, "Skip handleProfileConnected, not broadcasting");
+ return;
+ }
+ BluetoothDevice device = cachedDevice.getDevice();
+ if (device == null) {
+ Log.d(TAG, "Skip handleProfileConnected, null device");
+ return;
+ }
+ // TODO: sync source in a reasonable place
+ if (BluetoothUtils.hasConnectedBroadcastSourceForBtDevice(device, btManager)) {
+ Log.d(TAG, "Skip handleProfileConnected, already has source");
+ return;
+ }
+ if (isAutoRejoinDevice(device)) {
+ Log.d(TAG, "Skip handleProfileConnected, auto rejoin device");
+ return;
+ }
+ boolean isLeAudioSupported = BluetoothUtils.isLeAudioSupported(cachedDevice);
+ // For eligible (LE audio) remote device, we only check assistant profile connected.
+ if (isLeAudioSupported
+ && bluetoothProfile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
+ Log.d(TAG, "Skip handleProfileConnected, lea sink, not the assistant profile");
+ return;
+ }
+ boolean isFirstConnectedProfile = isFirstConnectedProfile(cachedDevice, bluetoothProfile);
+ // For ineligible (classic) remote device, we only check its first connected profile.
+ if (!isLeAudioSupported && !isFirstConnectedProfile) {
+ Log.d(TAG, "Skip handleProfileConnected, classic sink, not the first profile");
+ return;
+ }
+
+ Intent intent = new Intent(
+ LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED);
+ intent.putExtra(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device);
+ intent.setPackage(SETTINGS_PKG);
+ Log.d(TAG, "notify device connected, device = " + device.getAnonymizedAddress());
+
+ mContext.sendBroadcast(intent);
+ }
+
+ private boolean isAutoRejoinDevice(@Nullable BluetoothDevice bluetoothDevice) {
+ String metadataValue = BluetoothUtils.getFastPairCustomizedField(bluetoothDevice,
+ AUTO_REJOIN_BROADCAST_TAG);
+ return getLatestBroadcastId() != UNKNOWN_VALUE_PLACEHOLDER && Objects.equals(metadataValue,
+ String.valueOf(getLatestBroadcastId()));
+ }
+
+ private boolean isFirstConnectedProfile(@Nullable CachedBluetoothDevice cachedDevice,
+ int bluetoothProfile) {
+ if (cachedDevice == null) return false;
+ return cachedDevice.getProfiles().stream()
+ .noneMatch(
+ profile ->
+ profile.getProfileId() != bluetoothProfile
+ && profile.getConnectionStatus(cachedDevice.getDevice())
+ == BluetoothProfile.STATE_CONNECTED);
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java
index ae17acb..8bb41cc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java
+++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java
@@ -16,8 +16,8 @@
package com.android.settingslib.qrcode;
+import android.annotation.NonNull;
import android.content.Context;
-import android.content.res.Configuration;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
@@ -75,12 +75,29 @@
@VisibleForTesting
Camera mCamera;
+ Camera.CameraInfo mCameraInfo;
+
+ /**
+ * The size of the preview image as requested to camera, e.g. 1920x1080.
+ */
private Size mPreviewSize;
+
+ /**
+ * Whether the preview image would be displayed in "portrait" (width less
+ * than height) orientation in current display orientation.
+ *
+ * Note that we don't distinguish between a rotation of 90 degrees or 270
+ * degrees here, since we center crop all the preview.
+ *
+ * TODO: Handle external camera / multiple display, this likely requires
+ * migrating to newer Camera2 API.
+ */
+ private boolean mPreviewInPortrait;
+
private WeakReference<Context> mContext;
private ScannerCallback mScannerCallback;
private MultiFormatReader mReader;
private DecodingTask mDecodeTask;
- private int mCameraOrientation;
@VisibleForTesting
Camera.Parameters mParameters;
@@ -152,8 +169,14 @@
* @param previewSize Is the preview size set by camera
* @param cameraOrientation Is the orientation of current Camera
* @return The rectangle would like to crop from the camera preview shot.
+ * @deprecated This is no longer used, and the frame position is
+ * automatically calculated from the preview size and the
+ * background View size.
*/
- Rect getFramePosition(Size previewSize, int cameraOrientation);
+ @Deprecated
+ default @NonNull Rect getFramePosition(@NonNull Size previewSize, int cameraOrientation) {
+ throw new AssertionError("getFramePosition shouldn't be used");
+ }
/**
* Sets the transform to associate with preview area.
@@ -172,6 +195,41 @@
boolean isValid(String qrCode);
}
+ private boolean setPreviewDisplayOrientation() {
+ if (mContext.get() == null) {
+ return false;
+ }
+
+ final WindowManager winManager =
+ (WindowManager) mContext.get().getSystemService(Context.WINDOW_SERVICE);
+ final int rotation = winManager.getDefaultDisplay().getRotation();
+ int degrees = 0;
+ switch (rotation) {
+ case Surface.ROTATION_0:
+ degrees = 0;
+ break;
+ case Surface.ROTATION_90:
+ degrees = 90;
+ break;
+ case Surface.ROTATION_180:
+ degrees = 180;
+ break;
+ case Surface.ROTATION_270:
+ degrees = 270;
+ break;
+ }
+ int rotateDegrees = 0;
+ if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ rotateDegrees = (mCameraInfo.orientation + degrees) % 360;
+ rotateDegrees = (360 - rotateDegrees) % 360; // compensate the mirror
+ } else {
+ rotateDegrees = (mCameraInfo.orientation - degrees + 360) % 360;
+ }
+ mCamera.setDisplayOrientation(rotateDegrees);
+ mPreviewInPortrait = (rotateDegrees == 90 || rotateDegrees == 270);
+ return true;
+ }
+
@VisibleForTesting
void setCameraParameter() {
mParameters = mCamera.getParameters();
@@ -195,37 +253,39 @@
mCamera.setParameters(mParameters);
}
- private boolean startPreview() {
- if (mContext.get() == null) {
- return false;
- }
+ /**
+ * Set transform matrix to crop and center the preview picture.
+ */
+ private void setTransformationMatrix() {
+ final Size previewDisplaySize = rotateIfPortrait(mPreviewSize);
+ final Size viewSize = mScannerCallback.getViewSize();
+ final Rect cropRegion = calculateCenteredCrop(previewDisplaySize, viewSize);
- final WindowManager winManager =
- (WindowManager) mContext.get().getSystemService(Context.WINDOW_SERVICE);
- final int rotation = winManager.getDefaultDisplay().getRotation();
- int degrees = 0;
- switch (rotation) {
- case Surface.ROTATION_0:
- degrees = 0;
- break;
- case Surface.ROTATION_90:
- degrees = 90;
- break;
- case Surface.ROTATION_180:
- degrees = 180;
- break;
- case Surface.ROTATION_270:
- degrees = 270;
- break;
- }
- final int rotateDegrees = (mCameraOrientation - degrees + 360) % 360;
- mCamera.setDisplayOrientation(rotateDegrees);
+ // Note that strictly speaking, since the preview is mirrored in front
+ // camera case, we should also mirror the crop region here. But since
+ // we're cropping at the center, mirroring would result in the same
+ // crop region other than small off-by-one error from floating point
+ // calculation and wouldn't be noticeable.
+
+ // Calculate transformation matrix.
+ float scaleX = previewDisplaySize.getWidth() / (float) cropRegion.width();
+ float scaleY = previewDisplaySize.getHeight() / (float) cropRegion.height();
+ float translateX = -cropRegion.left / (float) cropRegion.width() * viewSize.getWidth();
+ float translateY = -cropRegion.top / (float) cropRegion.height() * viewSize.getHeight();
+
+ // Set the transform matrix.
+ final Matrix matrix = new Matrix();
+ matrix.setScale(scaleX, scaleY);
+ matrix.postTranslate(translateX, translateY);
+ mScannerCallback.setTransform(matrix);
+ }
+
+ private void startPreview() {
mCamera.startPreview();
if (Camera.Parameters.FOCUS_MODE_AUTO.equals(mParameters.getFocusMode())) {
mCamera.autoFocus(/* Camera.AutoFocusCallback */ null);
sendMessageDelayed(obtainMessage(MSG_AUTO_FOCUS), AUTOFOCUS_INTERVAL_MS);
}
- return true;
}
private class DecodingTask extends AsyncTask<Void, Void, String> {
@@ -300,7 +360,7 @@
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
releaseCamera();
mCamera = Camera.open(i);
- mCameraOrientation = cameraInfo.orientation;
+ mCameraInfo = cameraInfo;
break;
}
}
@@ -309,7 +369,7 @@
Camera.getCameraInfo(0, cameraInfo);
releaseCamera();
mCamera = Camera.open(0);
- mCameraOrientation = cameraInfo.orientation;
+ mCameraInfo = cameraInfo;
}
} catch (RuntimeException e) {
Log.e(TAG, "Fail to open camera: " + e);
@@ -323,11 +383,12 @@
throw new IOException("Cannot find available camera");
}
mCamera.setPreviewTexture(surface);
+ if (!setPreviewDisplayOrientation()) {
+ throw new IOException("Lost context");
+ }
setCameraParameter();
setTransformationMatrix();
- if (!startPreview()) {
- throw new IOException("Lost contex");
- }
+ startPreview();
} catch (IOException ioe) {
Log.e(TAG, "Fail to startPreview camera: " + ioe);
mCamera = null;
@@ -345,32 +406,30 @@
}
}
- /** Set transform matrix to crop and center the preview picture */
- private void setTransformationMatrix() {
- final boolean isPortrait = mContext.get().getResources().getConfiguration().orientation
- == Configuration.ORIENTATION_PORTRAIT;
-
- final int previewWidth = isPortrait ? mPreviewSize.getWidth() : mPreviewSize.getHeight();
- final int previewHeight = isPortrait ? mPreviewSize.getHeight() : mPreviewSize.getWidth();
- final float ratioPreview = (float) getRatio(previewWidth, previewHeight);
-
- // Calculate transformation matrix.
- float scaleX = 1.0f;
- float scaleY = 1.0f;
- if (previewWidth > previewHeight) {
- scaleY = scaleX / ratioPreview;
+ /**
+ * Calculates the crop region in `previewSize` to have the same aspect
+ * ratio as `viewSize` and center aligned.
+ */
+ private Rect calculateCenteredCrop(Size previewSize, Size viewSize) {
+ final double previewRatio = getRatio(previewSize);
+ final double viewRatio = getRatio(viewSize);
+ int width;
+ int height;
+ if (previewRatio > viewRatio) {
+ width = previewSize.getWidth();
+ height = (int) Math.round(width * viewRatio);
} else {
- scaleX = scaleY / ratioPreview;
+ height = previewSize.getHeight();
+ width = (int) Math.round(height / viewRatio);
}
-
- // Set the transform matrix.
- final Matrix matrix = new Matrix();
- matrix.setScale(scaleX, scaleY);
- mScannerCallback.setTransform(matrix);
+ final int left = (previewSize.getWidth() - width) / 2;
+ final int top = (previewSize.getHeight() - height) / 2;
+ return new Rect(left, top, left + width, top + height);
}
private QrYuvLuminanceSource getFrameImage(byte[] imageData) {
- final Rect frame = mScannerCallback.getFramePosition(mPreviewSize, mCameraOrientation);
+ final Size viewSize = mScannerCallback.getViewSize();
+ final Rect frame = calculateCenteredCrop(mPreviewSize, rotateIfPortrait(viewSize));
final QrYuvLuminanceSource image = new QrYuvLuminanceSource(imageData,
mPreviewSize.getWidth(), mPreviewSize.getHeight());
return (QrYuvLuminanceSource)
@@ -398,17 +457,18 @@
*/
private Size getBestPreviewSize(Camera.Parameters parameters) {
final double minRatioDiffPercent = 0.1;
- final Size windowSize = mScannerCallback.getViewSize();
- final double winRatio = getRatio(windowSize.getWidth(), windowSize.getHeight());
+ final Size viewSize = rotateIfPortrait(mScannerCallback.getViewSize());
+ final double viewRatio = getRatio(viewSize);
double bestChoiceRatio = 0;
Size bestChoice = new Size(0, 0);
for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
- double ratio = getRatio(size.width, size.height);
+ final Size newSize = toAndroidSize(size);
+ final double ratio = getRatio(newSize);
if (size.height * size.width > bestChoice.getWidth() * bestChoice.getHeight()
- && (Math.abs(bestChoiceRatio - winRatio) / winRatio > minRatioDiffPercent
- || Math.abs(ratio - winRatio) / winRatio <= minRatioDiffPercent)) {
- bestChoice = new Size(size.width, size.height);
- bestChoiceRatio = getRatio(size.width, size.height);
+ && (Math.abs(bestChoiceRatio - viewRatio) / viewRatio > minRatioDiffPercent
+ || Math.abs(ratio - viewRatio) / viewRatio <= minRatioDiffPercent)) {
+ bestChoice = newSize;
+ bestChoiceRatio = ratio;
}
}
return bestChoice;
@@ -419,25 +479,26 @@
* picture size and aspect ratio to choose the best one.
*/
private Size getBestPictureSize(Camera.Parameters parameters) {
- final Camera.Size previewSize = parameters.getPreviewSize();
- final double previewRatio = getRatio(previewSize.width, previewSize.height);
+ final Size previewSize = mPreviewSize;
+ final double previewRatio = getRatio(previewSize);
List<Size> bestChoices = new ArrayList<>();
final List<Size> similarChoices = new ArrayList<>();
// Filter by ratio
- for (Camera.Size size : parameters.getSupportedPictureSizes()) {
- double ratio = getRatio(size.width, size.height);
+ for (Camera.Size picSize : parameters.getSupportedPictureSizes()) {
+ final Size size = toAndroidSize(picSize);
+ final double ratio = getRatio(size);
if (ratio == previewRatio) {
- bestChoices.add(new Size(size.width, size.height));
+ bestChoices.add(size);
} else if (Math.abs(ratio - previewRatio) < MAX_RATIO_DIFF) {
- similarChoices.add(new Size(size.width, size.height));
+ similarChoices.add(size);
}
}
if (bestChoices.size() == 0 && similarChoices.size() == 0) {
Log.d(TAG, "No proper picture size, return default picture size");
Camera.Size defaultPictureSize = parameters.getPictureSize();
- return new Size(defaultPictureSize.width, defaultPictureSize.height);
+ return toAndroidSize(defaultPictureSize);
}
if (bestChoices.size() == 0) {
@@ -447,7 +508,7 @@
// Get the best by area
int bestAreaDifference = Integer.MAX_VALUE;
Size bestChoice = null;
- final int previewArea = previewSize.width * previewSize.height;
+ final int previewArea = previewSize.getWidth() * previewSize.getHeight();
for (Size size : bestChoices) {
int areaDifference = Math.abs(size.getWidth() * size.getHeight() - previewArea);
if (areaDifference < bestAreaDifference) {
@@ -458,8 +519,20 @@
return bestChoice;
}
- private double getRatio(double x, double y) {
- return (x < y) ? x / y : y / x;
+ private Size rotateIfPortrait(Size size) {
+ if (mPreviewInPortrait) {
+ return new Size(size.getHeight(), size.getWidth());
+ } else {
+ return size;
+ }
+ }
+
+ private double getRatio(Size size) {
+ return size.getHeight() / (double) size.getWidth();
+ }
+
+ private Size toAndroidSize(Camera.Size size) {
+ return new Size(size.width, size.height);
}
@VisibleForTesting
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
index b86f4b3..eac69234 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
@@ -23,7 +23,6 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -38,12 +37,14 @@
import android.content.IntentFilter;
import android.os.UserHandle;
import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.telephony.TelephonyManager;
import com.android.settingslib.R;
import com.android.settingslib.flags.Flags;
import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settingslib.utils.ThreadUtils;
import org.junit.Before;
import org.junit.Rule;
@@ -54,6 +55,8 @@
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
import org.robolectric.shadow.api.Shadow;
import java.util.ArrayList;
@@ -61,7 +64,7 @@
import java.util.List;
@RunWith(RobolectricTestRunner.class)
-@Config(shadows = {ShadowBluetoothAdapter.class})
+@Config(shadows = {ShadowBluetoothAdapter.class, BluetoothEventManagerTest.ShadowThreadUtils.class})
public class BluetoothEventManagerTest {
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -100,6 +103,8 @@
private BluetoothUtils.ErrorListener mErrorListener;
@Mock
private LocalBluetoothLeBroadcast mBroadcast;
+ @Mock
+ private UserManager mUserManager;
private Context mContext;
private Intent mIntent;
@@ -130,6 +135,7 @@
mCachedDevice1 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice1);
mCachedDevice2 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice2);
mCachedDevice3 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice3);
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
BluetoothUtils.setErrorListener(mErrorListener);
}
@@ -196,6 +202,7 @@
* callback.
*/
@Test
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
public void dispatchProfileConnectionStateChanged_registerCallback_shouldDispatchCallback() {
mBluetoothEventManager.registerCallback(mBluetoothCallback);
@@ -208,10 +215,12 @@
/**
* dispatchProfileConnectionStateChanged should not call {@link
- * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when audio sharing flag is off.
+ * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and
+ * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when audio sharing flag is off.
*/
@Test
- public void dispatchProfileConnectionStateChanged_flagOff_noUpdateFallbackDevice() {
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+ public void dispatchProfileConnectionStateChanged_flagOff_noCallToBroadcastProfile() {
setUpAudioSharing(/* enableFlag= */ false, /* enableFeature= */ true, /* enableProfile= */
true, /* workProfile= */ false);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -219,16 +228,19 @@
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
- verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
}
/**
* dispatchProfileConnectionStateChanged should not call {@link
- * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when the device does not
- * support audio sharing.
+ * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and
+ * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when the device does not support
+ * audio sharing.
*/
@Test
- public void dispatchProfileConnectionStateChanged_notSupport_noUpdateFallbackDevice() {
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+ public void dispatchProfileConnectionStateChanged_notSupport_noCallToBroadcastProfile() {
setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ false, /* enableProfile= */
true, /* workProfile= */ false);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -236,7 +248,8 @@
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
- verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
}
/**
@@ -245,6 +258,7 @@
* not ready.
*/
@Test
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
public void dispatchProfileConnectionStateChanged_profileNotReady_noUpdateFallbackDevice() {
setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
false, /* workProfile= */ false);
@@ -253,7 +267,7 @@
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
- verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
}
/**
@@ -262,6 +276,7 @@
* other than LE_AUDIO_BROADCAST_ASSISTANT or state other than STATE_DISCONNECTED.
*/
@Test
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
public void dispatchProfileConnectionStateChanged_notAssistantProfile_noUpdateFallbackDevice() {
setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
true, /* workProfile= */ false);
@@ -270,16 +285,17 @@
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO);
- verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
}
/**
* dispatchProfileConnectionStateChanged should not call {@link
- * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when triggered for
- * work profile.
+ * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and
+ * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when triggered for work profile.
*/
@Test
- public void dispatchProfileConnectionStateChanged_workProfile_noUpdateFallbackDevice() {
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+ public void dispatchProfileConnectionStateChanged_workProfile_noCallToBroadcastProfile() {
setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
true, /* workProfile= */ true);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -287,7 +303,8 @@
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
- verify(mBroadcast).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
}
/**
@@ -296,7 +313,8 @@
* disconnected and audio sharing is enabled.
*/
@Test
- public void dispatchProfileConnectionStateChanged_audioSharing_updateFallbackDevice() {
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+ public void dispatchProfileConnectionStateChanged_assistDisconnected_updateFallbackDevice() {
setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
true, /* workProfile= */ false);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -305,6 +323,27 @@
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
verify(mBroadcast).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
+ }
+
+ /**
+ * dispatchProfileConnectionStateChanged should call {@link
+ * LocalBluetoothLeBroadcast}#handleProfileConnected when assistant profile is connected and
+ * audio sharing is enabled.
+ */
+ @Test
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+ public void dispatchProfileConnectionStateChanged_assistConnected_handleStateChanged() {
+ setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
+ true, /* workProfile= */ false);
+ mBluetoothEventManager.dispatchProfileConnectionStateChanged(
+ mCachedBluetoothDevice,
+ BluetoothProfile.STATE_CONNECTED,
+ BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast).handleProfileConnected(mCachedBluetoothDevice,
+ BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, mBtManager);
}
private void setUpAudioSharing(boolean enableFlag, boolean enableFeature,
@@ -325,13 +364,19 @@
LocalBluetoothLeBroadcastAssistant assistant =
mock(LocalBluetoothLeBroadcastAssistant.class);
when(assistant.isProfileReady()).thenReturn(enableProfile);
- LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class);
- when(profileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
- when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
- when(mBtManager.getProfileManager()).thenReturn(profileManager);
- UserManager userManager = mock(UserManager.class);
- when(mContext.getSystemService(UserManager.class)).thenReturn(userManager);
- when(userManager.isManagedProfile()).thenReturn(workProfile);
+ when(mLocalProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+ when(mLocalProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
+ when(mUserManager.isManagedProfile()).thenReturn(workProfile);
+ if (workProfile) {
+ mBluetoothEventManager =
+ new BluetoothEventManager(
+ mLocalAdapter,
+ mBtManager,
+ mCachedDeviceManager,
+ mContext,
+ /* handler= */ null,
+ /* userHandle= */ null);
+ }
}
@Test
@@ -665,4 +710,12 @@
verify(mBluetoothCallback).onAutoOnStateChanged(anyInt());
}
+
+ @Implements(value = ThreadUtils.class)
+ public static class ShadowThreadUtils {
+ @Implementation
+ protected static void postOnBackgroundThread(Runnable runnable) {
+ runnable.run();
+ }
+ }
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index 0325c0e..b781412 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -1349,6 +1349,36 @@
}
@Test
+ public void isMediaDevice_returnsFalse() {
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mAssistant));
+ assertThat(BluetoothUtils.isMediaDevice(mCachedBluetoothDevice)).isFalse();
+ }
+
+ @Test
+ public void isMediaDevice_returnsTrue() {
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+ assertThat(BluetoothUtils.isMediaDevice(mCachedBluetoothDevice)).isTrue();
+ }
+
+ @Test
+ public void isLeAudioSupported_returnsFalse() {
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+ when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+ when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(false);
+
+ assertThat(BluetoothUtils.isLeAudioSupported(mCachedBluetoothDevice)).isFalse();
+ }
+
+ @Test
+ public void isLeAudioSupported_returnsTrue() {
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+ when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+ when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(true);
+
+ assertThat(BluetoothUtils.isLeAudioSupported(mCachedBluetoothDevice)).isTrue();
+ }
+
+ @Test
public void isTemporaryBondDevice_hasMetadata_returnsTrue() {
when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
.thenReturn(TEMP_BOND_METADATA.getBytes());
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java
index a9fd380..76b6aa8 100644
--- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java
@@ -28,6 +28,7 @@
public class ShadowColorDisplayManager extends org.robolectric.shadows.ShadowColorDisplayManager {
private boolean mIsReduceBrightColorsActivated;
+ private int mColorMode;
@Implementation
@SystemApi
@@ -43,4 +44,13 @@
return mIsReduceBrightColorsActivated;
}
+ @Implementation
+ public int getColorMode() {
+ return mColorMode;
+ }
+
+ @Implementation
+ public void setColorMode(int colorMode) {
+ mColorMode = colorMode;
+ }
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
index ed144bd..375dade 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
@@ -78,6 +78,10 @@
val underSurface: Color,
val weatherTemp: Color,
val widgetBackground: Color,
+ val surfaceEffect0: Color,
+ val surfaceEffect1: Color,
+ val surfaceEffect2: Color,
+ val surfaceEffect3: Color,
) {
companion object {
internal fun color(context: Context, @ColorRes id: Int): Color {
@@ -123,6 +127,10 @@
underSurface = color(context, R.color.customColorUnderSurface),
weatherTemp = color(context, R.color.customColorWeatherTemp),
widgetBackground = color(context, R.color.customColorWidgetBackground),
+ surfaceEffect0 = color(context, R.color.surface_effect_0),
+ surfaceEffect1 = color(context, R.color.surface_effect_1),
+ surfaceEffect2 = color(context, R.color.surface_effect_2),
+ surfaceEffect3 = color(context, R.color.surface_effect_3),
)
}
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt
index 71ec63c..84370ed 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt
@@ -31,6 +31,7 @@
import com.android.compose.theme.typography.TypefaceNames
import com.android.compose.theme.typography.TypefaceTokens
import com.android.compose.theme.typography.TypographyTokens
+import com.android.compose.theme.typography.VariableFontTypeScaleEmphasizedTokens
import com.android.compose.theme.typography.platformTypography
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.compose.windowsizeclass.calculateWindowSizeClass
@@ -44,9 +45,15 @@
val colorScheme = remember(context, isDarkTheme) { platformColorScheme(isDarkTheme, context) }
val androidColorScheme = remember(context) { AndroidColorScheme(context) }
val typefaceNames = remember(context) { TypefaceNames.get(context) }
+ val typefaceTokens = remember(typefaceNames) { TypefaceTokens(typefaceNames) }
val typography =
- remember(typefaceNames) {
- platformTypography(TypographyTokens(TypeScaleTokens(TypefaceTokens(typefaceNames))))
+ remember(typefaceTokens) {
+ platformTypography(
+ TypographyTokens(
+ TypeScaleTokens(typefaceTokens),
+ VariableFontTypeScaleEmphasizedTokens(typefaceTokens),
+ )
+ )
}
val windowSizeClass = calculateWindowSizeClass()
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt
index 1ce1ae3..652f946 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt
@@ -16,6 +16,7 @@
package com.android.compose.theme.typography
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography
@@ -25,6 +26,7 @@
* Do not use directly and call [MaterialTheme.typography] instead to access the different text
* styles.
*/
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
internal fun platformTypography(typographyTokens: TypographyTokens): Typography {
return Typography(
displayLarge = typographyTokens.displayLarge,
@@ -42,5 +44,21 @@
labelLarge = typographyTokens.labelLarge,
labelMedium = typographyTokens.labelMedium,
labelSmall = typographyTokens.labelSmall,
+ // GSF emphasized tokens
+ displayLargeEmphasized = typographyTokens.displayLargeEmphasized,
+ displayMediumEmphasized = typographyTokens.displayMediumEmphasized,
+ displaySmallEmphasized = typographyTokens.displaySmallEmphasized,
+ headlineLargeEmphasized = typographyTokens.headlineLargeEmphasized,
+ headlineMediumEmphasized = typographyTokens.headlineMediumEmphasized,
+ headlineSmallEmphasized = typographyTokens.headlineSmallEmphasized,
+ titleLargeEmphasized = typographyTokens.titleLargeEmphasized,
+ titleMediumEmphasized = typographyTokens.titleMediumEmphasized,
+ titleSmallEmphasized = typographyTokens.titleSmallEmphasized,
+ bodyLargeEmphasized = typographyTokens.bodyLargeEmphasized,
+ bodyMediumEmphasized = typographyTokens.bodyMediumEmphasized,
+ bodySmallEmphasized = typographyTokens.bodySmallEmphasized,
+ labelLargeEmphasized = typographyTokens.labelLargeEmphasized,
+ labelMediumEmphasized = typographyTokens.labelMediumEmphasized,
+ labelSmallEmphasized = typographyTokens.labelSmallEmphasized,
)
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt
index 13acfd6..280b8d9 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt
@@ -34,6 +34,29 @@
private val brandFont = DeviceFontFamilyName(typefaceNames.brand)
private val plainFont = DeviceFontFamilyName(typefaceNames.plain)
+ // Google Sans Flex emphasized styles
+ private val displayLargeEmphasizedFont =
+ DeviceFontFamilyName("variable-display-large-emphasized")
+ private val displayMediumEmphasizedFont =
+ DeviceFontFamilyName("variable-display-medium-emphasized")
+ private val displaySmallEmphasizedFont =
+ DeviceFontFamilyName("variable-display-small-emphasized")
+ private val headlineLargeEmphasizedFont =
+ DeviceFontFamilyName("variable-headline-large-emphasized")
+ private val headlineMediumEmphasizedFont =
+ DeviceFontFamilyName("variable-headline-medium-emphasized")
+ private val headlineSmallEmphasizedFont =
+ DeviceFontFamilyName("variable-headline-small-emphasized")
+ private val titleLargeEmphasizedFont = DeviceFontFamilyName("variable-title-large-emphasized")
+ private val titleMediumEmphasizedFont = DeviceFontFamilyName("variable-title-medium-emphasized")
+ private val titleSmallEmphasizedFont = DeviceFontFamilyName("variable-title-small-emphasized")
+ private val bodyLargeEmphasizedFont = DeviceFontFamilyName("variable-body-large-emphasized")
+ private val bodyMediumEmphasizedFont = DeviceFontFamilyName("variable-body-medium-emphasized")
+ private val bodySmallEmphasizedFont = DeviceFontFamilyName("variable-body-small-emphasized")
+ private val labelLargeEmphasizedFont = DeviceFontFamilyName("variable-label-large-emphasized")
+ private val labelMediumEmphasizedFont = DeviceFontFamilyName("variable-label-medium-emphasized")
+ private val labelSmallEmphasizedFont = DeviceFontFamilyName("variable-label-small-emphasized")
+
val brand =
FontFamily(
Font(brandFont, weight = WeightMedium),
@@ -44,6 +67,22 @@
Font(plainFont, weight = WeightMedium),
Font(plainFont, weight = WeightRegular),
)
+
+ val displayLargeEmphasized = FontFamily(Font(displayLargeEmphasizedFont))
+ val displayMediumEmphasized = FontFamily(Font(displayMediumEmphasizedFont))
+ val displaySmallEmphasized = FontFamily(Font(displaySmallEmphasizedFont))
+ val headlineLargeEmphasized = FontFamily(Font(headlineLargeEmphasizedFont))
+ val headlineMediumEmphasized = FontFamily(Font(headlineMediumEmphasizedFont))
+ val headlineSmallEmphasized = FontFamily(Font(headlineSmallEmphasizedFont))
+ val titleLargeEmphasized = FontFamily(Font(titleLargeEmphasizedFont))
+ val titleMediumEmphasized = FontFamily(Font(titleMediumEmphasizedFont))
+ val titleSmallEmphasized = FontFamily(Font(titleSmallEmphasizedFont))
+ val bodyLargeEmphasized = FontFamily(Font(bodyLargeEmphasizedFont))
+ val bodyMediumEmphasized = FontFamily(Font(bodyMediumEmphasizedFont))
+ val bodySmallEmphasized = FontFamily(Font(bodySmallEmphasizedFont))
+ val labelLargeEmphasized = FontFamily(Font(labelLargeEmphasizedFont))
+ val labelMediumEmphasized = FontFamily(Font(labelMediumEmphasizedFont))
+ val labelSmallEmphasized = FontFamily(Font(labelSmallEmphasizedFont))
}
internal data class TypefaceNames
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt
index 38aadb8..4115647 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt
@@ -18,7 +18,10 @@
import androidx.compose.ui.text.TextStyle
-internal class TypographyTokens(typeScaleTokens: TypeScaleTokens) {
+internal class TypographyTokens(
+ typeScaleTokens: TypeScaleTokens,
+ variableTypeScaleTokens: VariableFontTypeScaleEmphasizedTokens,
+) {
val bodyLarge =
TextStyle(
fontFamily = typeScaleTokens.bodyLargeFont,
@@ -139,4 +142,112 @@
lineHeight = typeScaleTokens.titleSmallLineHeight,
letterSpacing = typeScaleTokens.titleSmallTracking,
)
+ // GSF emphasized styles
+ // note: we don't need to define fontWeight or axes values because they are pre-defined
+ // as part of the font family in fonts_customization.xml (for performance optimization)
+ val displayLargeEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.displayLargeFont,
+ fontSize = variableTypeScaleTokens.displayLargeSize,
+ lineHeight = variableTypeScaleTokens.displayLargeLineHeight,
+ letterSpacing = variableTypeScaleTokens.displayLargeTracking,
+ )
+ val displayMediumEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.displayMediumFont,
+ fontSize = variableTypeScaleTokens.displayMediumSize,
+ lineHeight = variableTypeScaleTokens.displayMediumLineHeight,
+ letterSpacing = variableTypeScaleTokens.displayMediumTracking,
+ )
+ val displaySmallEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.displaySmallFont,
+ fontSize = variableTypeScaleTokens.displaySmallSize,
+ lineHeight = variableTypeScaleTokens.displaySmallLineHeight,
+ letterSpacing = variableTypeScaleTokens.displaySmallTracking,
+ )
+ val headlineLargeEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.headlineLargeFont,
+ fontSize = variableTypeScaleTokens.headlineLargeSize,
+ lineHeight = variableTypeScaleTokens.headlineLargeLineHeight,
+ letterSpacing = variableTypeScaleTokens.headlineLargeTracking,
+ )
+ val headlineMediumEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.headlineMediumFont,
+ fontSize = variableTypeScaleTokens.headlineMediumSize,
+ lineHeight = variableTypeScaleTokens.headlineMediumLineHeight,
+ letterSpacing = variableTypeScaleTokens.headlineMediumTracking,
+ )
+ val headlineSmallEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.headlineSmallFont,
+ fontSize = variableTypeScaleTokens.headlineSmallSize,
+ lineHeight = variableTypeScaleTokens.headlineSmallLineHeight,
+ letterSpacing = variableTypeScaleTokens.headlineSmallTracking,
+ )
+ val titleLargeEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.titleLargeFont,
+ fontSize = variableTypeScaleTokens.titleLargeSize,
+ lineHeight = variableTypeScaleTokens.titleLargeLineHeight,
+ letterSpacing = variableTypeScaleTokens.titleLargeTracking,
+ )
+ val titleMediumEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.titleMediumFont,
+ fontSize = variableTypeScaleTokens.titleMediumSize,
+ lineHeight = variableTypeScaleTokens.titleMediumLineHeight,
+ letterSpacing = variableTypeScaleTokens.titleMediumTracking,
+ )
+ val titleSmallEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.titleSmallFont,
+ fontSize = variableTypeScaleTokens.titleSmallSize,
+ lineHeight = variableTypeScaleTokens.titleSmallLineHeight,
+ letterSpacing = variableTypeScaleTokens.titleSmallTracking,
+ )
+ val bodyLargeEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.bodyLargeFont,
+ fontSize = variableTypeScaleTokens.bodyLargeSize,
+ lineHeight = variableTypeScaleTokens.bodyLargeLineHeight,
+ letterSpacing = variableTypeScaleTokens.bodyLargeTracking,
+ )
+ val bodyMediumEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.bodyMediumFont,
+ fontSize = variableTypeScaleTokens.bodyMediumSize,
+ lineHeight = variableTypeScaleTokens.bodyMediumLineHeight,
+ letterSpacing = variableTypeScaleTokens.bodyMediumTracking,
+ )
+ val bodySmallEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.bodySmallFont,
+ fontSize = variableTypeScaleTokens.bodySmallSize,
+ lineHeight = variableTypeScaleTokens.bodySmallLineHeight,
+ letterSpacing = variableTypeScaleTokens.bodySmallTracking,
+ )
+ val labelLargeEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.labelLargeFont,
+ fontSize = variableTypeScaleTokens.labelLargeSize,
+ lineHeight = variableTypeScaleTokens.labelLargeLineHeight,
+ letterSpacing = variableTypeScaleTokens.labelLargeTracking,
+ )
+ val labelMediumEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.labelMediumFont,
+ fontSize = variableTypeScaleTokens.labelMediumSize,
+ lineHeight = variableTypeScaleTokens.labelMediumLineHeight,
+ letterSpacing = variableTypeScaleTokens.labelMediumTracking,
+ )
+ val labelSmallEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.labelSmallFont,
+ fontSize = variableTypeScaleTokens.labelSmallSize,
+ lineHeight = variableTypeScaleTokens.labelSmallLineHeight,
+ letterSpacing = variableTypeScaleTokens.labelSmallTracking,
+ )
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt
new file mode 100644
index 0000000..52b9390
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.theme.typography
+
+import androidx.compose.ui.unit.sp
+
+internal class VariableFontTypeScaleEmphasizedTokens(typefaceTokens: TypefaceTokens) {
+ val bodyLargeFont = typefaceTokens.bodyLargeEmphasized
+ val bodyLargeLineHeight = 24.0.sp
+ val bodyLargeSize = 16.sp
+ val bodyLargeTracking = 0.0.sp
+ val bodyMediumFont = typefaceTokens.bodyMediumEmphasized
+ val bodyMediumLineHeight = 20.0.sp
+ val bodyMediumSize = 14.sp
+ val bodyMediumTracking = 0.0.sp
+ val bodySmallFont = typefaceTokens.bodySmallEmphasized
+ val bodySmallLineHeight = 16.0.sp
+ val bodySmallSize = 12.sp
+ val bodySmallTracking = 0.0.sp
+ val displayLargeFont = typefaceTokens.displayLargeEmphasized
+ val displayLargeLineHeight = 64.0.sp
+ val displayLargeSize = 57.sp
+ val displayLargeTracking = 0.0.sp
+ val displayMediumFont = typefaceTokens.displayMediumEmphasized
+ val displayMediumLineHeight = 52.0.sp
+ val displayMediumSize = 45.sp
+ val displayMediumTracking = 0.0.sp
+ val displaySmallFont = typefaceTokens.displaySmallEmphasized
+ val displaySmallLineHeight = 44.0.sp
+ val displaySmallSize = 36.sp
+ val displaySmallTracking = 0.0.sp
+ val headlineLargeFont = typefaceTokens.headlineLargeEmphasized
+ val headlineLargeLineHeight = 40.0.sp
+ val headlineLargeSize = 32.sp
+ val headlineLargeTracking = 0.0.sp
+ val headlineMediumFont = typefaceTokens.headlineMediumEmphasized
+ val headlineMediumLineHeight = 36.0.sp
+ val headlineMediumSize = 28.sp
+ val headlineMediumTracking = 0.0.sp
+ val headlineSmallFont = typefaceTokens.headlineSmallEmphasized
+ val headlineSmallLineHeight = 32.0.sp
+ val headlineSmallSize = 24.sp
+ val headlineSmallTracking = 0.0.sp
+ val labelLargeFont = typefaceTokens.labelLargeEmphasized
+ val labelLargeLineHeight = 20.0.sp
+ val labelLargeSize = 14.sp
+ val labelLargeTracking = 0.0.sp
+ val labelMediumFont = typefaceTokens.labelMediumEmphasized
+ val labelMediumLineHeight = 16.0.sp
+ val labelMediumSize = 12.sp
+ val labelMediumTracking = 0.0.sp
+ val labelSmallFont = typefaceTokens.labelSmallEmphasized
+ val labelSmallLineHeight = 16.0.sp
+ val labelSmallSize = 11.sp
+ val labelSmallTracking = 0.0.sp
+ val titleLargeFont = typefaceTokens.titleLargeEmphasized
+ val titleLargeLineHeight = 28.0.sp
+ val titleLargeSize = 22.sp
+ val titleLargeTracking = 0.0.sp
+ val titleMediumFont = typefaceTokens.titleMediumEmphasized
+ val titleMediumLineHeight = 24.0.sp
+ val titleMediumSize = 16.sp
+ val titleMediumTracking = 0.0.sp
+ val titleSmallFont = typefaceTokens.titleSmallEmphasized
+ val titleSmallLineHeight = 20.0.sp
+ val titleSmallSize = 14.sp
+ val titleSmallTracking = 0.0.sp
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt
index 48dee24..f1b273a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt
@@ -24,6 +24,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.UserAction
@@ -102,6 +103,8 @@
viewModel,
dialogFactory,
Modifier.element(Bouncer.Elements.Content)
+ // TODO(b/393516240): Use the same sysuiResTag() as views instead.
+ .testTag(Bouncer.Elements.Content.testTag)
.overscroll(verticalOverscrollEffect)
.sysuiResTag(Bouncer.TestTags.Root)
.fillMaxSize(),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
index 5e61af6..aa07370 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
@@ -55,7 +56,11 @@
@Composable
override fun ContentScope.Content(modifier: Modifier) {
- LockscreenScene(lockscreenContent = lockscreenContent, modifier = modifier)
+ LockscreenScene(
+ lockscreenContent = lockscreenContent,
+ // TODO(b/393516240): Use the same sysuiResTag() as views instead.
+ modifier = modifier.testTag(key.rootElementKey.testTag),
+ )
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
index b11c83c..4b3ebc2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
@@ -35,9 +35,9 @@
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon as MaterialIcon
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -73,11 +73,15 @@
import com.android.systemui.res.R
import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState
+import com.android.systemui.volume.ui.slider.AccessibilityParams
+import com.android.systemui.volume.ui.slider.Haptics
+import com.android.systemui.volume.ui.slider.Slider
import kotlin.math.round
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VolumeSlider(
state: SliderState,
@@ -102,17 +106,6 @@
return
}
- val value by valueState(state)
- val interactionSource = remember { MutableInteractionSource() }
- val hapticsViewModel: SliderHapticsViewModel? =
- setUpHapticsViewModel(
- value,
- state.valueRange,
- state.hapticFilter,
- interactionSource,
- hapticsViewModelFactory,
- )
-
Column(modifier = modifier.animateContentSize(), verticalArrangement = Arrangement.Top) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
@@ -134,60 +127,30 @@
)
button?.invoke()
}
+
Slider(
- value = value,
+ value = state.value,
valueRange = state.valueRange,
- onValueChange = { newValue ->
- hapticsViewModel?.addVelocityDataPoint(newValue)
- onValueChange(newValue)
- },
- onValueChangeFinished = {
- hapticsViewModel?.onValueChangeEnded()
- onValueChangeFinished?.invoke()
- },
- enabled = state.isEnabled,
+ onValueChanged = onValueChange,
+ onValueChangeFinished = { onValueChangeFinished?.invoke() },
+ isEnabled = state.isEnabled,
+ stepDistance = state.a11yStep,
+ accessibilityParams =
+ AccessibilityParams(
+ label = state.label,
+ disabledMessage = state.disabledMessage,
+ currentStateDescription = state.a11yStateDescription,
+ ),
+ haptics =
+ hapticsViewModelFactory?.let {
+ Haptics.Enabled(
+ hapticsViewModelFactory = it,
+ hapticFilter = state.hapticFilter,
+ orientation = Orientation.Horizontal,
+ )
+ } ?: Haptics.Disabled,
modifier =
- Modifier.height(40.dp)
- .padding(top = 4.dp, bottom = 12.dp)
- .sysuiResTag(state.label)
- .clearAndSetSemantics {
- if (state.isEnabled) {
- contentDescription = state.label
- state.a11yClickDescription?.let {
- customActions =
- listOf(
- CustomAccessibilityAction(it) {
- onIconTapped()
- true
- }
- )
- }
-
- state.a11yStateDescription?.let { stateDescription = it }
- progressBarRangeInfo =
- ProgressBarRangeInfo(state.value, state.valueRange)
- } else {
- disabled()
- contentDescription =
- state.disabledMessage?.let { "${state.label}, $it" } ?: state.label
- }
- setProgress { targetValue ->
- val targetDirection =
- when {
- targetValue > value -> 1
- targetValue < value -> -1
- else -> 0
- }
-
- val newValue =
- (value + targetDirection * state.a11yStep).coerceIn(
- state.valueRange.start,
- state.valueRange.endInclusive,
- )
- onValueChange(newValue)
- true
- }
- },
+ Modifier.height(40.dp).padding(top = 4.dp, bottom = 12.dp).sysuiResTag(state.label),
)
state.disabledMessage?.let { disabledMessage ->
AnimatedVisibility(visible = !state.isEnabled) {
@@ -348,7 +311,7 @@
}
@Composable
-fun setUpHapticsViewModel(
+private fun setUpHapticsViewModel(
value: Float,
valueRange: ClosedFloatingPointRange<Float>,
hapticFilter: SliderHapticFeedbackFilter,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 907b5bc..05958a2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -169,7 +169,7 @@
Modifier.maybeElevateInContent(layoutImpl, content, key, currentTransitionStates)
}
.then(ElementModifier(layoutImpl, currentTransitionStates, content, key))
- .testTag(key.testTag)
+ .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) }
}
/**
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 53d0ee1..404f1b2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -66,6 +66,8 @@
swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
swipeDetector: SwipeDetector = DefaultSwipeDetector,
@FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f,
+ // TODO(b/240432457) Remove this once test utils can access the internal STLForTesting().
+ implicitTestTags: Boolean = false,
builder: SceneTransitionLayoutScope<ContentScope>.() -> Unit,
) {
SceneTransitionLayoutForTesting(
@@ -74,6 +76,7 @@
swipeSourceDetector,
swipeDetector,
transitionInterceptionThreshold,
+ implicitTestTags = implicitTestTags,
onLayoutImpl = null,
builder = builder,
)
@@ -727,10 +730,8 @@
}
/**
- * An internal version of [SceneTransitionLayout] to be used for tests.
- *
- * Important: You should use this only in tests and if you need to access the underlying
- * [SceneTransitionLayoutImpl]. In other cases, you should use [SceneTransitionLayout].
+ * An internal version of [SceneTransitionLayout] to be used for tests, that provides access to the
+ * internal [SceneTransitionLayoutImpl] and implicitly tags all scenes and elements.
*/
@Composable
internal fun SceneTransitionLayoutForTesting(
@@ -743,6 +744,7 @@
sharedElementMap: MutableMap<ElementKey, Element> = remember { mutableMapOf() },
ancestors: List<Ancestor> = remember { emptyList() },
lookaheadScope: LookaheadScope? = null,
+ implicitTestTags: Boolean = true,
builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit,
) {
val density = LocalDensity.current
@@ -767,6 +769,7 @@
directionChangeSlop = directionChangeSlop,
defaultEffectFactory = defaultEffectFactory,
decayAnimationSpec = decayAnimationSpec,
+ implicitTestTags = implicitTestTags,
)
.also { onLayoutImpl?.invoke(it) }
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 53996d2..e3c4eb0 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -122,6 +122,9 @@
* This is used to enable transformations and shared elements across NestedSTLs.
*/
internal val ancestors: List<Ancestor> = emptyList(),
+
+ /** Whether elements and scene should be tagged using `Modifier.testTag`. */
+ internal val implicitTestTags: Boolean = false,
lookaheadScope: LookaheadScope? = null,
defaultEffectFactory: OverscrollFactory,
) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
index 9ca45fe..149a9e7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
@@ -173,7 +173,7 @@
.thenIf(layoutImpl.state.isElevationPossible(content = key, element = null)) {
Modifier.container(containerState)
}
- .testTag(key.testTag)
+ .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) }
) {
CompositionLocalProvider(LocalOverscrollFactory provides lastFactory) {
scope.content()
@@ -301,6 +301,7 @@
sharedElementMap = layoutImpl.elements,
ancestors = ancestors,
lookaheadScope = layoutImpl.lookaheadScope,
+ implicitTestTags = layoutImpl.implicitTestTags,
)
}
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 338fb9b..86cbfe4 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -227,7 +227,7 @@
to = SceneB,
transitionLayout = { state ->
coroutineScope = rememberCoroutineScope()
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneA) {
Box(Modifier.size(layoutSize)) {
// Transformed element
@@ -633,7 +633,7 @@
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneA) { Box(Modifier.element(TestElements.Foo).size(20.dp)) }
scene(SceneB) {}
}
@@ -674,7 +674,7 @@
CompositionLocalProvider(
LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
) {
- SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) {
scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
Spacer(Modifier.fillMaxSize())
}
@@ -734,7 +734,7 @@
CompositionLocalProvider(
LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
) {
- SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) {
scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
Spacer(
Modifier.overscroll(verticalOverscrollEffect)
@@ -834,7 +834,7 @@
CompositionLocalProvider(
LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
) {
- SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) {
scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
Spacer(Modifier.fillMaxSize())
}
@@ -893,7 +893,7 @@
CompositionLocalProvider(
LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
) {
- SceneTransitionLayout(
+ SceneTransitionLayoutForTesting(
state = state,
modifier = Modifier.size(layoutWidth, layoutHeight),
) {
@@ -970,7 +970,7 @@
rule.setContent {
touchSlop = LocalViewConfiguration.current.touchSlop
- SceneTransitionLayout(
+ SceneTransitionLayoutForTesting(
state = state,
modifier = Modifier.size(layoutWidth, layoutHeight),
) {
@@ -1057,7 +1057,7 @@
rule.setContent {
coroutineScope = rememberCoroutineScope()
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneA) {
Box(Modifier.size(layoutSize)) {
Box(
@@ -1374,7 +1374,7 @@
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) {
scene(SceneA) {
Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopStart)) }
}
@@ -1742,7 +1742,7 @@
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Foo(offset = 0.dp) }
scene(SceneB) { Foo(offset = 20.dp) }
scene(SceneC) { Foo(offset = 40.dp) }
@@ -1828,7 +1828,7 @@
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) }
// Define A after B so that Foo is placed in A during A <=> B.
@@ -1887,7 +1887,7 @@
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneA) { Foo() }
scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
index 04c762f..98ecb64 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
@@ -90,7 +90,7 @@
lateinit var coroutineScope: CoroutineScope
rule.setContent {
coroutineScope = rememberCoroutineScope()
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
overlay(OverlayA) { Foo() }
}
@@ -132,7 +132,7 @@
lateinit var coroutineScope: CoroutineScope
rule.setContent {
coroutineScope = rememberCoroutineScope()
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
overlay(OverlayA) { Foo() }
overlay(OverlayB) { Foo() }
@@ -230,7 +230,7 @@
lateinit var coroutineScope: CoroutineScope
rule.setContent {
coroutineScope = rememberCoroutineScope()
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) { MovableBar() } }
overlay(OverlayA) { MovableBar() }
overlay(OverlayB) { MovableBar() }
@@ -302,7 +302,7 @@
}
var alignment by mutableStateOf(Alignment.Center)
rule.setContent {
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
overlay(OverlayA, alignment = alignment) { Foo() }
}
@@ -761,7 +761,7 @@
val movableElementChildTag = "movableElementChildTag"
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneA) {
MovableElement(key, Modifier) {
content { Box(Modifier.testTag(movableElementChildTag).size(100.dp)) }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
index 2bf2358..366b11d 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
@@ -250,7 +250,7 @@
}
rule.setContent {
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) }
overlay(OverlayA) { Box(Modifier.fillMaxSize()) }
overlay(OverlayB) { Box(Modifier.fillMaxSize()) }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index d7f7a51..fa7661b6 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -97,7 +97,7 @@
MutableSceneTransitionLayoutStateForTests(SceneA, EmptyTestTransitions)
}
- SceneTransitionLayout(state = layoutState, modifier = Modifier.size(LayoutSize)) {
+ SceneTransitionLayoutForTesting(state = layoutState, modifier = Modifier.size(LayoutSize)) {
scene(SceneA, userActions = mapOf(Back to SceneB)) {
Box(Modifier.fillMaxSize()) {
SharedFoo(size = 50.dp, childOffset = 0.dp, Modifier.align(Alignment.TopEnd))
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 751b314..11abbbe 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -763,7 +763,7 @@
var touchSlop = 0f
rule.setContent {
touchSlop = LocalViewConfiguration.current.touchSlop
- SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) {
scene(SceneA, userActions = mapOf(Swipe.Start to SceneB, Swipe.End to SceneC)) {
Box(Modifier.fillMaxSize())
}
@@ -837,7 +837,7 @@
rule.setContent {
touchSlop = LocalViewConfiguration.current.touchSlop
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) {
scene(SceneA, userActions = mapOf(Swipe.Start to SceneB, Swipe.End to SceneC)) {
Box(Modifier.fillMaxSize())
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
index bb511bc..8b56892 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
@@ -40,7 +40,7 @@
import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateForTests
import com.android.compose.animation.scene.Scale
import com.android.compose.animation.scene.SceneKey
-import com.android.compose.animation.scene.SceneTransitionLayout
+import com.android.compose.animation.scene.SceneTransitionLayoutForTesting
import com.android.compose.animation.scene.SceneTransitions
import com.android.compose.animation.scene.TestScenes
import com.android.compose.animation.scene.testNestedTransition
@@ -114,7 +114,7 @@
@Composable
(states: List<MutableSceneTransitionLayoutState>) -> Unit =
{ states ->
- SceneTransitionLayout(states[0]) {
+ SceneTransitionLayoutForTesting(states[0]) {
scene(TestScenes.SceneA, content = { TestElement(elementVariant0A) })
scene(
TestScenes.SceneB,
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
index 6d47bab..e56d1be 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
@@ -30,5 +30,7 @@
content: @Composable ContentScope.() -> Unit,
) {
val state = rememberMutableSceneTransitionLayoutState(currentScene)
- SceneTransitionLayout(state, modifier) { scene(currentScene, content = content) }
+ SceneTransitionLayout(state, modifier, implicitTestTags = true) {
+ scene(currentScene, content = content)
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index f94a7ed..a362a37 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -137,7 +137,7 @@
},
changeState = changeState,
transitionLayout = { state ->
- SceneTransitionLayout(state, layoutModifier) {
+ SceneTransitionLayout(state, layoutModifier, implicitTestTags = true) {
scene(fromScene, content = fromSceneContent)
scene(toScene, content = toSceneContent)
}
@@ -163,7 +163,7 @@
)
},
transitionLayout = { state ->
- SceneTransitionLayout(state) {
+ SceneTransitionLayout(state, implicitTestTags = true) {
scene(fromScene) { fromSceneContent() }
overlay(overlay) { overlayContent() }
}
@@ -191,7 +191,7 @@
)
},
transitionLayout = { state ->
- SceneTransitionLayout(state) {
+ SceneTransitionLayout(state, implicitTestTags = true) {
scene(toScene) { toSceneContent() }
overlay(overlay) { overlayContent() }
}
@@ -223,7 +223,7 @@
)
},
transitionLayout = { state ->
- SceneTransitionLayout(state) {
+ SceneTransitionLayout(state, implicitTestTags = true) {
scene(currentScene) { currentSceneContent() }
overlay(from, alignment = fromAlignment) { fromContent() }
overlay(to, alignment = toAlignment) { toContent() }
@@ -273,7 +273,7 @@
}
}
- SceneTransitionLayout(state, layoutModifier) {
+ SceneTransitionLayout(state, layoutModifier, implicitTestTags = true) {
scene(fromScene, content = fromSceneContent)
scene(toScene, content = toSceneContent)
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index aad1276..654478a 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -28,6 +28,7 @@
import com.android.systemui.plugins.clocks.ClockPickerConfig
import com.android.systemui.plugins.clocks.ClockProvider
import com.android.systemui.plugins.clocks.ClockSettings
+import com.android.systemui.shared.clocks.FlexClockController.Companion.AXIS_PRESETS
import com.android.systemui.shared.clocks.FlexClockController.Companion.getDefaultAxes
private val TAG = DefaultClockProvider::class.simpleName
@@ -98,16 +99,16 @@
throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG")
}
- val fontAxes =
- if (!isClockReactiveVariantsEnabled) listOf()
- else getDefaultAxes(settings).merge(settings.axes)
return ClockPickerConfig(
settings.clockId ?: DEFAULT_CLOCK_ID,
resources.getString(R.string.clock_default_name),
resources.getString(R.string.clock_default_description),
resources.getDrawable(R.drawable.clock_default_thumbnail, null),
isReactiveToTone = true,
- axes = fontAxes,
+ axes =
+ if (!isClockReactiveVariantsEnabled) emptyList()
+ else getDefaultAxes(settings).merge(settings.axes),
+ axisPresets = if (!isClockReactiveVariantsEnabled) emptyList() else AXIS_PRESETS,
)
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
index ac1c5a8..1a1033b 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
@@ -132,7 +132,7 @@
listOf(
GSFAxes.WEIGHT.toClockAxis(
type = AxisType.Float,
- currentValue = 400f,
+ currentValue = 475f,
name = "Weight",
description = "Glyph Weight",
),
@@ -161,5 +161,59 @@
GSFAxes.ROUND.toClockAxisSetting(100f),
GSFAxes.SLANT.toClockAxisSetting(0f),
)
+
+ val AXIS_PRESETS =
+ listOf(
+ FONT_AXES.map { it.toSetting() },
+ LEGACY_FLEX_SETTINGS,
+ listOf( // Porcelain
+ GSFAxes.WEIGHT.toClockAxisSetting(500f),
+ GSFAxes.WIDTH.toClockAxisSetting(100f),
+ GSFAxes.ROUND.toClockAxisSetting(0f),
+ GSFAxes.SLANT.toClockAxisSetting(0f),
+ ),
+ listOf( // Midnight
+ GSFAxes.WEIGHT.toClockAxisSetting(300f),
+ GSFAxes.WIDTH.toClockAxisSetting(100f),
+ GSFAxes.ROUND.toClockAxisSetting(100f),
+ GSFAxes.SLANT.toClockAxisSetting(-10f),
+ ),
+ listOf( // Sterling
+ GSFAxes.WEIGHT.toClockAxisSetting(1000f),
+ GSFAxes.WIDTH.toClockAxisSetting(100f),
+ GSFAxes.ROUND.toClockAxisSetting(0f),
+ GSFAxes.SLANT.toClockAxisSetting(0f),
+ ),
+ listOf( // Smoky Green
+ GSFAxes.WEIGHT.toClockAxisSetting(150f),
+ GSFAxes.WIDTH.toClockAxisSetting(50f),
+ GSFAxes.ROUND.toClockAxisSetting(0f),
+ GSFAxes.SLANT.toClockAxisSetting(0f),
+ ),
+ listOf( // Iris
+ GSFAxes.WEIGHT.toClockAxisSetting(500f),
+ GSFAxes.WIDTH.toClockAxisSetting(100f),
+ GSFAxes.ROUND.toClockAxisSetting(100f),
+ GSFAxes.SLANT.toClockAxisSetting(0f),
+ ),
+ listOf( // Margarita
+ GSFAxes.WEIGHT.toClockAxisSetting(300f),
+ GSFAxes.WIDTH.toClockAxisSetting(30f),
+ GSFAxes.ROUND.toClockAxisSetting(100f),
+ GSFAxes.SLANT.toClockAxisSetting(-10f),
+ ),
+ listOf( // Raspberry
+ GSFAxes.WEIGHT.toClockAxisSetting(700f),
+ GSFAxes.WIDTH.toClockAxisSetting(140f),
+ GSFAxes.ROUND.toClockAxisSetting(100f),
+ GSFAxes.SLANT.toClockAxisSetting(-7f),
+ ),
+ listOf( // Ultra Blue
+ GSFAxes.WEIGHT.toClockAxisSetting(850f),
+ GSFAxes.WIDTH.toClockAxisSetting(130f),
+ GSFAxes.ROUND.toClockAxisSetting(0f),
+ GSFAxes.SLANT.toClockAxisSetting(0f),
+ ),
+ )
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
index 781e416..ede29d8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
@@ -26,6 +26,9 @@
import com.android.systemui.common.shared.model.PackageInstallSession
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.backgroundScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
@@ -173,6 +176,58 @@
}
@Test
+ fun onCreateUpdatedSession_ignoreNullPackageNameSessions() =
+ kosmos.runTest {
+ val nullPackageSession =
+ SessionInfo().apply {
+ sessionId = 1
+ appPackageName = null
+ appIcon = icon1
+ }
+
+ val wellFormedSession =
+ SessionInfo().apply {
+ sessionId = 2
+ appPackageName = "pkg_name"
+ appIcon = icon2
+ }
+
+ defaultSessions = listOf(wellFormedSession)
+
+ whenever(packageInstaller.allSessions).thenReturn(defaultSessions)
+ whenever(packageInstaller.getSessionInfo(1)).thenReturn(nullPackageSession)
+ whenever(packageInstaller.getSessionInfo(2)).thenReturn(wellFormedSession)
+
+ val packageInstallerMonitor =
+ PackageInstallerMonitor(
+ handler,
+ backgroundScope,
+ logcatLogBuffer("PackageInstallerRepositoryImplTest"),
+ packageInstaller,
+ )
+
+ val sessions by collectLastValue(packageInstallerMonitor.installSessionsForPrimaryUser)
+
+ // Verify flow updated with the new session
+ assertThat(sessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions)
+
+ val callback =
+ withArgCaptor<PackageInstaller.SessionCallback> {
+ verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+ }
+
+ // New session added
+ callback.onCreated(nullPackageSession.sessionId)
+
+ // Verify flow updated with the new session
+ assertThat(sessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions)
+ }
+
+ @Test
fun installSessions_newSessionsAreAdded() =
testScope.runTest {
val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
index e53155d..ed73d89 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
@@ -21,6 +21,8 @@
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.communalMediaRepository
+import com.android.systemui.communal.data.repository.communalSmartspaceRepository
import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository
import com.android.systemui.communal.domain.interactor.communalInteractor
@@ -28,12 +30,12 @@
import com.android.systemui.communal.domain.interactor.setCommunalEnabled
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -42,46 +44,64 @@
@EnableFlags(FLAG_COMMUNAL_HUB)
@RunWith(AndroidJUnit4::class)
class CommunalOngoingContentStartableTest : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
- private val mediaRepository = kosmos.fakeCommunalMediaRepository
- private val smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository
+ private var showUmoOnHub = true
- private lateinit var underTest: CommunalOngoingContentStartable
+ private val Kosmos.underTest by
+ Kosmos.Fixture {
+ CommunalOngoingContentStartable(
+ bgScope = applicationCoroutineScope,
+ communalInteractor = communalInteractor,
+ communalMediaRepository = communalMediaRepository,
+ communalSettingsInteractor = communalSettingsInteractor,
+ communalSmartspaceRepository = communalSmartspaceRepository,
+ showUmoOnHub = showUmoOnHub,
+ )
+ }
@Before
fun setUp() {
kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
- underTest =
- CommunalOngoingContentStartable(
- bgScope = kosmos.applicationCoroutineScope,
- communalInteractor = kosmos.communalInteractor,
- communalMediaRepository = mediaRepository,
- communalSettingsInteractor = kosmos.communalSettingsInteractor,
- communalSmartspaceRepository = smartspaceRepository,
- )
}
@Test
- fun testListenForOngoingContentWhenCommunalIsEnabled() =
- testScope.runTest {
+ fun testListenForOngoingContent() =
+ kosmos.runTest {
underTest.start()
- runCurrent()
- assertThat(mediaRepository.isListening()).isFalse()
- assertThat(smartspaceRepository.isListening()).isFalse()
+ assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
kosmos.setCommunalEnabled(true)
- runCurrent()
- assertThat(mediaRepository.isListening()).isTrue()
- assertThat(smartspaceRepository.isListening()).isTrue()
+ assertThat(fakeCommunalMediaRepository.isListening()).isTrue()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue()
kosmos.setCommunalEnabled(false)
- runCurrent()
- assertThat(mediaRepository.isListening()).isFalse()
- assertThat(smartspaceRepository.isListening()).isFalse()
+ assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
+ }
+
+ @Test
+ fun testListenForOngoingContent_showUmoFalse() =
+ kosmos.runTest {
+ showUmoOnHub = false
+ underTest.start()
+
+ assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
+
+ kosmos.setCommunalEnabled(true)
+
+ // Media listening does not start when UMO is disabled.
+ assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue()
+
+ kosmos.setCommunalEnabled(false)
+
+ assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt
index 943ada9..4e14fec 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt
@@ -18,9 +18,6 @@
import android.animation.Animator
import android.animation.ObjectAnimator
-import android.icu.text.MeasureFormat
-import android.icu.util.Measure
-import android.icu.util.MeasureUnit
import android.testing.TestableLooper
import android.view.View
import android.widget.SeekBar
@@ -33,7 +30,6 @@
import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
import com.android.systemui.res.R
import com.google.common.truth.Truth.assertThat
-import java.util.Locale
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -65,11 +61,11 @@
fun setUp() {
context.orCreateTestableResources.addOverride(
R.dimen.qs_media_enabled_seekbar_height,
- enabledHeight,
+ enabledHeight
)
context.orCreateTestableResources.addOverride(
R.dimen.qs_media_disabled_seekbar_height,
- disabledHeight,
+ disabledHeight
)
seekBarView = SeekBar(context)
@@ -114,31 +110,14 @@
@Test
fun seekBarProgress() {
- val elapsedTime = 3000
- val duration = (1.5 * 60 * 60 * 1000).toInt()
// WHEN part of the track has been played
- val data = SeekBarViewModel.Progress(true, true, true, false, elapsedTime, duration, true)
+ val data = SeekBarViewModel.Progress(true, true, true, false, 3000, 120000, true)
observer.onChanged(data)
// THEN seek bar shows the progress
- assertThat(seekBarView.progress).isEqualTo(elapsedTime)
- assertThat(seekBarView.max).isEqualTo(duration)
+ assertThat(seekBarView.progress).isEqualTo(3000)
+ assertThat(seekBarView.max).isEqualTo(120000)
- val expectedProgress =
- MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
- .formatMeasures(Measure(3, MeasureUnit.SECOND))
- val expectedDuration =
- MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
- .formatMeasures(
- Measure(1, MeasureUnit.HOUR),
- Measure(30, MeasureUnit.MINUTE),
- Measure(0, MeasureUnit.SECOND),
- )
- val desc =
- context.getString(
- R.string.controls_media_seekbar_description,
- expectedProgress,
- expectedDuration,
- )
+ val desc = context.getString(R.string.controls_media_seekbar_description, "00:03", "02:00")
assertThat(seekBarView.contentDescription).isEqualTo(desc)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
index 917f356..80ce43d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
@@ -65,8 +65,7 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val sceneInteractor = kosmos.sceneInteractor
-
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val underTest by lazy { kosmos.notificationsShadeOverlayContentViewModel }
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
index 1899b7d..0e5e333 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
@@ -423,5 +423,15 @@
@Override
public void destroy() {}
+
+ @Override
+ public boolean isDestroyed() {
+ return false;
+ }
+
+ @Override
+ public int getCurrentTileUser() {
+ return 0;
+ }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/TilesAvailabilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/TilesAvailabilityInteractorTest.kt
index 5a58597..67fb100 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/TilesAvailabilityInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/TilesAvailabilityInteractorTest.kt
@@ -56,166 +56,178 @@
private val createdTiles = mutableListOf<FakeQSTile>()
- private val kosmos = testKosmos().apply {
- tileAvailabilityInteractorsMap = buildMap {
- put(AIRPLANE_MODE_TILE_SPEC, QSTileAvailabilityInteractor.AlwaysAvailableInteractor)
- put(WORK_MODE_TILE_SPEC, FakeTileAvailabilityInteractor(
- mapOf(
- fakeUserRepository.getSelectedUserInfo().id to flowOf(true),
- ).withDefault { flowOf(false) }
- ))
- put(HOTSPOT_TILE_SPEC, FakeTileAvailabilityInteractor(
- emptyMap<Int, Flow<Boolean>>().withDefault { flowOf(false) }
- ))
- }
+ private val kosmos =
+ testKosmos().apply {
+ tileAvailabilityInteractorsMap = buildMap {
+ put(AIRPLANE_MODE_TILE_SPEC, QSTileAvailabilityInteractor.AlwaysAvailableInteractor)
+ put(
+ WORK_MODE_TILE_SPEC,
+ FakeTileAvailabilityInteractor(
+ mapOf(fakeUserRepository.getSelectedUserInfo().id to flowOf(true))
+ .withDefault { flowOf(false) }
+ ),
+ )
+ put(
+ HOTSPOT_TILE_SPEC,
+ FakeTileAvailabilityInteractor(
+ emptyMap<Int, Flow<Boolean>>().withDefault { flowOf(false) }
+ ),
+ )
+ }
- qsTileFactory = constantFactory(
- tilesForCreator(
+ qsTileFactory =
+ constantFactory(
+ tilesForCreator(
userRepository.getSelectedUserInfo().id,
mapOf(
- AIRPLANE_MODE_TILE_SPEC to false,
- WORK_MODE_TILE_SPEC to false,
- HOTSPOT_TILE_SPEC to true,
- INTERNET_TILE_SPEC to true,
- FLASHLIGHT_TILE_SPEC to false,
- )
+ AIRPLANE_MODE_TILE_SPEC to false,
+ WORK_MODE_TILE_SPEC to false,
+ HOTSPOT_TILE_SPEC to true,
+ INTERNET_TILE_SPEC to true,
+ FLASHLIGHT_TILE_SPEC to false,
+ ),
+ )
)
- )
- }
+ }
private val underTest by lazy { kosmos.tilesAvailabilityInteractor }
@Test
@DisableFlags(FLAG_QS_NEW_TILES)
- fun flagOff_usesAvailabilityFromFactoryTiles() = with(kosmos) {
- testScope.runTest {
- val unavailableTiles = underTest.getUnavailableTiles(
- setOf(
- AIRPLANE_MODE_TILE_SPEC,
- WORK_MODE_TILE_SPEC,
- HOTSPOT_TILE_SPEC,
- INTERNET_TILE_SPEC,
- FLASHLIGHT_TILE_SPEC,
- ).map(TileSpec::create)
- )
- assertThat(unavailableTiles).isEqualTo(setOf(
- AIRPLANE_MODE_TILE_SPEC,
- WORK_MODE_TILE_SPEC,
- FLASHLIGHT_TILE_SPEC,
- ).mapTo(mutableSetOf(), TileSpec::create))
- }
- }
-
- @Test
- fun tileCannotBeCreated_isUnavailable() = with(kosmos) {
- testScope.runTest {
- val badSpec = TileSpec.create("unknown")
- val unavailableTiles = underTest.getUnavailableTiles(
- setOf(
- badSpec
+ fun flagOff_usesAvailabilityFromFactoryTiles() =
+ with(kosmos) {
+ testScope.runTest {
+ val unavailableTiles =
+ underTest.getUnavailableTiles(
+ setOf(
+ AIRPLANE_MODE_TILE_SPEC,
+ WORK_MODE_TILE_SPEC,
+ HOTSPOT_TILE_SPEC,
+ INTERNET_TILE_SPEC,
+ FLASHLIGHT_TILE_SPEC,
+ )
+ .map(TileSpec::create)
)
- )
- assertThat(unavailableTiles).contains(badSpec)
+ assertThat(unavailableTiles)
+ .isEqualTo(
+ setOf(AIRPLANE_MODE_TILE_SPEC, WORK_MODE_TILE_SPEC, FLASHLIGHT_TILE_SPEC)
+ .mapTo(mutableSetOf(), TileSpec::create)
+ )
+ }
}
- }
+
+ @Test
+ fun tileCannotBeCreated_isUnavailable() =
+ with(kosmos) {
+ testScope.runTest {
+ val badSpec = TileSpec.create("unknown")
+ val unavailableTiles = underTest.getUnavailableTiles(setOf(badSpec))
+ assertThat(unavailableTiles).contains(badSpec)
+ }
+ }
@Test
@EnableFlags(FLAG_QS_NEW_TILES)
- fun flagOn_defaultsToInteractorTiles_usesFactoryForOthers() = with(kosmos) {
- testScope.runTest {
- val unavailableTiles = underTest.getUnavailableTiles(
+ fun flagOn_defaultsToInteractorTiles_usesFactoryForOthers() =
+ with(kosmos) {
+ testScope.runTest {
+ val unavailableTiles =
+ underTest.getUnavailableTiles(
+ setOf(
+ AIRPLANE_MODE_TILE_SPEC,
+ WORK_MODE_TILE_SPEC,
+ HOTSPOT_TILE_SPEC,
+ INTERNET_TILE_SPEC,
+ FLASHLIGHT_TILE_SPEC,
+ )
+ .map(TileSpec::create)
+ )
+ assertThat(unavailableTiles)
+ .isEqualTo(
+ setOf(HOTSPOT_TILE_SPEC, FLASHLIGHT_TILE_SPEC)
+ .mapTo(mutableSetOf(), TileSpec::create)
+ )
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_QS_NEW_TILES)
+ fun flagOn_defaultsToInteractorTiles_usesFactoryForOthers_userChange() =
+ with(kosmos) {
+ testScope.runTest {
+ fakeUserRepository.asMainUser()
+ val unavailableTiles =
+ underTest.getUnavailableTiles(
+ setOf(
+ AIRPLANE_MODE_TILE_SPEC,
+ WORK_MODE_TILE_SPEC,
+ HOTSPOT_TILE_SPEC,
+ INTERNET_TILE_SPEC,
+ FLASHLIGHT_TILE_SPEC,
+ )
+ .map(TileSpec::create)
+ )
+ assertThat(unavailableTiles)
+ .isEqualTo(
+ setOf(WORK_MODE_TILE_SPEC, HOTSPOT_TILE_SPEC, FLASHLIGHT_TILE_SPEC)
+ .mapTo(mutableSetOf(), TileSpec::create)
+ )
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_QS_NEW_TILES)
+ fun flagOn_onlyNeededTilesAreCreated_andThenDestroyed() =
+ with(kosmos) {
+ testScope.runTest {
+ underTest.getUnavailableTiles(
setOf(
AIRPLANE_MODE_TILE_SPEC,
WORK_MODE_TILE_SPEC,
HOTSPOT_TILE_SPEC,
INTERNET_TILE_SPEC,
FLASHLIGHT_TILE_SPEC,
- ).map(TileSpec::create)
- )
- assertThat(unavailableTiles).isEqualTo(setOf(
- HOTSPOT_TILE_SPEC,
- FLASHLIGHT_TILE_SPEC,
- ).mapTo(mutableSetOf(), TileSpec::create))
- }
- }
-
- @Test
- @EnableFlags(FLAG_QS_NEW_TILES)
- fun flagOn_defaultsToInteractorTiles_usesFactoryForOthers_userChange() = with(kosmos) {
- testScope.runTest {
- fakeUserRepository.asMainUser()
- val unavailableTiles = underTest.getUnavailableTiles(
- setOf(
- AIRPLANE_MODE_TILE_SPEC,
- WORK_MODE_TILE_SPEC,
- HOTSPOT_TILE_SPEC,
- INTERNET_TILE_SPEC,
- FLASHLIGHT_TILE_SPEC,
- ).map(TileSpec::create)
- )
- assertThat(unavailableTiles).isEqualTo(setOf(
- WORK_MODE_TILE_SPEC,
- HOTSPOT_TILE_SPEC,
- FLASHLIGHT_TILE_SPEC,
- ).mapTo(mutableSetOf(), TileSpec::create))
- }
- }
-
- @Test
- @EnableFlags(FLAG_QS_NEW_TILES)
- fun flagOn_onlyNeededTilesAreCreated_andThenDestroyed() = with(kosmos) {
- testScope.runTest {
- underTest.getUnavailableTiles(
- setOf(
- AIRPLANE_MODE_TILE_SPEC,
- WORK_MODE_TILE_SPEC,
- HOTSPOT_TILE_SPEC,
- INTERNET_TILE_SPEC,
- FLASHLIGHT_TILE_SPEC,
- ).map(TileSpec::create)
- )
- assertThat(createdTiles.map { it.tileSpec })
+ )
+ .map(TileSpec::create)
+ )
+ assertThat(createdTiles.map { it.tileSpec })
.containsExactly(INTERNET_TILE_SPEC, FLASHLIGHT_TILE_SPEC)
- assertThat(createdTiles.all { it.destroyed }).isTrue()
+ assertThat(createdTiles.all { it.isDestroyed }).isTrue()
+ }
}
- }
@Test
@DisableFlags(FLAG_QS_NEW_TILES)
- fun flagOn_TilesAreCreatedAndThenDestroyed() = with(kosmos) {
- testScope.runTest {
- val allTiles = setOf(
- AIRPLANE_MODE_TILE_SPEC,
- WORK_MODE_TILE_SPEC,
- HOTSPOT_TILE_SPEC,
- INTERNET_TILE_SPEC,
- FLASHLIGHT_TILE_SPEC,
- )
- underTest.getUnavailableTiles(allTiles.map(TileSpec::create))
- assertThat(createdTiles.map { it.tileSpec })
- .containsExactlyElementsIn(allTiles)
- assertThat(createdTiles.all { it.destroyed }).isTrue()
+ fun flagOn_TilesAreCreatedAndThenDestroyed() =
+ with(kosmos) {
+ testScope.runTest {
+ val allTiles =
+ setOf(
+ AIRPLANE_MODE_TILE_SPEC,
+ WORK_MODE_TILE_SPEC,
+ HOTSPOT_TILE_SPEC,
+ INTERNET_TILE_SPEC,
+ FLASHLIGHT_TILE_SPEC,
+ )
+ underTest.getUnavailableTiles(allTiles.map(TileSpec::create))
+ assertThat(createdTiles.map { it.tileSpec }).containsExactlyElementsIn(allTiles)
+ assertThat(createdTiles.all { it.isDestroyed }).isTrue()
+ }
}
- }
-
private fun constantFactory(creatorTiles: Set<FakeQSTile>): QSFactory {
return FakeQSFactory { spec ->
- creatorTiles.firstOrNull { it.tileSpec == spec }?.also {
- createdTiles.add(it)
- }
+ creatorTiles.firstOrNull { it.tileSpec == spec }?.also { createdTiles.add(it) }
}
}
companion object {
private fun tilesForCreator(
- user: Int,
- specAvailabilities: Map<String, Boolean>
+ user: Int,
+ specAvailabilities: Map<String, Boolean>,
): Set<FakeQSTile> {
return specAvailabilities.mapTo(mutableSetOf()) {
- FakeQSTile(user, it.value).apply {
- tileSpec = it.key
- }
+ FakeQSTile(user, it.value).apply { tileSpec = it.key }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index 9b50f1b..c308976 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -17,10 +17,10 @@
package com.android.systemui.qs.pipeline.domain.interactor
import android.content.ComponentName
-import android.content.Context
import android.content.Intent
import android.content.pm.UserInfo
import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
import android.service.quicksettings.Tile
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -28,653 +28,702 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dump.nano.SystemUIProtoDump
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.qs.QSTile.BooleanState
import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.FakeQSFactory
import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.external.CustomTile
-import com.android.systemui.qs.external.CustomTileStatePersister
import com.android.systemui.qs.external.TileLifecycleManager
import com.android.systemui.qs.external.TileServiceKey
-import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository
-import com.android.systemui.qs.pipeline.data.repository.FakeCustomTileAddedRepository
-import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository
-import com.android.systemui.qs.pipeline.data.repository.FakeTileSpecRepository
-import com.android.systemui.qs.pipeline.data.repository.MinimumTilesFixedRepository
-import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
+import com.android.systemui.qs.external.customTileStatePersister
+import com.android.systemui.qs.external.tileLifecycleManagerFactory
+import com.android.systemui.qs.pipeline.data.repository.customTileAddedRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository
+import com.android.systemui.qs.pipeline.data.repository.tileSpecRepository
import com.android.systemui.qs.pipeline.domain.model.TileModel
-import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
-import com.android.systemui.qs.tiles.di.NewQSTileFactory
+import com.android.systemui.qs.pipeline.shared.logging.qsLogger
+import com.android.systemui.qs.qsTileFactory
+import com.android.systemui.qs.tiles.di.newQSTileFactory
import com.android.systemui.qs.toProto
-import com.android.systemui.retail.data.repository.FakeRetailModeRepository
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.settings.fakeUserTracker
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.data.repository.userRepository
import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import com.google.protobuf.nano.MessageNano
-import kotlinx.coroutines.test.StandardTestDispatcher
+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.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyString
-import org.mockito.Mock
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableFlags(FLAG_QS_NEW_TILES)
class CurrentTilesInteractorImplTest : SysuiTestCase() {
- private val tileSpecRepository: TileSpecRepository = FakeTileSpecRepository()
- private val userRepository = FakeUserRepository()
- private val installedTilesPackageRepository = FakeInstalledTilesComponentRepository()
- private val tileFactory = FakeQSFactory(::tileCreator)
- private val customTileAddedRepository: CustomTileAddedRepository =
- FakeCustomTileAddedRepository()
- private val pipelineFlags = QSPipelineFlagsRepository()
- private val tileLifecycleManagerFactory = TLMFactory()
- private val minimumTilesRepository = MinimumTilesFixedRepository()
- private val retailModeRepository = FakeRetailModeRepository()
-
- @Mock private lateinit var customTileStatePersister: CustomTileStatePersister
-
- @Mock private lateinit var userTracker: UserTracker
-
- @Mock private lateinit var logger: QSPipelineLogger
-
- @Mock private lateinit var newQSTileFactory: NewQSTileFactory
-
- private val testDispatcher = StandardTestDispatcher()
- private val testScope = TestScope(testDispatcher)
+ private val kosmos =
+ testKosmos().apply {
+ qsTileFactory = FakeQSFactory { tileCreator(it) }
+ fakeUserTracker.set(listOf(USER_INFO_0), 0)
+ fakeUserRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1))
+ tileLifecycleManagerFactory = TLMFactory()
+ newQSTileFactory = mock()
+ qsLogger = mock()
+ }
private val unavailableTiles = mutableSetOf("e")
- private lateinit var underTest: CurrentTilesInteractorImpl
-
- @Before
- fun setup() {
- MockitoAnnotations.initMocks(this)
-
- mSetFlagsRule.enableFlags(FLAG_QS_NEW_TILES)
-
- userRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1))
-
- setUserTracker(0)
-
- underTest =
- CurrentTilesInteractorImpl(
- tileSpecRepository = tileSpecRepository,
- installedTilesComponentRepository = installedTilesPackageRepository,
- userRepository = userRepository,
- minimumTilesRepository = minimumTilesRepository,
- retailModeRepository = retailModeRepository,
- customTileStatePersister = customTileStatePersister,
- tileFactory = tileFactory,
- newQSTileFactory = { newQSTileFactory },
- customTileAddedRepository = customTileAddedRepository,
- tileLifecycleManagerFactory = tileLifecycleManagerFactory,
- userTracker = userTracker,
- mainDispatcher = testDispatcher,
- backgroundDispatcher = testDispatcher,
- scope = testScope.backgroundScope,
- logger = logger,
- featureFlags = pipelineFlags,
- )
- }
+ private val underTest = kosmos.currentTilesInteractor
@Test
fun initialState() =
- testScope.runTest(USER_INFO_0) {
- assertThat(underTest.currentTiles.value).isEmpty()
- assertThat(underTest.currentQSTiles).isEmpty()
- assertThat(underTest.currentTilesSpecs).isEmpty()
- assertThat(underTest.userId.value).isEqualTo(0)
- assertThat(underTest.userContext.value.userId).isEqualTo(0)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ assertThat(underTest.currentTiles.value).isEmpty()
+ assertThat(underTest.currentQSTiles).isEmpty()
+ assertThat(underTest.currentTilesSpecs).isEmpty()
+ assertThat(underTest.userId.value).isEqualTo(0)
+ assertThat(underTest.userContext.value.userId).isEqualTo(0)
+ }
}
@Test
fun correctTiles() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs =
- listOf(
- TileSpec.create("a"),
- TileSpec.create("e"),
- CUSTOM_TILE_SPEC,
- TileSpec.create("d"),
- TileSpec.create("non_existent"),
- )
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ val specs =
+ listOf(
+ TileSpec.create("a"),
+ TileSpec.create("e"),
+ CUSTOM_TILE_SPEC,
+ TileSpec.create("d"),
+ TileSpec.create("non_existent"),
+ )
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- // check each tile
+ // check each tile
- // Tile a
- val tile0 = tiles!![0]
- assertThat(tile0.spec).isEqualTo(specs[0])
- assertThat(tile0.tile.tileSpec).isEqualTo(specs[0].spec)
- assertThat(tile0.tile).isInstanceOf(FakeQSTile::class.java)
- assertThat(tile0.tile.isAvailable).isTrue()
+ // Tile a
+ val tile0 = tiles!![0]
+ assertThat(tile0.spec).isEqualTo(specs[0])
+ assertThat(tile0.tile.tileSpec).isEqualTo(specs[0].spec)
+ assertThat(tile0.tile).isInstanceOf(FakeQSTile::class.java)
+ assertThat(tile0.tile.isAvailable).isTrue()
- // Tile e is not available and is not in the list
+ // Tile e is not available and is not in the list
- // Custom Tile
- val tile1 = tiles!![1]
- assertThat(tile1.spec).isEqualTo(specs[2])
- assertThat(tile1.tile.tileSpec).isEqualTo(specs[2].spec)
- assertThat(tile1.tile).isInstanceOf(CustomTile::class.java)
- assertThat(tile1.tile.isAvailable).isTrue()
+ // Custom Tile
+ val tile1 = tiles!![1]
+ assertThat(tile1.spec).isEqualTo(specs[2])
+ assertThat(tile1.tile.tileSpec).isEqualTo(specs[2].spec)
+ assertThat(tile1.tile).isInstanceOf(CustomTile::class.java)
+ assertThat(tile1.tile.isAvailable).isTrue()
- // Tile d
- val tile2 = tiles!![2]
- assertThat(tile2.spec).isEqualTo(specs[3])
- assertThat(tile2.tile.tileSpec).isEqualTo(specs[3].spec)
- assertThat(tile2.tile).isInstanceOf(FakeQSTile::class.java)
- assertThat(tile2.tile.isAvailable).isTrue()
+ // Tile d
+ val tile2 = tiles!![2]
+ assertThat(tile2.spec).isEqualTo(specs[3])
+ assertThat(tile2.tile.tileSpec).isEqualTo(specs[3].spec)
+ assertThat(tile2.tile).isInstanceOf(FakeQSTile::class.java)
+ assertThat(tile2.tile.isAvailable).isTrue()
- // Tile non-existent shouldn't be created. Therefore, only 3 tiles total
- assertThat(tiles?.size).isEqualTo(3)
+ // Tile non-existent shouldn't be created. Therefore, only 3 tiles total
+ assertThat(tiles?.size).isEqualTo(3)
+ }
}
@Test
fun logTileCreated() =
- testScope.runTest(USER_INFO_0) {
- val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- runCurrent()
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ runCurrent()
- specs.forEach { verify(logger).logTileCreated(it) }
+ specs.forEach { verify(qsLogger).logTileCreated(it) }
+ }
}
@Test
fun logTileNotFoundInFactory() =
- testScope.runTest(USER_INFO_0) {
- val specs = listOf(TileSpec.create("non_existing"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- runCurrent()
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val specs = listOf(TileSpec.create("non_existing"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ runCurrent()
- verify(logger, never()).logTileCreated(any())
- verify(logger).logTileNotFoundInFactory(specs[0])
+ verify(qsLogger, never()).logTileCreated(any())
+ verify(qsLogger).logTileNotFoundInFactory(specs[0])
+ }
}
@Test
fun tileNotAvailableDestroyed_logged() =
- testScope.runTest(USER_INFO_0) {
- val specs = listOf(TileSpec.create("e"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- runCurrent()
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val specs = listOf(TileSpec.create("e"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ runCurrent()
- verify(logger, never()).logTileCreated(any())
- verify(logger)
- .logTileDestroyed(
- specs[0],
- QSPipelineLogger.TileDestroyedReason.NEW_TILE_NOT_AVAILABLE,
- )
+ verify(qsLogger, never()).logTileCreated(any())
+ verify(qsLogger)
+ .logTileDestroyed(
+ specs[0],
+ QSPipelineLogger.TileDestroyedReason.NEW_TILE_NOT_AVAILABLE,
+ )
+ }
}
@Test
fun someTilesNotValid_repositorySetToDefinitiveList() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
- val specs = listOf(TileSpec.create("a"), TileSpec.create("e"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ val specs = listOf(TileSpec.create("a"), TileSpec.create("e"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- assertThat(tiles).isEqualTo(listOf(TileSpec.create("a")))
+ assertThat(tiles).isEqualTo(listOf(TileSpec.create("a")))
+ }
}
@Test
fun deduplicatedTiles() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"), TileSpec.create("a"))
+ val specs = listOf(TileSpec.create("a"), TileSpec.create("a"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- assertThat(tiles?.size).isEqualTo(1)
- assertThat(tiles!![0].spec).isEqualTo(specs[0])
+ assertThat(tiles?.size).isEqualTo(1)
+ assertThat(tiles!![0].spec).isEqualTo(specs[0])
+ }
}
@Test
fun tilesChange_platformTileNotRecreated() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"))
+ val specs = listOf(TileSpec.create("a"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- val originalTileA = tiles!![0].tile
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ val originalTileA = tiles!![0].tile
- tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b"))
+ tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b"))
- assertThat(tiles?.size).isEqualTo(2)
- assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA)
+ assertThat(tiles?.size).isEqualTo(2)
+ assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA)
+ }
}
@Test
fun tileRemovedIsDestroyed() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"), TileSpec.create("c"))
+ val specs = listOf(TileSpec.create("a"), TileSpec.create("c"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- val originalTileC = tiles!![1].tile
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ val originalTileC = tiles!![1].tile
- tileSpecRepository.removeTiles(USER_INFO_0.id, listOf(TileSpec.create("c")))
+ tileSpecRepository.removeTiles(USER_INFO_0.id, listOf(TileSpec.create("c")))
- assertThat(tiles?.size).isEqualTo(1)
- assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("a"))
+ assertThat(tiles?.size).isEqualTo(1)
+ assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("a"))
- assertThat((originalTileC as FakeQSTile).destroyed).isTrue()
- verify(logger)
- .logTileDestroyed(
- TileSpec.create("c"),
- QSPipelineLogger.TileDestroyedReason.TILE_REMOVED,
- )
+ assertThat(originalTileC.isDestroyed).isTrue()
+ verify(qsLogger)
+ .logTileDestroyed(
+ TileSpec.create("c"),
+ QSPipelineLogger.TileDestroyedReason.TILE_REMOVED,
+ )
+ }
}
@Test
fun tileBecomesNotAvailable_destroyed() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
- val repoTiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
+ val repoTiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
- val specs = listOf(TileSpec.create("a"))
+ val specs = listOf(TileSpec.create("a"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- val originalTileA = tiles!![0].tile
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ val originalTileA = tiles!![0].tile
- // Tile becomes unavailable
- (originalTileA as FakeQSTile).available = false
- unavailableTiles.add("a")
- // and there is some change in the specs
- tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b"))
- runCurrent()
+ // Tile becomes unavailable
+ (originalTileA as FakeQSTile).available = false
+ unavailableTiles.add("a")
+ // and there is some change in the specs
+ tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b"))
+ runCurrent()
- assertThat(originalTileA.destroyed).isTrue()
- verify(logger)
- .logTileDestroyed(
- TileSpec.create("a"),
- QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE,
- )
+ assertThat(originalTileA.isDestroyed).isTrue()
+ verify(qsLogger)
+ .logTileDestroyed(
+ TileSpec.create("a"),
+ QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE,
+ )
- assertThat(tiles?.size).isEqualTo(1)
- assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("b"))
- assertThat(tiles!![0].tile).isNotSameInstanceAs(originalTileA)
+ assertThat(tiles?.size).isEqualTo(1)
+ assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("b"))
+ assertThat(tiles!![0].tile).isNotSameInstanceAs(originalTileA)
- assertThat(repoTiles).isEqualTo(tiles!!.map(TileModel::spec))
+ assertThat(repoTiles).isEqualTo(tiles!!.map(TileModel::spec))
+ }
}
@Test
fun userChange_tilesChange() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs0 = listOf(TileSpec.create("a"))
- val specs1 = listOf(TileSpec.create("b"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs0)
- tileSpecRepository.setTiles(USER_INFO_1.id, specs1)
+ val specs0 = listOf(TileSpec.create("a"))
+ val specs1 = listOf(TileSpec.create("b"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs0)
+ tileSpecRepository.setTiles(USER_INFO_1.id, specs1)
- switchUser(USER_INFO_1)
+ switchUser(USER_INFO_1)
- assertThat(tiles!![0].spec).isEqualTo(specs1[0])
- assertThat(tiles!![0].tile.tileSpec).isEqualTo(specs1[0].spec)
+ assertThat(tiles!![0].spec).isEqualTo(specs1[0])
+ assertThat(tiles!![0].tile.tileSpec).isEqualTo(specs1[0].spec)
+ }
}
@Test
fun tileNotPresentInSecondaryUser_destroyedInUserChange() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs0 = listOf(TileSpec.create("a"))
- val specs1 = listOf(TileSpec.create("b"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs0)
- tileSpecRepository.setTiles(USER_INFO_1.id, specs1)
+ val specs0 = listOf(TileSpec.create("a"))
+ val specs1 = listOf(TileSpec.create("b"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs0)
+ tileSpecRepository.setTiles(USER_INFO_1.id, specs1)
- val originalTileA = tiles!![0].tile
+ val originalTileA = tiles!![0].tile
- switchUser(USER_INFO_1)
- runCurrent()
+ switchUser(USER_INFO_1)
+ runCurrent()
- assertThat((originalTileA as FakeQSTile).destroyed).isTrue()
- verify(logger)
- .logTileDestroyed(
- specs0[0],
- QSPipelineLogger.TileDestroyedReason.TILE_NOT_PRESENT_IN_NEW_USER,
- )
+ assertThat(originalTileA.isDestroyed).isTrue()
+ verify(qsLogger)
+ .logTileDestroyed(
+ specs0[0],
+ QSPipelineLogger.TileDestroyedReason.TILE_NOT_PRESENT_IN_NEW_USER,
+ )
+ }
}
@Test
- fun userChange_customTileDestroyed_lifecycleNotTerminated() {
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
+ fun userChange_customTileDestroyed_lifecycleNotTerminated() =
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(CUSTOM_TILE_SPEC)
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- tileSpecRepository.setTiles(USER_INFO_1.id, specs)
+ val specs = listOf(CUSTOM_TILE_SPEC)
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ tileSpecRepository.setTiles(USER_INFO_1.id, specs)
- val originalCustomTile = tiles!![0].tile
+ val originalCustomTile = tiles!![0].tile
- switchUser(USER_INFO_1)
- runCurrent()
+ switchUser(USER_INFO_1)
+ runCurrent()
- verify(originalCustomTile).destroy()
- assertThat(tileLifecycleManagerFactory.created).isEmpty()
+ verify(originalCustomTile).destroy()
+ assertThat((tileLifecycleManagerFactory as TLMFactory).created).isEmpty()
+ }
}
- }
@Test
fun userChange_sameTileUserChanged() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- tileSpecRepository.setTiles(USER_INFO_1.id, specs)
+ val specs = listOf(TileSpec.create("a"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ tileSpecRepository.setTiles(USER_INFO_1.id, specs)
- val originalTileA = tiles!![0].tile as FakeQSTile
- assertThat(originalTileA.user).isEqualTo(USER_INFO_0.id)
+ val originalTileA = tiles!![0].tile as FakeQSTile
+ assertThat(originalTileA.user).isEqualTo(USER_INFO_0.id)
- switchUser(USER_INFO_1)
- runCurrent()
+ switchUser(USER_INFO_1)
+ runCurrent()
- assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA)
- assertThat(originalTileA.user).isEqualTo(USER_INFO_1.id)
- verify(logger).logTileUserChanged(specs[0], USER_INFO_1.id)
+ assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA)
+ assertThat(originalTileA.user).isEqualTo(USER_INFO_1.id)
+ verify(qsLogger).logTileUserChanged(specs[0], USER_INFO_1.id)
+ }
}
@Test
fun addTile() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
- val spec = TileSpec.create("a")
- val currentSpecs = listOf(TileSpec.create("b"), TileSpec.create("c"))
- tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+ val spec = TileSpec.create("a")
+ val currentSpecs = listOf(TileSpec.create("b"), TileSpec.create("c"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
- underTest.addTile(spec, position = 1)
+ underTest.addTile(spec, position = 1)
- val expectedSpecs = listOf(TileSpec.create("b"), spec, TileSpec.create("c"))
- assertThat(tiles).isEqualTo(expectedSpecs)
+ val expectedSpecs = listOf(TileSpec.create("b"), spec, TileSpec.create("c"))
+ assertThat(tiles).isEqualTo(expectedSpecs)
+ }
}
@Test
fun addTile_currentUser() =
- testScope.runTest(USER_INFO_1) {
- val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
- val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id))
- val spec = TileSpec.create("a")
- val currentSpecs = listOf(TileSpec.create("b"), TileSpec.create("c"))
- tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
- tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_1) {
+ val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+ val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id))
+ val spec = TileSpec.create("a")
+ val currentSpecs = listOf(TileSpec.create("b"), TileSpec.create("c"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
+ tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs)
- switchUser(USER_INFO_1)
- underTest.addTile(spec, position = 1)
+ switchUser(USER_INFO_1)
+ underTest.addTile(spec, position = 1)
- assertThat(tiles0).isEqualTo(currentSpecs)
+ assertThat(tiles0).isEqualTo(currentSpecs)
- val expectedSpecs = listOf(TileSpec.create("b"), spec, TileSpec.create("c"))
- assertThat(tiles1).isEqualTo(expectedSpecs)
+ val expectedSpecs = listOf(TileSpec.create("b"), spec, TileSpec.create("c"))
+ assertThat(tiles1).isEqualTo(expectedSpecs)
+ }
}
@Test
fun removeTile_platform() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
- val specs = listOf(TileSpec.create("a"), TileSpec.create("b"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- runCurrent()
+ val specs = listOf(TileSpec.create("a"), TileSpec.create("b"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ runCurrent()
- underTest.removeTiles(specs.subList(0, 1))
+ underTest.removeTiles(specs.subList(0, 1))
- assertThat(tiles).isEqualTo(specs.subList(1, 2))
+ assertThat(tiles).isEqualTo(specs.subList(1, 2))
+ }
}
@Test
- fun removeTile_customTile_lifecycleEnded() {
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+ fun removeTile_customTile_lifecycleEnded() =
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
- val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- runCurrent()
- assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id))
- .isTrue()
+ val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ runCurrent()
+ assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id))
+ .isTrue()
- underTest.removeTiles(listOf(CUSTOM_TILE_SPEC))
+ underTest.removeTiles(listOf(CUSTOM_TILE_SPEC))
- assertThat(tiles).isEqualTo(specs.subList(0, 1))
+ assertThat(tiles).isEqualTo(specs.subList(0, 1))
- val tileLifecycleManager =
- tileLifecycleManagerFactory.created[USER_INFO_0.id to TEST_COMPONENT]
- assertThat(tileLifecycleManager).isNotNull()
+ val tileLifecycleManager =
+ (tileLifecycleManagerFactory as TLMFactory)
+ .created[USER_INFO_0.id to TEST_COMPONENT]
+ assertThat(tileLifecycleManager).isNotNull()
- with(inOrder(tileLifecycleManager!!)) {
- verify(tileLifecycleManager).onStopListening()
- verify(tileLifecycleManager).onTileRemoved()
- verify(tileLifecycleManager).flushMessagesAndUnbind()
+ with(inOrder(tileLifecycleManager!!)) {
+ verify(tileLifecycleManager).onStopListening()
+ verify(tileLifecycleManager).onTileRemoved()
+ verify(tileLifecycleManager).flushMessagesAndUnbind()
+ }
+ assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id))
+ .isFalse()
+ assertThat(
+ customTileStatePersister.readState(
+ TileServiceKey(TEST_COMPONENT, USER_INFO_0.id)
+ )
+ )
+ .isNull()
}
- assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id))
- .isFalse()
- verify(customTileStatePersister)
- .removeState(TileServiceKey(TEST_COMPONENT, USER_INFO_0.id))
}
- }
@Test
fun removeTiles_currentUser() =
- testScope.runTest {
- val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
- val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id))
- val currentSpecs =
- listOf(TileSpec.create("a"), TileSpec.create("b"), TileSpec.create("c"))
- tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
- tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs)
+ with(kosmos) {
+ testScope.runTest {
+ val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+ val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id))
+ val currentSpecs =
+ listOf(TileSpec.create("a"), TileSpec.create("b"), TileSpec.create("c"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
+ tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs)
- switchUser(USER_INFO_1)
- runCurrent()
+ switchUser(USER_INFO_1)
+ runCurrent()
- underTest.removeTiles(currentSpecs.subList(0, 2))
+ underTest.removeTiles(currentSpecs.subList(0, 2))
- assertThat(tiles0).isEqualTo(currentSpecs)
- assertThat(tiles1).isEqualTo(currentSpecs.subList(2, 3))
+ assertThat(tiles0).isEqualTo(currentSpecs)
+ assertThat(tiles1).isEqualTo(currentSpecs.subList(2, 3))
+ }
}
@Test
fun setTiles() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
- val currentSpecs = listOf(TileSpec.create("a"), TileSpec.create("b"))
- tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
- runCurrent()
+ val currentSpecs = listOf(TileSpec.create("a"), TileSpec.create("b"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
+ runCurrent()
- val newSpecs = listOf(TileSpec.create("b"), TileSpec.create("c"), TileSpec.create("a"))
- underTest.setTiles(newSpecs)
- runCurrent()
+ val newSpecs =
+ listOf(TileSpec.create("b"), TileSpec.create("c"), TileSpec.create("a"))
+ underTest.setTiles(newSpecs)
+ runCurrent()
- assertThat(tiles).isEqualTo(newSpecs)
+ assertThat(tiles).isEqualTo(newSpecs)
+ }
}
@Test
fun setTiles_customTiles_lifecycleEndedIfGone() =
- testScope.runTest(USER_INFO_0) {
- val otherCustomTileSpec = TileSpec.create("custom(b/c)")
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val otherCustomTileSpec = TileSpec.create("custom(b/c)")
- val currentSpecs = listOf(CUSTOM_TILE_SPEC, TileSpec.create("a"), otherCustomTileSpec)
- tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
- runCurrent()
+ val currentSpecs =
+ listOf(CUSTOM_TILE_SPEC, TileSpec.create("a"), otherCustomTileSpec)
+ tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
+ runCurrent()
- val newSpecs = listOf(otherCustomTileSpec, TileSpec.create("a"))
+ val newSpecs = listOf(otherCustomTileSpec, TileSpec.create("a"))
- underTest.setTiles(newSpecs)
- runCurrent()
+ underTest.setTiles(newSpecs)
+ runCurrent()
- val tileLifecycleManager =
- tileLifecycleManagerFactory.created[USER_INFO_0.id to TEST_COMPONENT]!!
+ val tileLifecycleManager =
+ (tileLifecycleManagerFactory as TLMFactory)
+ .created[USER_INFO_0.id to TEST_COMPONENT]!!
- with(inOrder(tileLifecycleManager)) {
- verify(tileLifecycleManager).onStopListening()
- verify(tileLifecycleManager).onTileRemoved()
- verify(tileLifecycleManager).flushMessagesAndUnbind()
+ with(inOrder(tileLifecycleManager)) {
+ verify(tileLifecycleManager).onStopListening()
+ verify(tileLifecycleManager).onTileRemoved()
+ verify(tileLifecycleManager).flushMessagesAndUnbind()
+ }
+ assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id))
+ .isFalse()
+ assertThat(
+ customTileStatePersister.readState(
+ TileServiceKey(TEST_COMPONENT, USER_INFO_0.id)
+ )
+ )
+ .isNull()
}
- assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id))
- .isFalse()
- verify(customTileStatePersister)
- .removeState(TileServiceKey(TEST_COMPONENT, USER_INFO_0.id))
}
@Test
fun protoDump() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
+ val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- val stateA = tiles!![0].tile.state
- stateA.fillIn(Tile.STATE_INACTIVE, "A", "AA")
- val stateCustom = QSTile.BooleanState()
- stateCustom.fillIn(Tile.STATE_ACTIVE, "B", "BB")
- stateCustom.spec = CUSTOM_TILE_SPEC.spec
- whenever(tiles!![1].tile.state).thenReturn(stateCustom)
+ val stateA = tiles!![0].tile.state
+ stateA.fillIn(Tile.STATE_INACTIVE, "A", "AA")
+ val stateCustom = QSTile.BooleanState()
+ stateCustom.fillIn(Tile.STATE_ACTIVE, "B", "BB")
+ stateCustom.spec = CUSTOM_TILE_SPEC.spec
+ whenever(tiles!![1].tile.state).thenReturn(stateCustom)
- val proto = SystemUIProtoDump()
- underTest.dumpProto(proto, emptyArray())
+ val proto = SystemUIProtoDump()
+ underTest.dumpProto(proto, emptyArray())
- assertThat(MessageNano.messageNanoEquals(proto.tiles[0], stateA.toProto())).isTrue()
- assertThat(MessageNano.messageNanoEquals(proto.tiles[1], stateCustom.toProto()))
- .isTrue()
+ assertThat(MessageNano.messageNanoEquals(proto.tiles[0], stateA.toProto())).isTrue()
+ assertThat(MessageNano.messageNanoEquals(proto.tiles[1], stateCustom.toProto()))
+ .isTrue()
+ }
}
@Test
fun retainedTiles_callbackNotRemoved() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
- tileSpecRepository.setTiles(USER_INFO_0.id, listOf(TileSpec.create("a")))
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
+ tileSpecRepository.setTiles(USER_INFO_0.id, listOf(TileSpec.create("a")))
- val tileA = tiles!![0].tile
- val callback = mock<QSTile.Callback>()
- tileA.addCallback(callback)
+ val tileA = tiles!![0].tile
+ val callback = mock<QSTile.Callback>()
+ tileA.addCallback(callback)
- tileSpecRepository.setTiles(
- USER_INFO_0.id,
- listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC),
- )
- val newTileA = tiles!![0].tile
- assertThat(tileA).isSameInstanceAs(newTileA)
+ tileSpecRepository.setTiles(
+ USER_INFO_0.id,
+ listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC),
+ )
+ val newTileA = tiles!![0].tile
+ assertThat(tileA).isSameInstanceAs(newTileA)
- assertThat((tileA as FakeQSTile).callbacks).containsExactly(callback)
+ assertThat((tileA as FakeQSTile).callbacks).containsExactly(callback)
+ }
}
@Test
fun packageNotInstalled_customTileNotVisible() =
- testScope.runTest(USER_INFO_0) {
- installedTilesPackageRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ fakeInstalledTilesRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
- val tiles by collectLastValue(underTest.currentTiles)
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- assertThat(tiles!!.size).isEqualTo(1)
- assertThat(tiles!![0].spec).isEqualTo(specs[0])
+ assertThat(tiles!!.size).isEqualTo(1)
+ assertThat(tiles!![0].spec).isEqualTo(specs[0])
+ }
}
@Test
fun packageInstalledLater_customTileAdded() =
- testScope.runTest(USER_INFO_0) {
- installedTilesPackageRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ fakeInstalledTilesRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
- val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC, TileSpec.create("b"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ val tiles by collectLastValue(underTest.currentTiles)
+ val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC, TileSpec.create("b"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- assertThat(tiles!!.size).isEqualTo(2)
+ assertThat(tiles!!.size).isEqualTo(2)
- installedTilesPackageRepository.setInstalledPackagesForUser(
- USER_INFO_0.id,
- setOf(TEST_COMPONENT),
- )
+ fakeInstalledTilesRepository.setInstalledPackagesForUser(
+ USER_INFO_0.id,
+ setOf(TEST_COMPONENT),
+ )
- assertThat(tiles!!.size).isEqualTo(3)
- assertThat(tiles!![1].spec).isEqualTo(CUSTOM_TILE_SPEC)
+ assertThat(tiles!!.size).isEqualTo(3)
+ assertThat(tiles!![1].spec).isEqualTo(CUSTOM_TILE_SPEC)
+ }
}
@Test
fun tileAddedOnEmptyList_blocked() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"), TileSpec.create("b"))
- val newTile = TileSpec.create("c")
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
+ val specs = listOf(TileSpec.create("a"), TileSpec.create("b"))
+ val newTile = TileSpec.create("c")
- underTest.addTile(newTile)
+ underTest.addTile(newTile)
- assertThat(tiles!!.isEmpty()).isTrue()
+ assertThat(tiles!!.isEmpty()).isTrue()
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- assertThat(tiles!!.size).isEqualTo(3)
+ assertThat(tiles!!.size).isEqualTo(3)
+ }
}
@Test
fun changeInPackagesTiles_doesntTriggerUserChange_logged() =
- testScope.runTest(USER_INFO_0) {
- val specs = listOf(TileSpec.create("a"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- runCurrent()
- // Settled on the same list of tiles.
- assertThat(underTest.currentTilesSpecs).isEqualTo(specs)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val specs = listOf(TileSpec.create("a"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ runCurrent()
+ // Settled on the same list of tiles.
+ assertThat(underTest.currentTilesSpecs).isEqualTo(specs)
- installedTilesPackageRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
- runCurrent()
+ fakeInstalledTilesRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
+ runCurrent()
- verify(logger, never()).logTileUserChanged(TileSpec.create("a"), 0)
+ verify(qsLogger, never()).logTileUserChanged(TileSpec.create("a"), 0)
+ }
}
@Test
fun getTileDetails() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
- val tileA = TileSpec.create("a")
- val tileB = TileSpec.create("b")
- val tileNoDetails = TileSpec.create("NoDetails")
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
+ val tileA = TileSpec.create("a")
+ val tileB = TileSpec.create("b")
+ val tileNoDetails = TileSpec.create("NoDetails")
- val specs = listOf(tileA, tileB, tileNoDetails)
+ val specs = listOf(tileA, tileB, tileNoDetails)
- assertThat(tiles!!.isEmpty()).isTrue()
+ assertThat(tiles!!.isEmpty()).isTrue()
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- assertThat(tiles!!.size).isEqualTo(3)
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ assertThat(tiles!!.size).isEqualTo(3)
- // The third tile doesn't have a details view.
- assertThat(tiles!![2].spec).isEqualTo(tileNoDetails)
- (tiles!![2].tile as FakeQSTile).hasDetailsViewModel = false
+ // The third tile doesn't have a details view.
+ assertThat(tiles!![2].spec).isEqualTo(tileNoDetails)
+ (tiles!![2].tile as FakeQSTile).hasDetailsViewModel = false
- var currentModel: TileDetailsViewModel? = null
- val setCurrentModel = { model: TileDetailsViewModel? -> currentModel = model }
- tiles!![0].tile.getDetailsViewModel(setCurrentModel)
- assertThat(currentModel?.getTitle()).isEqualTo("a")
+ var currentModel: TileDetailsViewModel? = null
+ val setCurrentModel = { model: TileDetailsViewModel? -> currentModel = model }
+ tiles!![0].tile.getDetailsViewModel(setCurrentModel)
+ assertThat(currentModel?.getTitle()).isEqualTo("a")
- currentModel = null
- tiles!![1].tile.getDetailsViewModel(setCurrentModel)
- assertThat(currentModel?.getTitle()).isEqualTo("b")
+ currentModel = null
+ tiles!![1].tile.getDetailsViewModel(setCurrentModel)
+ assertThat(currentModel?.getTitle()).isEqualTo("b")
- currentModel = null
- tiles!![2].tile.getDetailsViewModel(setCurrentModel)
- assertThat(currentModel).isNull()
+ currentModel = null
+ tiles!![2].tile.getDetailsViewModel(setCurrentModel)
+ assertThat(currentModel).isNull()
+ }
+ }
+
+ @Test
+ fun destroyedTilesNotReused() =
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
+ val specs = listOf(TileSpec.create("a"), TileSpec.create("b"))
+ val newTile = TileSpec.create("c")
+
+ underTest.setTiles(specs)
+
+ val tileABefore = tiles!!.first { it.spec == specs[0] }.tile
+
+ // We destroy it manually, in prod, this could happen if the tile processing action
+ // is interrupted in the middle.
+ tileABefore.destroy()
+
+ underTest.addTile(newTile)
+
+ val tileAAfter = tiles!!.first { it.spec == specs[0] }.tile
+ assertThat(tileAAfter).isNotSameInstanceAs(tileABefore)
+ }
}
private fun QSTile.State.fillIn(state: Int, label: CharSequence, secondaryLabel: CharSequence) {
@@ -686,20 +735,21 @@
}
}
- private fun tileCreator(spec: String): QSTile? {
- val currentUser = userTracker.userId
+ private fun Kosmos.tileCreator(spec: String): QSTile? {
+ val currentUser = userRepository.getSelectedUserInfo().id
return when (spec) {
CUSTOM_TILE_SPEC.spec ->
mock<CustomTile> {
var tileSpecReference: String? = null
- whenever(user).thenReturn(currentUser)
- whenever(component).thenReturn(CUSTOM_TILE_SPEC.componentName)
- whenever(isAvailable).thenReturn(true)
- whenever(setTileSpec(anyString())).thenAnswer {
- tileSpecReference = it.arguments[0] as? String
- Unit
- }
- whenever(tileSpec).thenAnswer { tileSpecReference }
+ on { user } doReturn currentUser
+ on { component } doReturn CUSTOM_TILE_SPEC.componentName
+ on { isAvailable } doReturn true
+ on { setTileSpec(anyString()) }
+ .thenAnswer {
+ tileSpecReference = it.arguments[0] as? String
+ Unit
+ }
+ on { tileSpec }.thenAnswer { tileSpecReference }
// Also, add it to the set of added tiles (as this happens as part of the tile
// creation).
customTileAddedRepository.setTileAdded(
@@ -714,22 +764,16 @@
}
private fun TestScope.runTest(user: UserInfo, body: suspend TestScope.() -> Unit) {
- return runTest {
+ return kosmos.runTest {
switchUser(user)
body()
}
}
- private suspend fun switchUser(user: UserInfo) {
- setUserTracker(user.id)
- installedTilesPackageRepository.setInstalledPackagesForUser(user.id, setOf(TEST_COMPONENT))
- userRepository.setSelectedUserInfo(user)
- }
-
- private fun setUserTracker(user: Int) {
- val mockContext = mockUserContext(user)
- whenever(userTracker.userContext).thenReturn(mockContext)
- whenever(userTracker.userId).thenReturn(user)
+ private suspend fun Kosmos.switchUser(user: UserInfo) {
+ fakeUserTracker.set(listOf(user), 0)
+ fakeInstalledTilesRepository.setInstalledPackagesForUser(user.id, setOf(TEST_COMPONENT))
+ fakeUserRepository.setSelectedUserInfo(user)
}
private class TLMFactory : TileLifecycleManager.Factory {
@@ -745,13 +789,6 @@
}
}
- private fun mockUserContext(user: Int): Context {
- return mock {
- whenever(this.userId).thenReturn(user)
- whenever(this.user).thenReturn(UserHandle.of(user))
- }
- }
-
companion object {
private val USER_INFO_0 = UserInfo().apply { id = 0 }
private val USER_INFO_1 = UserInfo().apply { id = 1 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
index 296478b..1d8c6cc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
@@ -508,6 +508,12 @@
assertThat(mTile.mRefreshes).isEqualTo(1);
}
+ @Test
+ public void testIsDestroyedImmediately() {
+ mTile.destroy();
+ assertThat(mTile.isDestroyed()).isTrue();
+ }
+
private void assertEvent(UiEventLogger.UiEventEnum eventType,
UiEventLoggerFake.FakeUiEvent fakeEvent) {
assertEquals(eventType.getId(), fakeEvent.eventId);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
index fba6151..da3cebd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
@@ -22,6 +22,7 @@
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.FakeTileDetailsViewModel
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor
@@ -97,6 +98,7 @@
testCoroutineDispatcher,
testCoroutineDispatcher,
testScope.backgroundScope,
+ FakeTileDetailsViewModel("QSTileViewModelImplTest"),
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
index 3db5efc..261e3de 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
@@ -26,8 +26,6 @@
import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
-import com.android.systemui.qs.tiles.dialog.InternetDetailsContentManager
-import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
import com.android.systemui.qs.tiles.dialog.InternetDialogManager
import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
import com.android.systemui.statusbar.connectivity.AccessPointController
@@ -39,11 +37,8 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.eq
-import org.mockito.kotlin.any
import org.mockito.kotlin.mock
-import org.mockito.kotlin.times
import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
@SmallTest
@EnabledOnRavenwood
@@ -56,31 +51,17 @@
private lateinit var internetDialogManager: InternetDialogManager
private lateinit var controller: AccessPointController
- private lateinit var internetDetailsViewModelFactory: InternetDetailsViewModel.Factory
- private lateinit var internetDetailsContentManagerFactory: InternetDetailsContentManager.Factory
- private lateinit var internetDetailsViewModel: InternetDetailsViewModel
@Before
fun setup() {
internetDialogManager = mock<InternetDialogManager>()
controller = mock<AccessPointController>()
- internetDetailsViewModelFactory = mock<InternetDetailsViewModel.Factory>()
- internetDetailsContentManagerFactory = mock<InternetDetailsContentManager.Factory>()
- internetDetailsViewModel =
- InternetDetailsViewModel(
- onLongClick = {},
- accessPointController = mock<AccessPointController>(),
- contentManagerFactory = internetDetailsContentManagerFactory,
- )
- whenever(internetDetailsViewModelFactory.create(any())).thenReturn(internetDetailsViewModel)
-
underTest =
InternetTileUserActionInteractor(
kosmos.testScope.coroutineContext,
internetDialogManager,
controller,
inputHandler,
- internetDetailsViewModelFactory,
)
}
@@ -127,12 +108,4 @@
assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS)
}
}
-
- @Test
- fun detailsViewModel() =
- kosmos.testScope.runTest {
- assertThat(underTest.detailsViewModel.getTitle()).isEqualTo("Internet")
- assertThat(underTest.detailsViewModel.getSubTitle())
- .isEqualTo("Tab a network to connect")
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
index 0598a8b..4e9b635 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
@@ -25,6 +25,7 @@
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.FakeTileDetailsViewModel
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
@@ -171,21 +172,6 @@
.isEqualTo(FakeQSTileDataInteractor.AvailabilityRequest(USER))
}
- @Test
- fun tileDetails() =
- testScope.runTest {
- assertThat(tileUserActionInteractor.detailsViewModel).isNotNull()
- assertThat(tileUserActionInteractor.detailsViewModel?.getTitle())
- .isEqualTo("FakeQSTileUserActionInteractor")
- assertThat(underTest.detailsViewModel).isNotNull()
- assertThat(underTest.detailsViewModel?.getTitle())
- .isEqualTo("FakeQSTileUserActionInteractor")
-
- tileUserActionInteractor.detailsViewModel = null
- assertThat(tileUserActionInteractor.detailsViewModel).isNull()
- assertThat(underTest.detailsViewModel).isNull()
- }
-
private fun createViewModel(
scope: TestScope,
config: QSTileConfig = tileConfig,
@@ -209,6 +195,7 @@
testCoroutineDispatcher,
testCoroutineDispatcher,
scope.backgroundScope,
+ FakeTileDetailsViewModel("QSTileViewModelTest"),
)
private companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
index ece21e1..166e950 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
@@ -22,6 +22,7 @@
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.FakeTileDetailsViewModel
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
@@ -253,5 +254,6 @@
testCoroutineDispatcher,
testCoroutineDispatcher,
scope.backgroundScope,
+ FakeTileDetailsViewModel("QSTileViewModelUserInputTest"),
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
index c69ebab..baf0aeb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
@@ -61,8 +61,7 @@
usingMediaInComposeFragment = false // This is not for the compose fragment
}
private val testScope = kosmos.testScope
- private val sceneInteractor = kosmos.sceneInteractor
-
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val underTest by lazy { kosmos.quickSettingsShadeOverlayContentViewModel }
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 559e363..d3f5923 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -73,9 +73,8 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val fakeSceneDataSource = kosmos.fakeSceneDataSource
-
- private val underTest = kosmos.sceneInteractor
+ private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource }
+ private val underTest by lazy { kosmos.sceneInteractor }
@Before
fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
index 4a011c0..ccc876c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
@@ -50,11 +50,10 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val configurationRepository = kosmos.fakeConfigurationRepository
- private val keyguardRepository = kosmos.fakeKeyguardRepository
- private val sceneInteractor = kosmos.sceneInteractor
+ private val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
+ private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
-
private val underTest by lazy { kosmos.shadeInteractorSceneContainerImpl }
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index 37b4688..a832f48 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -15,7 +15,9 @@
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.runCurrent
import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.plugins.activityStarter
import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -51,12 +53,11 @@
@RunWith(AndroidJUnit4::class)
@EnableSceneContainer
class ShadeHeaderViewModelTest : SysuiTestCase() {
- private val kosmos = testKosmos()
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val testScope = kosmos.testScope
- private val mobileIconsInteractor = kosmos.fakeMobileIconsInteractor
- private val sceneInteractor = kosmos.sceneInteractor
- private val deviceEntryInteractor = kosmos.deviceEntryInteractor
-
+ private val mobileIconsInteractor by lazy { kosmos.fakeMobileIconsInteractor }
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
+ private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
private val underTest by lazy { kosmos.shadeHeaderViewModel }
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 3d8da61..70df82d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -22,6 +22,7 @@
import static android.service.quickaccesswallet.Flags.FLAG_LAUNCH_WALLET_VIA_SYSUI_CALLBACKS;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -556,9 +557,9 @@
@Test
public void testImmersiveModeChanged() {
final int displayAreaId = 10;
- mCommandQueue.immersiveModeChanged(displayAreaId, true);
+ mCommandQueue.immersiveModeChanged(displayAreaId, true, TYPE_APPLICATION);
waitForIdleSync();
- verify(mCallbacks).immersiveModeChanged(displayAreaId, true);
+ verify(mCallbacks).immersiveModeChanged(displayAreaId, true, TYPE_APPLICATION);
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt
index 510167d..d3cd240 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt
@@ -84,7 +84,7 @@
controller.onIntentStarted(willAnimate = false)
assertTrue(HeadsUpUtil.isClickedHeadsUpNotification(notification))
- assertFalse(notification.entry.isExpandAnimationRunning)
+ assertFalse(notification.isLaunchAnimationRunning)
val isExpandAnimationRunning by
testScope.collectLastValue(
notificationLaunchAnimationInteractor.isLaunchAnimationRunning
@@ -107,7 +107,7 @@
controller.onTransitionAnimationCancelled()
assertTrue(HeadsUpUtil.isClickedHeadsUpNotification(notification))
- assertFalse(notification.entry.isExpandAnimationRunning)
+ assertFalse(notification.isLaunchAnimationRunning)
val isExpandAnimationRunning by
testScope.collectLastValue(
notificationLaunchAnimationInteractor.isLaunchAnimationRunning
@@ -130,7 +130,7 @@
controller.onTransitionAnimationEnd(isExpandingFullyAbove = true)
assertFalse(HeadsUpUtil.isClickedHeadsUpNotification(notification))
- assertFalse(notification.entry.isExpandAnimationRunning)
+ assertFalse(notification.isLaunchAnimationRunning)
val isExpandAnimationRunning by
testScope.collectLastValue(
notificationLaunchAnimationInteractor.isLaunchAnimationRunning
@@ -199,7 +199,7 @@
fun testNotificationIsExpandingDuringAnimation() {
controller.onIntentStarted(willAnimate = true)
- assertTrue(notification.entry.isExpandAnimationRunning)
+ assertTrue(notification.isLaunchAnimationRunning)
val isExpandAnimationRunning by
testScope.collectLastValue(
notificationLaunchAnimationInteractor.isLaunchAnimationRunning
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
index 92b8c3a..13da04e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
@@ -24,6 +24,7 @@
import com.android.systemui.flags.andSceneContainer
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
import com.android.systemui.statusbar.notification.headsup.AvalancheController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.util.mockito.mock
@@ -47,6 +48,7 @@
private val statusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>()
private val largeScreenShadeInterpolator = mock<LargeScreenShadeInterpolator>()
private val avalancheController = mock<AvalancheController>()
+ private val headsupRepository = mock<HeadsUpRepository>()
private lateinit var sut: AmbientState
@@ -72,6 +74,7 @@
bypassController,
statusBarKeyguardViewManager,
largeScreenShadeInterpolator,
+ headsupRepository,
avalancheController,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 33211d4..ce655ef 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -22,6 +22,7 @@
import com.android.systemui.statusbar.notification.RoundableState
import com.android.systemui.statusbar.notification.collection.EntryAdapter
import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView.FooterViewState
@@ -61,6 +62,7 @@
private val dumpManager = mock<DumpManager>()
private val mStatusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>()
private val notificationShelf = mock<NotificationShelf>()
+ private val headsUpRepository = mock<HeadsUpRepository>()
private val emptyShadeView =
EmptyShadeView(context, /* attrs= */ null).apply {
layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100)
@@ -74,6 +76,7 @@
/* bypassController */ { false },
mStatusBarKeyguardViewManager,
largeScreenShadeInterpolator,
+ headsUpRepository,
avalancheController,
)
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
index 6e4dc14..0cbc30d 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
@@ -34,6 +34,9 @@
/** Font axes that can be modified on this clock */
val axes: List<ClockFontAxis> = listOf(),
+
+ /** List of font presets for this clock. Can be assigned directly. */
+ val axisPresets: List<List<ClockFontAxisSetting>> = listOf(),
)
/** Represents an Axis that can be modified */
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
index 56176cf..d197cdb7 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
@@ -42,7 +42,7 @@
@DependsOn(target = Icon.class)
@DependsOn(target = State.class)
public interface QSTile {
- int VERSION = 4;
+ int VERSION = 5;
String getTileSpec();
@@ -78,6 +78,7 @@
void longClick(@Nullable Expandable expandable);
void userSwitch(int currentUser);
+ int getCurrentTileUser();
/**
* @deprecated not needed as {@link com.android.internal.logging.UiEvent} will use
@@ -150,6 +151,8 @@
return null;
}
+ boolean isDestroyed();
+
@ProvidesInterface(version = Callback.VERSION)
interface Callback {
static final int VERSION = 2;
diff --git a/packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.xml b/packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.xml
new file mode 100644
index 0000000..1de8c2b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+ <solid
+ android:color="@color/notification_guts_priority_button_bg_fill" />
+
+ <stroke
+ android:width="1.5dp"
+ android:color="@color/notification_guts_priority_button_bg_stroke" />
+
+ <corners android:radius="16dp" />
+</shape>
diff --git a/packages/SystemUI/res/layout/notification_2025_info.xml b/packages/SystemUI/res/layout/notification_2025_info.xml
new file mode 100644
index 0000000..7b69166
--- /dev/null
+++ b/packages/SystemUI/res/layout/notification_2025_info.xml
@@ -0,0 +1,365 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2025, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- extends LinearLayout -->
+<com.android.systemui.statusbar.notification.row.NotificationInfo
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/notification_guts"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:clipChildren="false"
+ android:clipToPadding="true"
+ android:orientation="vertical"
+ android:paddingStart="@*android:dimen/notification_2025_margin">
+
+ <!-- Package Info -->
+ <LinearLayout
+ android:id="@+id/header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:clipToPadding="true">
+ <ImageView
+ android:id="@+id/pkg_icon"
+ android:layout_width="@*android:dimen/notification_2025_icon_circle_size"
+ android:layout_height="@*android:dimen/notification_2025_icon_circle_size"
+ android:layout_marginTop="@*android:dimen/notification_2025_margin"
+ android:layout_marginEnd="@*android:dimen/notification_2025_margin" />
+ <LinearLayout
+ android:id="@+id/names"
+ android:layout_weight="1"
+ android:layout_width="0dp"
+ android:orientation="vertical"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@*android:dimen/notification_2025_margin"
+ android:minHeight="@*android:dimen/notification_2025_icon_circle_size">
+ <TextView
+ android:id="@+id/channel_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textDirection="locale"
+ style="@style/TextAppearance.NotificationImportanceChannel"/>
+ <TextView
+ android:id="@+id/group_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textDirection="locale"
+ android:ellipsize="end"
+ style="@style/TextAppearance.NotificationImportanceChannelGroup"/>
+ <TextView
+ android:id="@+id/pkg_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/TextAppearance.NotificationImportanceApp"
+ android:ellipsize="end"
+ android:textDirection="locale"
+ android:maxLines="1"/>
+ <TextView
+ android:id="@+id/delegate_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/TextAppearance.NotificationImportanceHeader"
+ android:ellipsize="end"
+ android:textDirection="locale"
+ android:text="@string/notification_delegate_header"
+ android:maxLines="1" />
+
+ </LinearLayout>
+
+ <!-- feedback for notificationassistantservice -->
+ <ImageButton
+ android:id="@+id/feedback"
+ android:layout_width="@dimen/notification_2025_guts_button_size"
+ android:layout_height="@dimen/notification_2025_guts_button_size"
+ android:visibility="gone"
+ android:background="@drawable/ripple_drawable"
+ android:contentDescription="@string/notification_guts_bundle_feedback"
+ android:src="@*android:drawable/ic_feedback"
+ android:paddingTop="@*android:dimen/notification_2025_margin"
+ android:tint="@androidprv:color/materialColorPrimary"/>
+
+ <!-- Optional link to app. Only appears if the channel is not disabled and the app
+ asked for it -->
+ <ImageButton
+ android:id="@+id/app_settings"
+ android:layout_width="@dimen/notification_2025_guts_button_size"
+ android:layout_height="@dimen/notification_2025_guts_button_size"
+ android:visibility="gone"
+ android:background="@drawable/ripple_drawable"
+ android:contentDescription="@string/notification_app_settings"
+ android:src="@drawable/ic_info"
+ android:paddingTop="@*android:dimen/notification_2025_margin"
+ android:tint="@androidprv:color/materialColorPrimary"/>
+
+ <!-- System notification settings -->
+ <ImageButton
+ android:id="@+id/info"
+ android:layout_width="@dimen/notification_2025_guts_button_size"
+ android:layout_height="@dimen/notification_2025_guts_button_size"
+ android:contentDescription="@string/notification_more_settings"
+ android:background="@drawable/ripple_drawable"
+ android:src="@drawable/ic_settings"
+ android:padding="@*android:dimen/notification_2025_margin"
+ android:tint="@androidprv:color/materialColorPrimary" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/inline_controls"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@*android:dimen/notification_2025_margin"
+ android:layout_marginTop="@*android:dimen/notification_2025_margin"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:orientation="vertical">
+
+ <!-- Non configurable app/channel text. appears instead of @+id/interruptiveness_settings-->
+ <TextView
+ android:id="@+id/non_configurable_text"
+ android:text="@string/notification_unblockable_desc"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@*android:style/TextAppearance.DeviceDefault.Notification" />
+
+ <!-- Non configurable app/channel text. appears instead of @+id/interruptiveness_settings-->
+ <TextView
+ android:id="@+id/non_configurable_call_text"
+ android:text="@string/notification_unblockable_call_desc"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@*android:style/TextAppearance.DeviceDefault.Notification" />
+
+ <!-- Non configurable multichannel text. appears instead of @+id/interruptiveness_settings-->
+ <TextView
+ android:id="@+id/non_configurable_multichannel_text"
+ android:text="@string/notification_multichannel_desc"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@*android:style/TextAppearance.DeviceDefault.Notification" />
+
+ <LinearLayout
+ android:id="@+id/interruptiveness_settings"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="vertical">
+ <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
+ android:id="@+id/automatic"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="@dimen/notification_2025_importance_button_padding_vertical"
+ android:paddingHorizontal="@dimen/notification_2025_importance_button_padding_horizontal"
+ android:gravity="center_vertical"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="@drawable/notification_2025_guts_priority_button_bg"
+ android:orientation="horizontal"
+ android:visibility="gone">
+ <ImageView
+ android:id="@+id/automatic_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingEnd="@*android:dimen/notification_2025_margin"
+ android:src="@drawable/ic_notifications_automatic"
+ android:background="@android:color/transparent"
+ android:tint="@color/notification_guts_priority_contents"
+ android:clickable="false"
+ android:focusable="false"/>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center"
+ >
+ <TextView
+ android:id="@+id/automatic_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:clickable="false"
+ android:focusable="false"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+ android:text="@string/notification_automatic_title"/>
+ <TextView
+ android:id="@+id/automatic_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/notification_importance_button_description_top_margin"
+ android:visibility="gone"
+ android:text="@string/notification_channel_summary_automatic"
+ android:clickable="false"
+ android:focusable="false"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
+ </LinearLayout>
+ </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
+
+ <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
+ android:id="@+id/alert"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="@dimen/notification_2025_importance_button_padding_vertical"
+ android:paddingHorizontal="@dimen/notification_2025_importance_button_padding_horizontal"
+ android:gravity="center_vertical"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="@drawable/notification_2025_guts_priority_button_bg"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@+id/alert_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingEnd="@*android:dimen/notification_2025_margin"
+ android:src="@drawable/ic_notifications_alert"
+ android:background="@android:color/transparent"
+ android:tint="@color/notification_guts_priority_contents"
+ android:clickable="false"
+ android:focusable="false"/>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center"
+ >
+ <TextView
+ android:id="@+id/alert_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:clickable="false"
+ android:focusable="false"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+ android:text="@string/notification_alert_title"/>
+ <TextView
+ android:id="@+id/alert_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:text="@string/notification_channel_summary_default"
+ android:clickable="false"
+ android:focusable="false"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
+ </LinearLayout>
+ </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
+
+ <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
+ android:id="@+id/silence"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/notification_importance_button_separation"
+ android:paddingVertical="@dimen/notification_2025_importance_button_padding_vertical"
+ android:paddingHorizontal="@dimen/notification_2025_importance_button_padding_horizontal"
+ android:gravity="center_vertical"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="@drawable/notification_2025_guts_priority_button_bg"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@+id/silence_icon"
+ android:src="@drawable/ic_notifications_silence"
+ android:background="@android:color/transparent"
+ android:tint="@color/notification_guts_priority_contents"
+ android:layout_gravity="center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingEnd="@*android:dimen/notification_2025_margin"
+ android:clickable="false"
+ android:focusable="false"/>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center"
+ >
+ <TextView
+ android:id="@+id/silence_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:clickable="false"
+ android:focusable="false"
+ android:layout_toEndOf="@id/silence_icon"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+ android:text="@string/notification_silence_title"/>
+ <TextView
+ android:id="@+id/silence_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:text="@string/notification_channel_summary_low"
+ android:clickable="false"
+ android:focusable="false"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
+ </LinearLayout>
+ </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/bottom_buttons"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@*android:dimen/notification_2025_margin"
+ android:minHeight="@dimen/notification_2025_guts_button_size"
+ android:gravity="center_vertical"
+ >
+ <TextView
+ android:id="@+id/turn_off_notifications"
+ android:text="@string/inline_turn_off_notifications"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="32dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="@*android:dimen/notification_2025_margin"
+ android:gravity="center"
+ android:minWidth="@dimen/notification_2025_min_tap_target_size"
+ android:minHeight="@dimen/notification_2025_min_tap_target_size"
+ android:maxWidth="200dp"
+ style="@style/TextAppearance.NotificationInfo.Button"
+ android:textSize="@*android:dimen/notification_2025_action_text_size"/>
+ <TextView
+ android:id="@+id/done"
+ android:text="@string/inline_ok_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="8dp"
+ android:paddingBottom="@*android:dimen/notification_2025_margin"
+ android:gravity="center"
+ android:minWidth="@dimen/notification_2025_min_tap_target_size"
+ android:minHeight="@dimen/notification_2025_min_tap_target_size"
+ android:maxWidth="125dp"
+ style="@style/TextAppearance.NotificationInfo.Button"
+ android:textSize="@*android:dimen/notification_2025_action_text_size"/>
+ </LinearLayout>
+ </LinearLayout>
+</com.android.systemui.statusbar.notification.row.NotificationInfo>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index b273886..4995858 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -1125,4 +1125,7 @@
<!-- Configuration to swipe to open glanceable hub -->
<bool name="config_swipeToOpenGlanceableHub">false</bool>
+
+ <!-- Whether or not to show the UMO on the glanceable hub when media is playing. -->
+ <bool name="config_showUmoOnHub">false</bool>
</resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 640e1fa..7c370d3 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -390,6 +390,12 @@
<!-- Extra space for guts bundle feedback button -->
<dimen name="notification_guts_bundle_feedback_size">48dp</dimen>
+ <!-- Size of icon buttons in notification info. -->
+ <!-- 24dp for the icon itself + 16dp * 2 for top and bottom padding -->
+ <dimen name="notification_2025_guts_button_size">56dp</dimen>
+
+ <dimen name="notification_2025_min_tap_target_size">48dp</dimen>
+
<dimen name="notification_importance_toggle_size">48dp</dimen>
<dimen name="notification_importance_button_separation">8dp</dimen>
<dimen name="notification_importance_drawable_padding">8dp</dimen>
@@ -402,6 +408,10 @@
<dimen name="notification_importance_button_description_top_margin">12dp</dimen>
<dimen name="rect_button_radius">8dp</dimen>
+ <!-- Padding for importance selection buttons in notification info, 2025 redesign version -->
+ <dimen name="notification_2025_importance_button_padding_vertical">12dp</dimen>
+ <dimen name="notification_2025_importance_button_padding_horizontal">16dp</dimen>
+
<!-- The minimum height for the snackbar shown after the snooze option has been chosen. -->
<dimen name="snooze_snackbar_min_height">56dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 084495f..6ff1240 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3176,8 +3176,8 @@
<string name="controls_media_settings_button">Settings</string>
<!-- Description for media control's playing media item, including information for the media's title, the artist, and source app [CHAR LIMIT=NONE]-->
<string name="controls_media_playing_item_description"><xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> by <xliff:g id="artist_name" example="Various artists">%2$s</xliff:g> is playing from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string>
- <!-- Content description for media controls progress bar [CHAR_LIMIT=NONE] -->
- <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1 hour 2 minutes 30 seconds">%1$s</xliff:g> of <xliff:g id="total_time" example="4 hours 5 seconds">%2$s</xliff:g></string>
+ <!-- Content description for media cotnrols progress bar [CHAR_LIMIT=NONE] -->
+ <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1:30">%1$s</xliff:g> of <xliff:g id="total_time" example="3:00">%2$s</xliff:g></string>
<!-- Placeholder title to inform user that an app has posted media controls [CHAR_LIMIT=NONE] -->
<string name="controls_media_empty_title"><xliff:g id="app_name" example="Foo Music App">%1$s</xliff:g> is running</string>
@@ -4178,4 +4178,7 @@
<string name="qs_edit_mode_reset_dialog_content">
All Quick Settings tiles will reset to the device’s original settings
</string>
+
+ <!-- Template that joins disabled message with the label for the voice over. [CHAR LIMIT=NONE] -->
+ <string name="volume_slider_disabled_message_template"><xliff:g example="Notification" id="stream_name">%1$s</xliff:g>, <xliff:g example="Disabled because ring is muted" id="disabled_message">%2$s</xliff:g></string>
</resources>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
index 4a4cb7a..8f8bcf2 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
@@ -239,7 +239,6 @@
R.dimen.keyguard_pattern_activated_dot_size));
mLockPatternView.setPathWidth(
getResources().getDimensionPixelSize(R.dimen.keyguard_pattern_stroke_width));
- mLockPatternView.setKeepDotActivated(true);
}
mEcaView = findViewById(R.id.keyguard_selector_fade_container);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index 7fb6664..f6df425 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -36,6 +36,7 @@
import com.android.internal.widget.LockscreenCredential;
import com.android.keyguard.EmergencyButtonController.EmergencyButtonCallback;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.systemui.Flags;
import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer;
import com.android.systemui.classifier.FalsingClassifier;
import com.android.systemui.classifier.FalsingCollector;
@@ -237,8 +238,12 @@
super.onViewAttached();
mLockPatternView.setOnPatternListener(new UnlockPatternListener());
mLockPatternView.setSaveEnabled(false);
- mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled(
- mSelectedUserInteractor.getSelectedUserId()));
+ boolean visiblePatternEnabled = mLockPatternUtils.isVisiblePatternEnabled(
+ mSelectedUserInteractor.getSelectedUserId());
+ mLockPatternView.setInStealthMode(!visiblePatternEnabled);
+ if (Flags.bouncerUiRevamp2()) {
+ mLockPatternView.setKeepDotActivated(visiblePatternEnabled);
+ }
mLockPatternView.setOnTouchListener((v, event) -> {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mFalsingCollector.avoidGesture();
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
index 208adc2..5f7dca8 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
@@ -64,15 +64,14 @@
synchronized(sessions) {
sessions.putAll(
packageInstaller.allSessions
- .filter { !TextUtils.isEmpty(it.appPackageName) }
- .map { session -> session.toModel() }
+ .mapNotNull { session -> session.toModel() }
.associateBy { it.sessionId }
)
updateInstallerSessionsFlow()
}
packageInstaller.registerSessionCallback(
this@PackageInstallerMonitor,
- bgHandler
+ bgHandler,
)
} else {
synchronized(sessions) {
@@ -130,7 +129,7 @@
if (session == null) {
sessions.remove(sessionId)
} else {
- sessions[sessionId] = session.toModel()
+ session.toModel()?.apply { sessions[sessionId] = this }
}
updateInstallerSessionsFlow()
}
@@ -144,7 +143,11 @@
companion object {
const val TAG = "PackageInstallerMonitor"
- private fun PackageInstaller.SessionInfo.toModel(): PackageInstallSession {
+ private fun PackageInstaller.SessionInfo.toModel(): PackageInstallSession? {
+ if (TextUtils.isEmpty(this.appPackageName)) {
+ return null
+ }
+
return PackageInstallSession(
sessionId = this.sessionId,
packageName = this.appPackageName,
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/colors/SurfaceEffectColors.kt b/packages/SystemUI/src/com/android/systemui/common/shared/colors/SurfaceEffectColors.kt
index 5e8c21f9..4451f07 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/colors/SurfaceEffectColors.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/colors/SurfaceEffectColors.kt
@@ -16,23 +16,27 @@
package com.android.systemui.common.shared.colors
-import android.content.res.Resources
+import android.content.Context
object SurfaceEffectColors {
@JvmStatic
- fun surfaceEffect0(r: Resources): Int {
- return r.getColor(com.android.internal.R.color.surface_effect_0)
+ fun surfaceEffect0(context: Context): Int {
+ return context.resources.getColor(
+ com.android.internal.R.color.surface_effect_0, context.theme)
}
@JvmStatic
- fun surfaceEffect1(r: Resources): Int {
- return r.getColor(com.android.internal.R.color.surface_effect_1)
+ fun surfaceEffect1(context: Context): Int {
+ return context.resources.getColor(
+ com.android.internal.R.color.surface_effect_1, context.theme)
}
@JvmStatic
- fun surfaceEffect2(r: Resources): Int {
- return r.getColor(com.android.internal.R.color.surface_effect_2)
+ fun surfaceEffect2(context: Context): Int {
+ return context.resources.getColor(
+ com.android.internal.R.color.surface_effect_2, context.theme)
}
@JvmStatic
- fun surfaceEffect3(r: Resources): Int {
- return r.getColor(com.android.internal.R.color.surface_effect_3)
+ fun surfaceEffect3(context: Context): Int {
+ return context.resources.getColor(
+ com.android.internal.R.color.surface_effect_3, context.theme)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt
index 48a6d9d..7765d00 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt
@@ -16,7 +16,9 @@
package com.android.systemui.communal
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.CoreStartable
+import com.android.systemui.communal.dagger.CommunalModule.Companion.SHOW_UMO
import com.android.systemui.communal.data.repository.CommunalMediaRepository
import com.android.systemui.communal.data.repository.CommunalSmartspaceRepository
import com.android.systemui.communal.domain.interactor.CommunalInteractor
@@ -24,8 +26,8 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
+import javax.inject.Named
import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
@SysUISingleton
class CommunalOngoingContentStartable
@@ -36,6 +38,7 @@
private val communalMediaRepository: CommunalMediaRepository,
private val communalSettingsInteractor: CommunalSettingsInteractor,
private val communalSmartspaceRepository: CommunalSmartspaceRepository,
+ @Named(SHOW_UMO) private val showUmoOnHub: Boolean,
) : CoreStartable {
override fun start() {
@@ -46,10 +49,14 @@
bgScope.launch {
communalInteractor.isCommunalEnabled.collect { enabled ->
if (enabled) {
- communalMediaRepository.startListening()
+ if (showUmoOnHub) {
+ communalMediaRepository.startListening()
+ }
communalSmartspaceRepository.startListening()
} else {
- communalMediaRepository.stopListening()
+ if (showUmoOnHub) {
+ communalMediaRepository.stopListening()
+ }
communalSmartspaceRepository.stopListening()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
index ff74162..bb3be53 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
@@ -105,6 +105,7 @@
const val LOGGABLE_PREFIXES = "loggable_prefixes"
const val LAUNCHER_PACKAGE = "launcher_package"
const val SWIPE_TO_HUB = "swipe_to_hub"
+ const val SHOW_UMO = "show_umo"
@Provides
@Communal
@@ -150,5 +151,11 @@
fun provideSwipeToHub(@Main resources: Resources): Boolean {
return resources.getBoolean(R.bool.config_swipeToOpenGlanceableHub)
}
+
+ @Provides
+ @Named(SHOW_UMO)
+ fun provideShowUmo(@Main resources: Resources): Boolean {
+ return resources.getBoolean(R.bool.config_showUmoOnHub)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 2650159..15a4722 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -29,8 +29,6 @@
import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod
import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
@@ -52,8 +50,6 @@
NotificationMinimalism.token dependsOn NotificationThrottleHun.token
ModesEmptyShadeFix.token dependsOn modesUi
- PromotedNotificationUiAod.token dependsOn PromotedNotificationUi.token
-
// SceneContainer dependencies
SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
index 74d471c..e119ec9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
@@ -101,7 +101,7 @@
if (Flags.notificationShadeBlur()) {
transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
} else {
- emptyFlow()
+ transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx)
},
flowWhenShadeIsNotExpanded =
transitionAnimation.sharedFlow(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
index 3c126aa..f14a5a2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
@@ -51,7 +51,7 @@
if (Flags.notificationShadeBlur()) {
transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
} else {
- emptyFlow()
+ transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx)
},
flowWhenShadeIsNotExpanded =
transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
index 5a111aa..4a39421 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
@@ -32,7 +32,6 @@
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.emptyFlow
/**
* Breaks down PRIMARY BOUNCER->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -81,7 +80,7 @@
if (Flags.notificationShadeBlur()) {
transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
} else {
- emptyFlow()
+ transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx)
},
flowWhenShadeIsNotExpanded =
transitionAnimation.sharedFlow(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt
index 0f0e7b6..31b20a7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt
@@ -27,7 +27,6 @@
import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.emptyFlow
@SysUISingleton
class PrimaryBouncerToOccludedTransitionViewModel
@@ -51,7 +50,7 @@
if (Flags.notificationShadeBlur()) {
transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
} else {
- emptyFlow()
+ transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx)
},
flowWhenShadeIsNotExpanded =
transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx),
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt
index c9716be..34f7c4d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt
@@ -18,9 +18,6 @@
import android.animation.Animator
import android.animation.ObjectAnimator
-import android.icu.text.MeasureFormat
-import android.icu.util.Measure
-import android.icu.util.MeasureUnit
import android.text.format.DateUtils
import androidx.annotation.UiThread
import androidx.lifecycle.Observer
@@ -31,11 +28,8 @@
import com.android.systemui.media.controls.ui.view.MediaViewHolder
import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
import com.android.systemui.res.R
-import java.util.Locale
private const val TAG = "SeekBarObserver"
-private const val MIN_IN_SEC = 60
-private const val HOUR_IN_SEC = MIN_IN_SEC * 60
/**
* Observer for changes from SeekBarViewModel.
@@ -133,9 +127,10 @@
}
holder.seekBar.setMax(data.duration)
- val totalTimeDescription = formatTimeContentDescription(data.duration)
+ val totalTimeString =
+ DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS)
if (data.scrubbing) {
- holder.scrubbingTotalTimeView.text = formatTimeLabel(data.duration)
+ holder.scrubbingTotalTimeView.text = totalTimeString
}
data.elapsedTime?.let {
@@ -153,62 +148,20 @@
}
}
- val elapsedTimeDescription = formatTimeContentDescription(it)
+ val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS)
if (data.scrubbing) {
- holder.scrubbingElapsedTimeView.text = formatTimeLabel(it)
+ holder.scrubbingElapsedTimeView.text = elapsedTimeString
}
holder.seekBar.contentDescription =
holder.seekBar.context.getString(
R.string.controls_media_seekbar_description,
- elapsedTimeDescription,
- totalTimeDescription,
+ elapsedTimeString,
+ totalTimeString
)
}
}
- /** Returns a time string suitable for display, e.g. "12:34" */
- private fun formatTimeLabel(milliseconds: Int): CharSequence {
- return DateUtils.formatElapsedTime(milliseconds / DateUtils.SECOND_IN_MILLIS)
- }
-
- /**
- * Returns a time string suitable for content description, e.g. "12 minutes 34 seconds"
- *
- * Follows same logic as Chronometer#formatDuration
- */
- private fun formatTimeContentDescription(milliseconds: Int): CharSequence {
- var seconds = milliseconds / DateUtils.SECOND_IN_MILLIS
-
- val hours =
- if (seconds >= HOUR_IN_SEC) {
- seconds / HOUR_IN_SEC
- } else {
- 0
- }
- seconds -= hours * HOUR_IN_SEC
-
- val minutes =
- if (seconds >= MIN_IN_SEC) {
- seconds / MIN_IN_SEC
- } else {
- 0
- }
- seconds -= minutes * MIN_IN_SEC
-
- val measures = arrayListOf<Measure>()
- if (hours > 0) {
- measures.add(Measure(hours, MeasureUnit.HOUR))
- }
- if (minutes > 0) {
- measures.add(Measure(minutes, MeasureUnit.MINUTE))
- }
- measures.add(Measure(seconds, MeasureUnit.SECOND))
-
- return MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
- .formatMeasures(*measures.toTypedArray())
- }
-
@VisibleForTesting
open fun buildResetAnimator(targetTime: Int): Animator {
val animator =
@@ -216,7 +169,7 @@
holder.seekBar,
"progress",
holder.seekBar.progress,
- targetTime + RESET_ANIMATION_DURATION_MS,
+ targetTime + RESET_ANIMATION_DURATION_MS
)
animator.setAutoCancel(true)
animator.duration = RESET_ANIMATION_DURATION_MS.toLong()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
index 609541b..c70a854 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
@@ -20,6 +20,7 @@
import android.content.Context
import android.content.Intent
import android.os.UserHandle
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.Dumpable
import com.android.systemui.ProtoDumpable
import com.android.systemui.dagger.SysUISingleton
@@ -62,7 +63,6 @@
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
-import com.android.app.tracing.coroutines.launchTraced as launch
import kotlinx.coroutines.withContext
/**
@@ -245,7 +245,6 @@
processExistingTile(
tileSpec,
specsToTiles.getValue(tileSpec),
- userChanged,
newUser,
) ?: createTile(tileSpec)
} else {
@@ -378,7 +377,6 @@
private fun processExistingTile(
tileSpec: TileSpec,
tileOrNotInstalled: TileOrNotInstalled,
- userChanged: Boolean,
user: Int,
): QSTile? {
return when (tileOrNotInstalled) {
@@ -386,6 +384,10 @@
is TileOrNotInstalled.Tile -> {
val qsTile = tileOrNotInstalled.tile
when {
+ qsTile.isDestroyed -> {
+ logger.logTileDestroyedIgnored(tileSpec)
+ null
+ }
!qsTile.isAvailable -> {
logger.logTileDestroyed(
tileSpec,
@@ -399,10 +401,11 @@
qsTile !is CustomTile -> {
// The tile is not a custom tile. Make sure they are reset to the correct
// user
- if (userChanged) {
+ if (qsTile.currentTileUser != user) {
qsTile.userSwitch(user)
logger.logTileUserChanged(tileSpec, user)
}
+
qsTile
}
qsTile.user == user -> {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
index e237ca9..21a8ec6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
@@ -60,7 +60,7 @@
bool1 = usesDefault
int1 = user
},
- { "Parsed tiles (default=$bool1, user=$int1): $str1" }
+ { "Parsed tiles (default=$bool1, user=$int1): $str1" },
)
}
@@ -77,7 +77,7 @@
str2 = reconciledTiles.toString()
int1 = user
},
- { "Tiles restored and reconciled for user: $int1\nWas: $str1\nSet to: $str2" }
+ { "Tiles restored and reconciled for user: $int1\nWas: $str1\nSet to: $str2" },
)
}
@@ -94,7 +94,7 @@
str2 = newList.toString()
int1 = userId
},
- { "Processing $str1 for user $int1\nNew list: $str2" }
+ { "Processing $str1 for user $int1\nNew list: $str2" },
)
}
@@ -107,7 +107,16 @@
str1 = spec.toString()
str2 = reason.readable
},
- { "Tile $str1 destroyed. Reason: $str2" }
+ { "Tile $str1 destroyed. Reason: $str2" },
+ )
+ }
+
+ fun logTileDestroyedIgnored(spec: TileSpec) {
+ tileListLogBuffer.log(
+ TILE_LIST_TAG,
+ LogLevel.DEBUG,
+ { str1 = spec.toString() },
+ { "Tile $str1 ignored as it was already destroyed." },
)
}
@@ -117,7 +126,7 @@
TILE_LIST_TAG,
LogLevel.DEBUG,
{ str1 = spec.toString() },
- { "Tile $str1 created" }
+ { "Tile $str1 created" },
)
}
@@ -127,7 +136,7 @@
TILE_LIST_TAG,
LogLevel.VERBOSE,
{ str1 = spec.toString() },
- { "Tile $str1 not found in factory" }
+ { "Tile $str1 not found in factory" },
)
}
@@ -140,7 +149,7 @@
str1 = spec.toString()
int1 = user
},
- { "User changed to $int1 for tile $str1" }
+ { "User changed to $int1 for tile $str1" },
)
}
@@ -156,7 +165,7 @@
str1 = tiles.toString()
int1 = user
},
- { "Tiles kept for not installed packages for user $int1: $str1" }
+ { "Tiles kept for not installed packages for user $int1: $str1" },
)
}
@@ -168,7 +177,7 @@
str1 = tiles.toString()
int1 = userId
},
- { "Auto add tiles parsed for user $int1: $str1" }
+ { "Auto add tiles parsed for user $int1: $str1" },
)
}
@@ -180,7 +189,7 @@
str1 = tiles.toString()
int1 = userId
},
- { "Auto-add tiles reconciled for user $int1: $str1" }
+ { "Auto-add tiles reconciled for user $int1: $str1" },
)
}
@@ -193,7 +202,7 @@
int2 = position
str1 = spec.toString()
},
- { "Tile $str1 auto added for user $int1 at position $int2" }
+ { "Tile $str1 auto added for user $int1 at position $int2" },
)
}
@@ -205,7 +214,7 @@
int1 = userId
str1 = spec.toString()
},
- { "Tile $str1 auto removed for user $int1" }
+ { "Tile $str1 auto removed for user $int1" },
)
}
@@ -217,7 +226,7 @@
int1 = userId
str1 = spec.toString()
},
- { "Tile $str1 unmarked as auto-added for user $int1" }
+ { "Tile $str1 unmarked as auto-added for user $int1" },
)
}
@@ -226,7 +235,7 @@
RESTORE_TAG,
LogLevel.DEBUG,
{ int1 = userId },
- { "Restored from single intent after user setup complete for user $int1" }
+ { "Restored from single intent after user setup complete for user $int1" },
)
}
@@ -243,7 +252,7 @@
"Restored settings data for user $int1\n" +
"\tRestored tiles: $str1\n" +
"\tRestored auto added tiles: $str2"
- }
+ },
)
}
@@ -258,7 +267,7 @@
str1 = restoreProcessorClassName
str2 = step.name
},
- { "Restore $str2 processed by $str1" }
+ { "Restore $str2 processed by $str1" },
)
}
@@ -273,6 +282,6 @@
enum class RestorePreprocessorStep {
PREPROCESSING,
- POSTPROCESSING
+ POSTPROCESSING,
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
index 1d1e991..c6fc868 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -74,6 +74,8 @@
import java.io.PrintWriter;
import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* Base quick-settings tile, extend this to create a new tile.
@@ -127,6 +129,8 @@
private int mIsFullQs;
private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
+ private final AtomicBoolean mIsDestroyed = new AtomicBoolean(false);
+ private final AtomicInteger mCurrentTileUser = new AtomicInteger();
/**
* Provides a new {@link TState} of the appropriate type to use between this tile and the
@@ -203,6 +207,7 @@
mMetricsLogger = metricsLogger;
mStatusBarStateController = statusBarStateController;
mActivityStarter = activityStarter;
+ mCurrentTileUser.set(host.getUserId());
resetStates();
mUiHandler.post(() -> mLifecycle.setCurrentState(CREATED));
@@ -352,11 +357,19 @@
}
public void userSwitch(int newUserId) {
+ mCurrentTileUser.set(newUserId);
mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget();
postStale();
}
+ @Override
+ public int getCurrentTileUser() {
+ return mCurrentTileUser.get();
+ }
+
public void destroy() {
+ // We mark it as soon as we start the destroy process, as nothing can interrupt it.
+ mIsDestroyed.set(true);
mHandler.sendEmptyMessage(H.DESTROY);
}
@@ -365,7 +378,7 @@
*
* Should be called upon creation of the tile, before performing other operations
*/
- public void initialize() {
+ public final void initialize() {
mHandler.sendEmptyMessage(H.INITIALIZE);
}
@@ -525,6 +538,11 @@
});
}
+ @Override
+ public final boolean isDestroyed() {
+ return mIsDestroyed.get();
+ }
+
protected void checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction) {
EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
userRestriction, mHost.getUserId());
@@ -799,7 +817,7 @@
*/
@Override
public void dump(PrintWriter pw, String[] args) {
- pw.println(this.getClass().getSimpleName() + ":");
+ pw.print(this.getClass().getSimpleName() + ":");
pw.print(" "); pw.println(getState().toString());
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
index f80b8fb..e48e943 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
@@ -99,7 +99,7 @@
}
override fun getDetailsViewModel(): TileDetailsViewModel {
- return internetDetailsViewModelFactory.create { longClick(null) }
+ return internetDetailsViewModelFactory.create()
}
override fun handleUpdateState(state: QSTile.BooleanState, arg: Any?) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
index e8c4274..8ad4e16 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
@@ -28,17 +28,4 @@
* It's safe to run long running computations inside this function.
*/
@WorkerThread suspend fun handleInput(input: QSTileInput<DATA_TYPE>)
-
- /**
- * Provides the [TileDetailsViewModel] for constructing the corresponding details view.
- *
- * This property is defined here to reuse the business logic. For example, reusing the user
- * long-click as the go-to-settings callback in the details view.
- * Subclasses can override this property to provide a specific [TileDetailsViewModel]
- * implementation.
- *
- * @return The [TileDetailsViewModel] instance, or null if not implemented.
- */
- val detailsViewModel: TileDetailsViewModel?
- get() = null
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
index 8c75cf0..7f475f3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
@@ -19,6 +19,7 @@
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor
@@ -70,9 +71,7 @@
* Creates [QSTileViewModelImpl] based on the interactors obtained from [QSTileComponent].
* Reference of that [QSTileComponent] is then stored along the view model.
*/
- fun create(
- tileSpec: TileSpec,
- ): QSTileViewModel {
+ fun create(tileSpec: TileSpec): QSTileViewModel {
val config = qsTileConfigProvider.getConfig(tileSpec.spec)
val component =
customTileComponentBuilder.qsTileConfigModule(QSTileConfigModule(config)).build()
@@ -90,6 +89,7 @@
backgroundDispatcher,
uiBackgroundDispatcher,
component.coroutineScope(),
+ /* tileDetailsViewModel= */ null,
)
}
}
@@ -127,6 +127,7 @@
userActionInteractor: QSTileUserActionInteractor<T>,
tileDataInteractor: QSTileDataInteractor<T>,
mapper: QSTileDataToStateMapper<T>,
+ tileDetailsViewModel: TileDetailsViewModel? = null,
): QSTileViewModelImpl<T> =
QSTileViewModelImpl(
qsTileConfigProvider.getConfig(tileSpec.spec),
@@ -142,6 +143,7 @@
backgroundDispatcher,
uiBackgroundDispatcher,
coroutineScopeFactory.create(),
+ tileDetailsViewModel,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
index 30bf5b3..3866c17 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
@@ -83,6 +83,7 @@
private val backgroundDispatcher: CoroutineDispatcher,
uiBackgroundDispatcher: CoroutineDispatcher,
private val tileScope: CoroutineScope,
+ override val tileDetailsViewModel: TileDetailsViewModel? = null,
) : QSTileViewModel, Dumpable {
private val users: MutableStateFlow<UserHandle> =
@@ -96,6 +97,9 @@
private val tileData: SharedFlow<DATA_TYPE?> = createTileDataFlow()
+ override val currentTileUser: Int
+ get() = users.value.identifier
+
override val state: StateFlow<QSTileState?> =
tileData
.map { data ->
@@ -114,9 +118,6 @@
.flowOn(backgroundDispatcher)
.stateIn(tileScope, SharingStarted.WhileSubscribed(), true)
- override val detailsViewModel: TileDetailsViewModel?
- get() = userActionInteractor().detailsViewModel
-
override fun forceUpdate() {
tileScope.launch(context = backgroundDispatcher) { forceUpdates.emit(Unit) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
index 0ed56f6..6709fd2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
@@ -16,9 +16,11 @@
package com.android.systemui.qs.tiles.dialog
+import android.content.Intent
+import android.provider.Settings
import com.android.systemui.plugins.qs.TileDetailsViewModel
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
import com.android.systemui.statusbar.connectivity.AccessPointController
-import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -27,10 +29,13 @@
constructor(
private val accessPointController: AccessPointController,
val contentManagerFactory: InternetDetailsContentManager.Factory,
- @Assisted private val onLongClick: () -> Unit,
+ private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
) : TileDetailsViewModel() {
override fun clickOnSettingsButton() {
- onLongClick()
+ qsTileIntentUserActionHandler.handle(
+ /* expandable= */ null,
+ Intent(Settings.ACTION_WIFI_SETTINGS),
+ )
}
override fun getTitle(): String {
@@ -58,7 +63,7 @@
}
@AssistedFactory
- interface Factory {
- fun create(onLongClick: () -> Unit): InternetDetailsViewModel
+ fun interface Factory {
+ fun create(): InternetDetailsViewModel
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
index 0adc4131..8d4a24e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
@@ -400,6 +400,9 @@
mInternetDialogTitle.setText(internetContent.mInternetDialogTitleString);
mInternetDialogSubTitle.setText(internetContent.mInternetDialogSubTitle);
+ if (!internetContent.mIsWifiEnabled) {
+ setProgressBarVisible(false);
+ }
mAirplaneModeButton.setVisibility(
internetContent.mIsAirplaneModeEnabled ? View.VISIBLE : View.GONE);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt
index 0ed46e7..5f692f2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt
@@ -16,6 +16,7 @@
package com.android.systemui.qs.tiles.impl.di
+import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
index 8e48fe4..0431e36 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
@@ -18,13 +18,10 @@
import android.content.Intent
import android.provider.Settings
-import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.interactor.QSTileInput
import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
-import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
import com.android.systemui.qs.tiles.dialog.InternetDialogManager
import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
@@ -41,7 +38,6 @@
private val internetDialogManager: InternetDialogManager,
private val accessPointController: AccessPointController,
private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
- private val internetDetailsViewModelFactory: InternetDetailsViewModel.Factory,
) : QSTileUserActionInteractor<InternetTileModel> {
override suspend fun handleInput(input: QSTileInput<InternetTileModel>): Unit =
@@ -58,16 +54,12 @@
}
}
is QSTileUserAction.LongClick -> {
- handleLongClick(action.expandable)
+ qsTileIntentUserActionHandler.handle(
+ action.expandable,
+ Intent(Settings.ACTION_WIFI_SETTINGS),
+ )
}
else -> {}
}
}
-
- override val detailsViewModel: TileDetailsViewModel =
- internetDetailsViewModelFactory.create { handleLongClick(null) }
-
- private fun handleLongClick(expandable: Expandable?) {
- qsTileIntentUserActionHandler.handle(expandable, Intent(Settings.ACTION_WIFI_SETTINGS))
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
index e8b9926..7a53388 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
@@ -39,9 +39,12 @@
val isAvailable: StateFlow<Boolean>
/** Specifies the [TileDetailsViewModel] for constructing the corresponding details view. */
- val detailsViewModel: TileDetailsViewModel?
+ val tileDetailsViewModel: TileDetailsViewModel?
get() = null
+ /** Returns the current user for this tile */
+ val currentTileUser: Int
+
/**
* Notifies about the user change. Implementations should avoid using 3rd party userId sources
* and use this value instead. This is to maintain consistent and concurrency-free behaviour
@@ -65,8 +68,6 @@
fun destroy()
}
-/**
- * Returns the immediate state of the tile or null if the state haven't been collected yet.
- */
+/** Returns the immediate state of the tile or null if the state haven't been collected yet. */
val QSTileViewModel.currentState: QSTileState?
get() = state.value
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 30d1f05..e607eae 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -19,7 +19,6 @@
import android.content.Context
import android.os.UserHandle
import android.util.Log
-import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.logging.InstanceId
import com.android.systemui.Dumpable
import com.android.systemui.animation.Expandable
@@ -156,8 +155,12 @@
qsTileViewModel.onUserChanged(UserHandle.of(currentUser))
}
+ override fun getCurrentTileUser(): Int {
+ return qsTileViewModel.currentTileUser
+ }
+
override fun getDetailsViewModel(): TileDetailsViewModel? {
- return qsTileViewModel.detailsViewModel
+ return qsTileViewModel.tileDetailsViewModel
}
@Deprecated(
@@ -213,6 +216,10 @@
qsTileViewModel.destroy()
}
+ override fun isDestroyed(): Boolean {
+ return !(tileAdapterJob?.isActive ?: false)
+ }
+
override fun getState(): QSTile.AdapterState =
qsTileViewModel.currentState?.let { mapState(context, it, qsTileViewModel.config) }
?: QSTile.AdapterState()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/StubQSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/StubQSTileViewModel.kt
index 00b7e61..bdd5c73 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/StubQSTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/StubQSTileViewModel.kt
@@ -37,4 +37,7 @@
override fun onActionPerformed(userAction: QSTileUserAction) = error("Don't call stubs")
override fun destroy() = error("Don't call stubs")
+
+ override val currentTileUser: Int
+ get() = error("Don't call stubs")
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 7dc2ae7..e44701d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -580,7 +580,8 @@
/**
* @see IStatusBar#immersiveModeChanged
*/
- default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {}
+ default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode,
+ int windowType) {}
/**
* @see IStatusBar#moveFocusedTaskToDesktop(int)
@@ -876,11 +877,13 @@
}
@Override
- public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+ public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode,
+ int windowType) {
synchronized (mLock) {
final SomeArgs args = SomeArgs.obtain();
args.argi1 = rootDisplayAreaId;
args.argi2 = isImmersiveMode ? 1 : 0;
+ args.argi3 = windowType;
mHandler.obtainMessage(MSG_IMMERSIVE_CHANGED, args).sendToTarget();
}
}
@@ -2030,8 +2033,10 @@
args = (SomeArgs) msg.obj;
int rootDisplayAreaId = args.argi1;
boolean isImmersiveMode = args.argi2 != 0;
+ int windowType = args.argi3;
for (int i = 0; i < mCallbacks.size(); i++) {
- mCallbacks.get(i).immersiveModeChanged(rootDisplayAreaId, isImmersiveMode);
+ mCallbacks.get(i).immersiveModeChanged(rootDisplayAreaId, isImmersiveMode,
+ windowType);
}
break;
case MSG_ENTER_DESKTOP: {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
index fed3f6e..97e62d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
@@ -23,6 +23,8 @@
import static android.app.StatusBarManager.DISABLE_RECENT;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
+import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;
import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
@@ -208,7 +210,8 @@
}
@Override
- public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+ public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode,
+ int windowType) {
mHandler.removeMessages(H.SHOW);
if (isImmersiveMode) {
if (DEBUG) Log.d(TAG, "immersiveModeChanged() sConfirmed=" + sConfirmed);
@@ -221,7 +224,9 @@
&& mCanSystemBarsBeShownByUser
&& !mNavBarEmpty
&& !UserManager.isDeviceInDemoMode(mDisplayContext)
- && (mLockTaskState != LOCK_TASK_MODE_LOCKED)) {
+ && (mLockTaskState != LOCK_TASK_MODE_LOCKED)
+ && windowType != TYPE_PRESENTATION
+ && windowType != TYPE_PRIVATE_PRESENTATION) {
final Message msg = mHandler.obtainMessage(
H.SHOW);
msg.arg1 = rootDisplayAreaId;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
index 10090283..48f0245 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
@@ -34,6 +34,7 @@
import com.android.systemui.qs.tiles.NfcTile
import com.android.systemui.qs.tiles.base.interactor.QSTileAvailabilityInteractor
import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
+import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
import com.android.systemui.qs.tiles.impl.airplane.domain.AirplaneModeMapper
import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileDataInteractor
import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileUserActionInteractor
@@ -162,13 +163,15 @@
factory: QSTileViewModelFactory.Static<AirplaneModeTileModel>,
mapper: AirplaneModeMapper,
stateInteractor: AirplaneModeTileDataInteractor,
- userActionInteractor: AirplaneModeTileUserActionInteractor
+ userActionInteractor: AirplaneModeTileUserActionInteractor,
+ internetDetailsViewModelFactory: InternetDetailsViewModel.Factory
): QSTileViewModel =
factory.create(
TileSpec.create(AIRPLANE_MODE_TILE_SPEC),
userActionInteractor,
stateInteractor,
mapper,
+ internetDetailsViewModelFactory.create(),
)
@Provides
@@ -226,13 +229,15 @@
factory: QSTileViewModelFactory.Static<InternetTileModel>,
mapper: InternetTileMapper,
stateInteractor: InternetTileDataInteractor,
- userActionInteractor: InternetTileUserActionInteractor
+ userActionInteractor: InternetTileUserActionInteractor,
+ internetDetailsViewModelFactory: InternetDetailsViewModel.Factory
): QSTileViewModel =
factory.create(
TileSpec.create(INTERNET_TILE_SPEC),
userActionInteractor,
stateInteractor,
mapper,
+ internetDetailsViewModelFactory.create(),
)
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt
index 243a868..874a059 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt
@@ -26,6 +26,7 @@
import com.android.systemui.statusbar.notification.headsup.HeadsUpManager
import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
import kotlin.math.ceil
import kotlin.math.max
@@ -117,7 +118,7 @@
params.startNotificationTop = location[1]
params.notificationParentTop =
notificationListContainer
- .getViewParentForNotification(notificationEntry)
+ .getViewParentForNotification()
.locationOnScreen[1]
params.startRoundedTopClipping = roundedTopClipping
params.startClipTopAmount = notification.clipTopAmount
@@ -148,7 +149,7 @@
Log.d(TAG, reason)
}
notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(willAnimate)
- notificationEntry.isExpandAnimationRunning = willAnimate
+ notification.isLaunchAnimationRunning = willAnimate
if (!willAnimate) {
removeHun(animate = true, reason)
@@ -158,7 +159,8 @@
private val headsUpNotificationRow: ExpandableNotificationRow?
get() {
- val pipelineParent = notificationEntry.parent
+ val pipelineParent = if (NotificationBundleUi.isEnabled)
+ notification.entryAdapter?.parent else notificationEntry.parent
val summaryEntry = (pipelineParent as? GroupEntry)?.summary
return when {
headsUpManager.isHeadsUpEntry(notificationKey) -> notification
@@ -190,7 +192,7 @@
// TODO(b/184121838): Should we call InteractionJankMonitor.cancel if the animation started
// here?
notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(false)
- notificationEntry.isExpandAnimationRunning = false
+ notification.isLaunchAnimationRunning = false
removeHun(animate = true, "onLaunchAnimationCancelled()")
onFinishAnimationCallback?.run()
}
@@ -210,7 +212,7 @@
notification.isExpandAnimationRunning = false
notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(false)
- notificationEntry.isExpandAnimationRunning = false
+ notification.isLaunchAnimationRunning = false
notificationListContainer.setExpandingNotification(null)
applyParams(null)
removeHun(animate = false, "onLaunchAnimationEnd()")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index 9ed1632..c2f0806 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -136,9 +136,7 @@
@NonNull NotifInflater.Params params,
NotificationRowContentBinder.InflationCallback callback)
throws InflationException {
- //TODO(b/217799515): Remove the entry parameter from getViewParentForNotification(), this
- // function returns the NotificationStackScrollLayout regardless of the entry.
- ViewGroup parent = mListContainer.getViewParentForNotification(entry);
+ ViewGroup parent = mListContainer.getViewParentForNotification();
if (entry.rowExists()) {
mLogger.logUpdatingRow(entry, params);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index 25deec3..d09546f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -391,7 +391,7 @@
if (!notificationFooterBackgroundTintOptimization()) {
if (notificationShadeBlur()) {
Color backgroundColor = Color.valueOf(
- SurfaceEffectColors.surfaceEffect1(getResources()));
+ SurfaceEffectColors.surfaceEffect1(getContext()));
scHigh = ColorUtils.setAlphaComponent(backgroundColor.toArgb(), 0xFF);
// Apply alpha on background drawables.
int backgroundAlpha = (int) (backgroundColor.alpha() * 0xFF);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
index 7c5f3b5..5157e7a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
@@ -332,7 +332,9 @@
onEntryAdded(headsUpEntry, requestedPinnedStatus);
// TODO(b/328390331) move accessibility events to the view layer
entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
- entry.setIsHeadsUpEntry(true);
+ if (!NotificationBundleUi.isEnabled()) {
+ entry.setIsHeadsUpEntry(true);
+ }
updateNotificationInternal(entry.getKey(), requestedPinnedStatus);
entry.setInterruption();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 4ed9dce..a081ad5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -129,7 +129,7 @@
private void updateColors() {
if (notificationRowTransparency()) {
- mNormalColor = SurfaceEffectColors.surfaceEffect1(getResources());
+ mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext());
} else {
mNormalColor = mContext.getColor(
com.android.internal.R.color.materialColorSurfaceContainerHigh);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
index 6bfc9f0..4bd6053 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
@@ -21,7 +21,6 @@
import android.app.NotificationChannel
import android.app.NotificationChannel.DEFAULT_CHANNEL_ID
import android.app.NotificationChannelGroup
-import android.app.NotificationManager.IMPORTANCE_NONE
import android.app.NotificationManager.Importance
import android.content.Context
import android.graphics.Color
@@ -40,7 +39,7 @@
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.res.R
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.shade.ShadeDisplayAware
+import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor
import javax.inject.Inject
private const val TAG = "ChannelDialogController"
@@ -59,9 +58,9 @@
*/
@SysUISingleton
class ChannelEditorDialogController @Inject constructor(
- @ShadeDisplayAware private val context: Context,
+ private val shadeDialogContextInteractor: ShadeDialogContextInteractor,
private val noMan: INotificationManager,
- private val dialogBuilder: ChannelEditorDialog.Builder
+ private val dialogBuilder: ChannelEditorDialog.Builder,
) {
private var prepared = false
@@ -272,7 +271,7 @@
}
private fun initDialog() {
- dialogBuilder.setContext(context)
+ dialogBuilder.setContext(shadeDialogContextInteractor.context)
dialog = dialogBuilder.build()
dialog.window?.requestFeature(Window.FEATURE_NO_TITLE)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 6134d1d..185e7fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -300,6 +300,7 @@
private boolean mIsSystemChildExpanded;
private PinnedStatus mPinnedStatus = PinnedStatus.NotPinned;
private boolean mExpandAnimationRunning;
+ private boolean mLaunchAnimationRunning;
private AboveShelfChangedListener mAboveShelfChangedListener;
private HeadsUpManager mHeadsUpManager;
private Consumer<Boolean> mHeadsUpAnimatingAwayListener;
@@ -4487,4 +4488,22 @@
}
mLogger.logRemoveTransientRow(row.getLoggingKey(), mLoggingKey);
}
+
+ /** Set whether this notification is currently used to animate a launch. */
+ public void setLaunchAnimationRunning(boolean launchAnimationRunning) {
+ if (NotificationBundleUi.isEnabled()) {
+ mLaunchAnimationRunning = launchAnimationRunning;
+ } else {
+ getEntry().setExpandAnimationRunning(launchAnimationRunning);
+ }
+ }
+
+ /** Whether this notification is currently used to animate a launch. */
+ public boolean isLaunchAnimationRunning() {
+ if (NotificationBundleUi.isEnabled()) {
+ return mLaunchAnimationRunning;
+ } else {
+ return getEntry().isExpandAnimationRunning();
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
index 33c36d8c..c0bc132 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
@@ -88,7 +88,7 @@
mDarkColoredStatefulColors = getResources().getColorStateList(
R.color.notification_state_color_dark);
if (notificationRowTransparency()) {
- mNormalColor = SurfaceEffectColors.surfaceEffect1(getResources());
+ mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext());
} else {
mNormalColor = mContext.getColor(
com.android.internal.R.color.materialColorSurfaceContainerHigh);
@@ -321,7 +321,7 @@
new PorterDuffColorFilter(
isColorized()
? ColorUtils.setAlphaComponent(mTintColor, (int) (255 * 0.9f))
- : SurfaceEffectColors.surfaceEffect1(getResources()),
+ : SurfaceEffectColors.surfaceEffect1(getContext()),
PorterDuff.Mode.SRC)); // SRC operator discards the drawable's color+alpha
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
index ab382df..e89a76f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.notification.row;
-import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS;
+import static android.app.Flags.notificationsRedesignTemplates;
import static android.view.HapticFeedbackConstants.CLOCK_TICK;
import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION;
@@ -706,8 +706,11 @@
static NotificationMenuItem createInfoItem(Context context) {
Resources res = context.getResources();
String infoDescription = res.getString(R.string.notification_menu_gear_description);
+ int layoutId = notificationsRedesignTemplates()
+ ? R.layout.notification_2025_info
+ : R.layout.notification_info;
NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
- R.layout.notification_info, null, false);
+ layoutId, null, false);
return new NotificationMenuItem(context, infoDescription, infoContent,
R.drawable.ic_settings);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 64ca815..1e24952 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -35,8 +35,10 @@
import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.BypassController;
import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.SectionProvider;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
@@ -60,6 +62,7 @@
private final BypassController mBypassController;
private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
private final AvalancheController mAvalancheController;
+ private final HeadsUpRepository mHeadsUpRepository;
/**
* Used to read bouncer states.
@@ -304,6 +307,7 @@
@NonNull BypassController bypassController,
@Nullable StatusBarKeyguardViewManager statusBarKeyguardViewManager,
@NonNull LargeScreenShadeInterpolator largeScreenShadeInterpolator,
+ @NonNull HeadsUpRepository headsUpRepository,
AvalancheController avalancheController
) {
mSectionProvider = sectionProvider;
@@ -311,6 +315,7 @@
mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mLargeScreenShadeInterpolator = largeScreenShadeInterpolator;
mAvalancheController = avalancheController;
+ mHeadsUpRepository = headsUpRepository;
reload(context);
dumpManager.registerDumpable(this);
}
@@ -690,7 +695,10 @@
}
public boolean isPulsing(NotificationEntry entry) {
- return mPulsing && entry.isHeadsUpEntry();
+ boolean isHeadsUp = NotificationBundleUi.isEnabled()
+ ? mHeadsUpRepository.isHeadsUpEntry(entry.getKey())
+ : entry.isHeadsUpEntry();
+ return mPulsing && isHeadsUp;
}
public void setPulsingRow(ExpandableNotificationRow row) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
index da98858..9bd5a5b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
@@ -291,7 +291,7 @@
* currently being swiped. From the center outwards, the multipliers apply to the neighbors
* of the swiped view.
*/
- private val MAGNETIC_TRANSLATION_MULTIPLIERS = listOf(0.18f, 0.28f, 0.5f, 0.28f, 0.18f)
+ private val MAGNETIC_TRANSLATION_MULTIPLIERS = listOf(0.04f, 0.12f, 0.5f, 0.12f, 0.04f)
const val MAGNETIC_REDUCTION = 0.65f
@@ -299,7 +299,7 @@
private const val DETACH_STIFFNESS = 800f
private const val DETACH_DAMPING_RATIO = 0.95f
private const val SNAP_BACK_STIFFNESS = 550f
- private const val SNAP_BACK_DAMPING_RATIO = 0.52f
+ private const val SNAP_BACK_DAMPING_RATIO = 0.6f
// Maximum value of corner roundness that gets applied during the pre-detach dragging
private const val MAX_PRE_DETACH_ROUNDNESS = 0.8f
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java
index f85545e..47fc2fb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java
@@ -113,10 +113,9 @@
/**
* Get the view parent for a notification entry. For example, NotificationStackScrollLayout.
*
- * @param entry entry to get the view parent for
* @return the view parent for entry
*/
- ViewGroup getViewParentForNotification(NotificationEntry entry);
+ ViewGroup getViewParentForNotification();
/**
* Resets the currently exposed menu view.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 6313258..e7a77eb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -2134,7 +2134,7 @@
}
}
- public ViewGroup getViewParentForNotification(NotificationEntry entry) {
+ public ViewGroup getViewParentForNotification() {
return this;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 5c96470..4459430 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -1908,8 +1908,8 @@
}
@Override
- public ViewGroup getViewParentForNotification(NotificationEntry entry) {
- return mView.getViewParentForNotification(entry);
+ public ViewGroup getViewParentForNotification() {
+ return mView.getViewParentForNotification();
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
index a0e3fbd..8b0f7c4 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
@@ -29,18 +29,13 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SliderDefaults
-import androidx.compose.material3.SliderState
-import androidx.compose.material3.VerticalSlider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
@@ -49,16 +44,17 @@
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.theme.PlatformTheme
import com.android.compose.ui.graphics.painter.DrawablePainter
+import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter
import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
-import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.res.R
import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
import com.android.systemui.volume.dialog.sliders.ui.compose.VolumeDialogSliderTrack
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
-import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
+import com.android.systemui.volume.ui.slider.AccessibilityParams
+import com.android.systemui.volume.ui.slider.Haptics
+import com.android.systemui.volume.ui.slider.Slider
import javax.inject.Inject
-import kotlin.math.round
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive
@@ -90,7 +86,7 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun VolumeDialogSlider(
viewModel: VolumeDialogSliderViewModel,
@@ -108,59 +104,8 @@
)
val collectedSliderStateModel by viewModel.state.collectAsStateWithLifecycle(null)
val sliderStateModel = collectedSliderStateModel ?: return
-
- val steps = with(sliderStateModel.valueRange) { endInclusive - start - 1 }.toInt()
-
val interactionSource = remember { MutableInteractionSource() }
- val hapticsViewModel: SliderHapticsViewModel? =
- hapticsViewModelFactory?.let {
- rememberViewModel(traceName = "SliderHapticsViewModel") {
- it.create(
- interactionSource,
- sliderStateModel.valueRange,
- Orientation.Vertical,
- VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(
- sliderStateModel.valueRange
- ),
- VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
- )
- }
- }
- val sliderState =
- remember(steps, sliderStateModel.valueRange) {
- SliderState(
- value = sliderStateModel.value,
- valueRange = sliderStateModel.valueRange,
- steps = steps,
- )
- .also { sliderState ->
- sliderState.onValueChangeFinished = {
- viewModel.onSliderChangeFinished(sliderState.value)
- hapticsViewModel?.onValueChangeEnded()
- }
- sliderState.onValueChange = { newValue ->
- sliderState.value = newValue
- hapticsViewModel?.addVelocityDataPoint(newValue)
- overscrollViewModel.setSlider(
- value = sliderState.value,
- min = sliderState.valueRange.start,
- max = sliderState.valueRange.endInclusive,
- )
- viewModel.setStreamVolume(newValue, true)
- }
- }
- }
-
- var lastDiscreteStep by remember { mutableFloatStateOf(round(sliderStateModel.value)) }
- LaunchedEffect(sliderStateModel.value) {
- val value = sliderStateModel.value
- sliderState.value = value
- if (value != lastDiscreteStep) {
- lastDiscreteStep = value
- hapticsViewModel?.onValueChange(value)
- }
- }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect {
when (it) {
@@ -171,24 +116,33 @@
}
}
- VerticalSlider(
- state = sliderState,
- enabled = !sliderStateModel.isDisabled,
- reverseDirection = true,
+ Slider(
+ value = sliderStateModel.value,
+ valueRange = sliderStateModel.valueRange,
+ onValueChanged = { value ->
+ overscrollViewModel.setSlider(
+ value = value,
+ min = sliderStateModel.valueRange.start,
+ max = sliderStateModel.valueRange.endInclusive,
+ )
+ viewModel.setStreamVolume(value, true)
+ },
+ onValueChangeFinished = { viewModel.onSliderChangeFinished(it) },
+ isEnabled = !sliderStateModel.isDisabled,
+ isReverseDirection = true,
+ isVertical = true,
colors = colors,
interactionSource = interactionSource,
- modifier =
- modifier.pointerInput(Unit) {
- coroutineScope {
- val currentContext = currentCoroutineContext()
- awaitPointerEventScope {
- while (currentContext.isActive) {
- viewModel.onTouchEvent(awaitPointerEvent())
- }
- }
- }
- },
- track = {
+ haptics =
+ hapticsViewModelFactory?.let {
+ Haptics.Enabled(
+ hapticsViewModelFactory = it,
+ hapticFilter = SliderHapticFeedbackFilter(),
+ orientation = Orientation.Vertical,
+ )
+ } ?: Haptics.Disabled,
+ stepDistance = 1f,
+ track = { sliderState ->
VolumeDialogSliderTrack(
sliderState,
colors = colors,
@@ -201,6 +155,19 @@
},
)
},
+ accessibilityParams =
+ AccessibilityParams(label = "", currentStateDescription = "", disabledMessage = ""),
+ modifier =
+ modifier.pointerInput(Unit) {
+ coroutineScope {
+ val currentContext = currentCoroutineContext()
+ awaitPointerEventScope {
+ while (currentContext.isActive) {
+ viewModel.onTouchEvent(awaitPointerEvent())
+ }
+ }
+ }
+ },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
index 3efb2b4..3d98eba 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
@@ -116,8 +116,8 @@
override val isEnabled: Boolean
get() = true
- override val a11yStep: Int
- get() = 1
+ override val a11yStep: Float
+ get() = 1f
override val disabledMessage: String?
get() = null
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index f9d776b..9d32285 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -165,7 +165,7 @@
label = label,
disabledMessage = disabledMessage,
isEnabled = isEnabled,
- a11yStep = volumeRange.step,
+ a11yStep = volumeRange.step.toFloat(),
a11yClickDescription =
if (isAffectedByMute) {
context.getString(
@@ -307,7 +307,7 @@
override val label: String,
override val disabledMessage: String?,
override val isEnabled: Boolean,
- override val a11yStep: Int,
+ override val a11yStep: Float,
override val a11yClickDescription: String?,
override val a11yStateDescription: String?,
override val isMutable: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
index d74a433..a6c8091 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
@@ -86,7 +86,7 @@
icon = Icon.Resource(R.drawable.ic_cast, null),
label = context.getString(R.string.media_device_cast),
isEnabled = true,
- a11yStep = 1,
+ a11yStep = 1f,
)
}
@@ -96,7 +96,7 @@
override val icon: Icon,
override val label: String,
override val isEnabled: Boolean,
- override val a11yStep: Int,
+ override val a11yStep: Float,
) : SliderState {
override val hapticFilter: SliderHapticFeedbackFilter
get() = SliderHapticFeedbackFilter()
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
index f135371..4bc237b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
@@ -36,7 +36,7 @@
* A11y slider controls works by adjusting one step up or down. The default slider step isn't
* enough to trigger rounding to the correct value.
*/
- val a11yStep: Int
+ val a11yStep: Float
val a11yClickDescription: String?
val a11yStateDescription: String?
val disabledMessage: String?
@@ -49,7 +49,7 @@
override val icon: Icon? = null
override val label: String = ""
override val disabledMessage: String? = null
- override val a11yStep: Int = 0
+ override val a11yStep: Float = 0f
override val a11yClickDescription: String? = null
override val a11yStateDescription: String? = null
override val isEnabled: Boolean = true
diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt
new file mode 100644
index 0000000..d3562e2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+
+package com.android.systemui.volume.ui.slider
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderColors
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.SliderState
+import androidx.compose.material3.VerticalSlider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.progressBarRangeInfo
+import androidx.compose.ui.semantics.setProgress
+import androidx.compose.ui.semantics.stateDescription
+import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter
+import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.res.R
+import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
+import kotlin.math.round
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+private val defaultSpring =
+ SpringSpec<Float>(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessHigh)
+private val defaultTrack: @Composable (SliderState) -> Unit =
+ @Composable { SliderDefaults.Track(it) }
+
+@Composable
+fun Slider(
+ value: Float,
+ valueRange: ClosedFloatingPointRange<Float>,
+ onValueChanged: (Float) -> Unit,
+ onValueChangeFinished: ((Float) -> Unit)?,
+ stepDistance: Float,
+ isEnabled: Boolean,
+ accessibilityParams: AccessibilityParams,
+ modifier: Modifier = Modifier,
+ colors: SliderColors = SliderDefaults.colors(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ haptics: Haptics = Haptics.Disabled,
+ isVertical: Boolean = false,
+ isReverseDirection: Boolean = false,
+ track: (@Composable (SliderState) -> Unit)? = null,
+) {
+ require(stepDistance > 0) { "stepDistance must be positive" }
+ val coroutineScope = rememberCoroutineScope()
+ val snappedValue = snapValue(value, valueRange, stepDistance)
+ val hapticsViewModel = haptics.createViewModel(snappedValue, valueRange, interactionSource)
+
+ val animatable = remember { Animatable(snappedValue) }
+ var animationJob: Job? by remember { mutableStateOf(null) }
+ val sliderState =
+ remember(valueRange) { SliderState(value = snappedValue, valueRange = valueRange) }
+ val valueChange: (Float) -> Unit = { newValue ->
+ hapticsViewModel?.onValueChange(newValue)
+ val snappedNewValue = snapValue(newValue, valueRange, stepDistance)
+ if (animatable.targetValue != snappedNewValue) {
+ onValueChanged(snappedNewValue)
+ animationJob?.cancel()
+ animationJob =
+ coroutineScope.launch {
+ animatable.animateTo(
+ targetValue = snappedNewValue,
+ animationSpec = defaultSpring,
+ )
+ }
+ }
+ }
+ val semantics =
+ accessibilityParams.createSemantics(
+ animatable.targetValue,
+ valueRange,
+ valueChange,
+ isEnabled,
+ stepDistance,
+ )
+
+ LaunchedEffect(snappedValue) {
+ if (!animatable.isRunning && animatable.targetValue != snappedValue) {
+ animationJob?.cancel()
+ animationJob =
+ coroutineScope.launch {
+ animatable.animateTo(targetValue = snappedValue, animationSpec = defaultSpring)
+ }
+ }
+ }
+
+ sliderState.onValueChangeFinished = {
+ hapticsViewModel?.onValueChangeEnded()
+ onValueChangeFinished?.invoke(animatable.targetValue)
+ }
+ sliderState.onValueChange = valueChange
+ sliderState.value = animatable.value
+
+ if (isVertical) {
+ VerticalSlider(
+ state = sliderState,
+ enabled = isEnabled,
+ reverseDirection = isReverseDirection,
+ interactionSource = interactionSource,
+ colors = colors,
+ track = track ?: defaultTrack,
+ modifier = modifier.clearAndSetSemantics(semantics),
+ )
+ } else {
+ Slider(
+ state = sliderState,
+ enabled = isEnabled,
+ interactionSource = interactionSource,
+ colors = colors,
+ track = track ?: defaultTrack,
+ modifier = modifier.clearAndSetSemantics(semantics),
+ )
+ }
+}
+
+private fun snapValue(
+ value: Float,
+ valueRange: ClosedFloatingPointRange<Float>,
+ stepDistance: Float,
+): Float {
+ if (stepDistance == 0f) {
+ return value
+ }
+ val coercedValue = value.coerceIn(valueRange)
+ return Math.round(coercedValue / stepDistance) * stepDistance
+}
+
+@Composable
+private fun AccessibilityParams.createSemantics(
+ value: Float,
+ valueRange: ClosedFloatingPointRange<Float>,
+ onValueChanged: (Float) -> Unit,
+ isEnabled: Boolean,
+ stepDistance: Float,
+): SemanticsPropertyReceiver.() -> Unit {
+ val semanticsContentDescription =
+ disabledMessage
+ ?.takeIf { !isEnabled }
+ ?.let { message ->
+ stringResource(R.string.volume_slider_disabled_message_template, label, message)
+ } ?: label
+ return {
+ contentDescription = semanticsContentDescription
+ if (isEnabled) {
+ currentStateDescription?.let { stateDescription = it }
+ progressBarRangeInfo = ProgressBarRangeInfo(value, valueRange)
+ } else {
+ disabled()
+ }
+ setProgress { targetValue ->
+ val targetDirection =
+ when {
+ targetValue > value -> 1
+ targetValue < value -> -1
+ else -> 0
+ }
+
+ val newValue =
+ (value + targetDirection * stepDistance).coerceIn(
+ valueRange.start,
+ valueRange.endInclusive,
+ )
+ onValueChanged(newValue)
+ true
+ }
+ }
+}
+
+@Composable
+private fun Haptics.createViewModel(
+ value: Float,
+ valueRange: ClosedFloatingPointRange<Float>,
+ interactionSource: MutableInteractionSource,
+): SliderHapticsViewModel? {
+ return when (this) {
+ is Haptics.Disabled -> null
+ is Haptics.Enabled -> {
+ hapticsViewModelFactory.let {
+ rememberViewModel(traceName = "SliderHapticsViewModel") {
+ it.create(
+ interactionSource,
+ valueRange,
+ orientation,
+ VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(
+ valueRange,
+ hapticFilter,
+ ),
+ VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
+ )
+ }
+ .also { hapticsViewModel ->
+ var lastDiscreteStep by remember { mutableFloatStateOf(value) }
+ LaunchedEffect(value) {
+ snapshotFlow { value }
+ .map { round(it) }
+ .filter { it != lastDiscreteStep }
+ .distinctUntilChanged()
+ .collect { discreteStep ->
+ lastDiscreteStep = discreteStep
+ hapticsViewModel.onValueChange(discreteStep)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+data class AccessibilityParams(
+ val label: String,
+ val currentStateDescription: String?,
+ val disabledMessage: String?,
+)
+
+sealed interface Haptics {
+ data object Disabled : Haptics
+
+ data class Enabled(
+ val hapticsViewModelFactory: SliderHapticsViewModel.Factory,
+ val hapticFilter: SliderHapticFeedbackFilter,
+ val orientation: Orientation,
+ ) : Haptics
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java
index 3d0a8f6..ebbe023 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java
@@ -878,4 +878,18 @@
mMobileDataLayout.setVisibility(mobileDataVisible ? View.VISIBLE : View.GONE);
mConnectedWifi.setVisibility(connectedWifiVisible ? View.VISIBLE : View.GONE);
}
+
+ @Test
+ public void updateDialog_wifiIsDisabled_turnOffProgressBar() {
+ when(mInternetDetailsContentController.isWifiEnabled()).thenReturn(false);
+ mInternetDialogDelegateLegacy.mIsProgressBarVisible = true;
+
+ mInternetDialogDelegateLegacy.updateDialog(false);
+
+ mBgExecutor.runAllReady();
+ mInternetDialogDelegateLegacy.mDataInternetContent.observe(
+ mInternetDialogDelegateLegacy.mLifecycleOwner, i -> {
+ assertThat(mInternetDialogDelegateLegacy.mIsProgressBarVisible).isFalse();
+ });
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
index c5b19ab..0b2fea5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
@@ -31,13 +31,13 @@
import android.view.View
import com.android.systemui.SysuiTestCase
+import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.runner.RunWith
import org.junit.Test
import org.mockito.Answers
-import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito
@@ -66,11 +66,14 @@
@Mock
private lateinit var dialog: ChannelEditorDialog
+ private val shadeDialogContextInteractor = FakeShadeDialogContextInteractor(mContext)
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
`when`(dialogBuilder.build()).thenReturn(dialog)
- controller = ChannelEditorDialogController(mContext, mockNoMan, dialogBuilder)
+ controller =
+ ChannelEditorDialogController(shadeDialogContextInteractor, mockNoMan, dialogBuilder)
channel1 = NotificationChannel(TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_DEFAULT)
channel2 = NotificationChannel(TEST_CHANNEL2, TEST_CHANNEL_NAME2, IMPORTANCE_NONE)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 955de273..7101df1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -85,10 +85,12 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository;
import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView;
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
import com.android.systemui.statusbar.notification.headsup.AvalancheController;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
@@ -157,6 +159,7 @@
@Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@Mock private LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
@Mock private AvalancheController mAvalancheController;
+ @Mock private HeadsUpRepository mHeadsUpRepository;
public NotificationStackScrollLayoutTest(FlagsParameterization flags) {
super();
@@ -176,6 +179,7 @@
mBypassController,
mStatusBarKeyguardViewManager,
mLargeScreenShadeInterpolator,
+ mHeadsUpRepository,
mAvalancheController
));
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
index 4714969..c7ea6db 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
@@ -22,7 +22,7 @@
class FakeQSTile(var user: Int, var available: Boolean = true) : QSTile {
private var tileSpec: String? = null
- var destroyed = false
+ private var destroyed = false
var hasDetailsViewModel: Boolean = true
private var state = QSTile.State()
val callbacks = mutableListOf<QSTile.Callback>()
@@ -64,6 +64,10 @@
user = currentUser
}
+ override fun getCurrentTileUser(): Int {
+ return user
+ }
+
override fun getMetricsCategory(): Int {
return 0
}
@@ -76,6 +80,10 @@
destroyed = true
}
+ override fun isDestroyed(): Boolean {
+ return destroyed
+ }
+
override fun getTileLabel(): CharSequence {
return ""
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
index 4978558..f038fdd 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
@@ -26,7 +26,7 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.time.fakeSystemClock
-val Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by
+var Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by
Kosmos.Fixture {
TileLifecycleManager.Factory { intent, userHandle ->
TileLifecycleManager(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLoggerKosmos.kt
index 7d52f5d..c153183 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLoggerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLoggerKosmos.kt
@@ -17,7 +17,14 @@
package com.android.systemui.qs.pipeline.shared.logging
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.log.logcatLogBuffer
/** mock */
-var Kosmos.qsLogger: QSPipelineLogger by Kosmos.Fixture { mock<QSPipelineLogger>() }
+var Kosmos.qsLogger: QSPipelineLogger by
+ Kosmos.Fixture {
+ QSPipelineLogger(
+ logcatLogBuffer(QSPipelineLogger.TILE_LIST_TAG),
+ logcatLogBuffer(QSPipelineLogger.AUTO_ADD_TAG),
+ logcatLogBuffer(QSPipelineLogger.RESTORE_TAG),
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
index bc1c60c..c058490 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
@@ -33,7 +33,4 @@
override suspend fun handleInput(input: QSTileInput<T>) {
mutex.withLock { mutableInputs.add(input) }
}
-
- override var detailsViewModel: TileDetailsViewModel? =
- FakeTileDetailsViewModel("FakeQSTileUserActionInteractor")
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt
index 6787b8e..c223be4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt
@@ -19,6 +19,7 @@
import android.os.UserHandle
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.qs.instanceIdSequenceFake
+import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.shared.model.TileCategory
import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
@@ -56,7 +57,11 @@
override val config: QSTileConfig = config
override val isAvailable: StateFlow<Boolean> = MutableStateFlow(true)
- override fun onUserChanged(user: UserHandle) {}
+ override var currentTileUser = currentTilesInteractor.userId.value
+
+ override fun onUserChanged(user: UserHandle) {
+ currentTileUser = user.identifier
+ }
override fun forceUpdate() {}
@@ -68,7 +73,7 @@
}
}
-val Kosmos.newQSTileFactory by
+var Kosmos.newQSTileFactory by
Kosmos.Fixture {
NewQSTileFactory(
qSTileConfigProvider,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt
index d65a4a0..4f1bf95 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt
@@ -22,6 +22,7 @@
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.shade.transition.largeScreenShadeInterpolator
import com.android.systemui.statusbar.notification.headsup.mockAvalancheController
+import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager
val Kosmos.ambientState by Fixture {
@@ -32,6 +33,7 @@
/*bypassController=*/ stackScrollAlgorithmBypassController,
/*statusBarKeyguardViewManager=*/ statusBarKeyguardViewManager,
/*largeScreenShadeInterpolator=*/ largeScreenShadeInterpolator,
+ /*headsUpRepository=*/ headsUpNotificationRepository,
/*avalancheController=*/ mockAvalancheController,
)
}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
index 3ebef02..b616766 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
@@ -23,13 +23,10 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.os.Bundle;
import android.platform.test.annotations.RavenwoodTestRunnerInitializing;
import android.platform.test.annotations.internal.InnerRunner;
import android.util.Log;
-import androidx.test.platform.app.InstrumentationRegistry;
-
import com.android.ravenwood.common.RavenwoodCommonUtils;
import org.junit.rules.TestRule;
@@ -285,11 +282,6 @@
private boolean onBefore(Description description, Scope scope, Order order) {
Log.v(TAG, "onBefore: description=" + description + ", " + scope + ", " + order);
- if (scope == Scope.Instance && order == Order.Outer) {
- // Start of a test method.
- mState.enterTestMethod(description);
- }
-
final var classDescription = getDescription();
// Class-level annotations are checked by the runner already, so we only check
@@ -299,6 +291,12 @@
return false;
}
}
+
+ if (scope == Scope.Instance && order == Order.Outer) {
+ // Start of a test method.
+ mState.enterTestMethod(description);
+ }
+
return true;
}
@@ -314,8 +312,7 @@
if (scope == Scope.Instance && order == Order.Outer) {
// End of a test method.
- mState.exitTestMethod();
-
+ mState.exitTestMethod(description);
}
// If RUN_DISABLED_TESTS is set, and the method did _not_ throw, make it an error.
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
index 70bc52b..705186e 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
@@ -81,12 +81,15 @@
RavenwoodRuntimeEnvironmentController.exitTestClass();
}
+ /** Called when a test method is about to start */
public void enterTestMethod(Description description) {
mMethodDescription = description;
- RavenwoodRuntimeEnvironmentController.initForMethod();
+ RavenwoodRuntimeEnvironmentController.enterTestMethod(description);
}
- public void exitTestMethod() {
+ /** Called when a test method finishes */
+ public void exitTestMethod(Description description) {
+ RavenwoodRuntimeEnvironmentController.exitTestMethod(description);
mMethodDescription = null;
}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index f205d23..d935626 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -51,6 +51,7 @@
import android.os.Bundle;
import android.os.HandlerThread;
import android.os.Looper;
+import android.os.Message;
import android.os.Process_ravenwood;
import android.os.ServiceManager;
import android.os.ServiceManager.ServiceNotFoundException;
@@ -74,6 +75,7 @@
import com.android.server.LocalServices;
import com.android.server.compat.PlatformCompat;
+import org.junit.AssumptionViolatedException;
import org.junit.internal.management.ManagementFactory;
import org.junit.runner.Description;
@@ -81,6 +83,7 @@
import java.io.IOException;
import java.io.PrintStream;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -93,6 +96,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
+import java.util.stream.Collectors;
/**
* Responsible for initializing and the environment.
@@ -107,32 +111,60 @@
@SuppressWarnings("UnusedVariable")
private static final PrintStream sStdErr = System.err;
- private static final String MAIN_THREAD_NAME = "RavenwoodMain";
+ private static final String MAIN_THREAD_NAME = "Ravenwood:Main";
+ private static final String TESTS_THREAD_NAME = "Ravenwood:Test";
+
private static final String LIBRAVENWOOD_INITIALIZER_NAME = "ravenwood_initializer";
private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
private static final String ANDROID_LOG_TAGS = "ANDROID_LOG_TAGS";
private static final String RAVENWOOD_ANDROID_LOG_TAGS = "RAVENWOOD_" + ANDROID_LOG_TAGS;
+ static volatile Thread sTestThread;
+ static volatile Thread sMainThread;
+
/**
* When enabled, attempt to dump all thread stacks just before we hit the
* overall Tradefed timeout, to aid in debugging deadlocks.
+ *
+ * Note, this timeout will _not_ stop the test, as there isn't really a clean way to do it.
+ * It'll merely print stacktraces.
*/
private static final boolean ENABLE_TIMEOUT_STACKS =
- "1".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS"));
+ !"0".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS"));
- private static final int TIMEOUT_MILLIS = 9_000;
+ private static final boolean TOLERATE_LOOPER_ASSERTS =
+ !"0".equals(System.getenv("RAVENWOOD_TOLERATE_LOOPER_ASSERTS"));
+
+ static final int DEFAULT_TIMEOUT_SECONDS = 10;
+ private static final int TIMEOUT_MILLIS = getTimeoutSeconds() * 1000;
+
+ static int getTimeoutSeconds() {
+ var e = System.getenv("RAVENWOOD_TIMEOUT_SECONDS");
+ if (e == null || e.isEmpty()) {
+ return DEFAULT_TIMEOUT_SECONDS;
+ }
+ return Integer.parseInt(e);
+ }
+
private static final ScheduledExecutorService sTimeoutExecutor =
- Executors.newScheduledThreadPool(1);
+ Executors.newScheduledThreadPool(1, (Runnable r) -> {
+ Thread t = Executors.defaultThreadFactory().newThread(r);
+ t.setName("Ravenwood:TimeoutMonitor");
+ t.setDaemon(true);
+ return t;
+ });
- private static ScheduledFuture<?> sPendingTimeout;
+ private static volatile ScheduledFuture<?> sPendingTimeout;
/**
* When enabled, attempt to detect uncaught exceptions from background threads.
*/
private static final boolean ENABLE_UNCAUGHT_EXCEPTION_DETECTION =
- "1".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION"));
+ !"0".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION"));
+
+ private static final boolean DIE_ON_UNCAUGHT_EXCEPTION = true;
/**
* When set, an unhandled exception was discovered (typically on a background thread), and we
@@ -141,12 +173,6 @@
private static final AtomicReference<Throwable> sPendingUncaughtException =
new AtomicReference<>();
- private static final Thread.UncaughtExceptionHandler sUncaughtExceptionHandler =
- (thread, throwable) -> {
- // Remember the first exception we discover
- sPendingUncaughtException.compareAndSet(null, throwable);
- };
-
// TODO: expose packCallingIdentity function in libbinder and use it directly
// See: packCallingIdentity in frameworks/native/libs/binder/IPCThreadState.cpp
private static long packBinderIdentityToken(
@@ -187,6 +213,8 @@
* Initialize the global environment.
*/
public static void globalInitOnce() {
+ sTestThread = Thread.currentThread();
+ Thread.currentThread().setName(TESTS_THREAD_NAME);
synchronized (sInitializationLock) {
if (!sInitialized) {
// globalInitOnce() is called from class initializer, which cause
@@ -194,6 +222,7 @@
sInitialized = true;
// This is the first call.
+ final long start = System.currentTimeMillis();
try {
globalInitInner();
} catch (Throwable th) {
@@ -202,6 +231,9 @@
sExceptionFromGlobalInit = th;
SneakyThrow.sneakyThrow(th);
}
+ final long end = System.currentTimeMillis();
+ // TODO Show user/system time too
+ Log.e(TAG, "globalInit() took " + (end - start) + "ms");
} else {
// Subsequent calls. If the first call threw, just throw the same error, to prevent
// the test from running.
@@ -220,7 +252,8 @@
RavenwoodCommonUtils.log(TAG, "globalInitInner()");
if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
- Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler);
+ Thread.setDefaultUncaughtExceptionHandler(
+ RavenwoodRuntimeEnvironmentController::reportUncaughtExceptions);
}
// Some process-wide initialization:
@@ -304,6 +337,7 @@
ActivityManager.init$ravenwood(SYSTEM.getIdentifier());
final var main = new HandlerThread(MAIN_THREAD_NAME);
+ sMainThread = main;
main.start();
Looper.setMainLooperForTest(main.getLooper());
@@ -350,9 +384,20 @@
var systemServerContext =
new RavenwoodContext(ANDROID_PACKAGE_NAME, main, systemResourcesLoader);
- sInstrumentation = new Instrumentation();
- sInstrumentation.basicInit(instContext, targetContext, null);
- InstrumentationRegistry.registerInstance(sInstrumentation, Bundle.EMPTY);
+ var instArgs = Bundle.EMPTY;
+ RavenwoodUtils.runOnMainThreadSync(() -> {
+ try {
+ // TODO We should get the instrumentation class name from the build file or
+ // somewhere.
+ var InstClass = Class.forName("android.app.Instrumentation");
+ sInstrumentation = (Instrumentation) InstClass.getConstructor().newInstance();
+ sInstrumentation.basicInit(instContext, targetContext, null);
+ sInstrumentation.onCreate(instArgs);
+ } catch (Exception e) {
+ SneakyThrow.sneakyThrow(e);
+ }
+ });
+ InstrumentationRegistry.registerInstance(sInstrumentation, instArgs);
RavenwoodSystemServer.init(systemServerContext);
@@ -399,22 +444,46 @@
SystemProperties.clearChangeCallbacksForTest();
- if (ENABLE_TIMEOUT_STACKS) {
- sPendingTimeout = sTimeoutExecutor.schedule(
- RavenwoodRuntimeEnvironmentController::dumpStacks,
- TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
- }
- if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
- maybeThrowPendingUncaughtException(false);
- }
+ maybeThrowPendingUncaughtException();
}
/**
- * Partially reset and initialize before each test method invocation
+ * Called when a test method is about to be started.
*/
- public static void initForMethod() {
+ public static void enterTestMethod(Description description) {
// TODO(b/375272444): this is a hacky workaround to ensure binder identity
Binder.restoreCallingIdentity(sCallingIdentity);
+
+ scheduleTimeout();
+ }
+
+ /**
+ * Called when a test method finished.
+ */
+ public static void exitTestMethod(Description description) {
+ cancelTimeout();
+ maybeThrowPendingUncaughtException();
+ }
+
+ private static void scheduleTimeout() {
+ if (!ENABLE_TIMEOUT_STACKS) {
+ return;
+ }
+ cancelTimeout();
+
+ sPendingTimeout = sTimeoutExecutor.schedule(
+ RavenwoodRuntimeEnvironmentController::onTestTimedOut,
+ TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ }
+
+ private static void cancelTimeout() {
+ if (!ENABLE_TIMEOUT_STACKS) {
+ return;
+ }
+ var pt = sPendingTimeout;
+ if (pt != null) {
+ pt.cancel(false);
+ }
}
private static void initializeCompatIds() {
@@ -473,15 +542,36 @@
}
/**
+ * Return if an exception is benign and okay to continue running the main looper even
+ * if we detect it.
+ */
+ private static boolean isThrowableBenign(Throwable th) {
+ return th instanceof AssertionError || th instanceof AssumptionViolatedException;
+ }
+
+ static void dispatchMessage(Message msg) {
+ try {
+ msg.getTarget().dispatchMessage(msg);
+ } catch (Throwable th) {
+ var desc = String.format("Detected %s on looper thread %s", th.getClass().getName(),
+ Thread.currentThread());
+ sStdErr.println(desc);
+ if (TOLERATE_LOOPER_ASSERTS && isThrowableBenign(th)) {
+ sStdErr.printf("*** Continuing the test because it's %s ***\n",
+ th.getClass().getSimpleName());
+ var e = new Exception(desc, th);
+ sPendingUncaughtException.compareAndSet(null, e);
+ return;
+ }
+ throw th;
+ }
+ }
+
+ /**
* A callback when a test class finishes its execution, mostly only for debugging.
*/
public static void exitTestClass() {
- if (ENABLE_TIMEOUT_STACKS) {
- sPendingTimeout.cancel(false);
- }
- if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
- maybeThrowPendingUncaughtException(true);
- }
+ maybeThrowPendingUncaughtException();
}
public static void logTestRunner(String label, Description description) {
@@ -491,35 +581,70 @@
+ "(" + description.getTestClass().getName() + ")");
}
- private static void dumpStacks() {
- final PrintStream out = System.err;
- out.println("-----BEGIN ALL THREAD STACKS-----");
- final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
- for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) {
- out.println();
- Thread t = stack.getKey();
- out.println(t.toString() + " ID=" + t.getId());
- for (StackTraceElement e : stack.getValue()) {
- out.println("\tat " + e);
- }
+ private static void maybeThrowPendingUncaughtException() {
+ final Throwable pending = sPendingUncaughtException.getAndSet(null);
+ if (pending != null) {
+ throw new IllegalStateException("Found an uncaught exception", pending);
}
- out.println("-----END ALL THREAD STACKS-----");
}
/**
- * If there's a pending uncaught exception, consume and throw it now. Typically used to
- * report an exception on a background thread as a failure for the currently running test.
+ * Prints the stack trace from all threads.
*/
- private static void maybeThrowPendingUncaughtException(boolean duringReset) {
- final Throwable pending = sPendingUncaughtException.getAndSet(null);
- if (pending != null) {
- if (duringReset) {
- throw new IllegalStateException(
- "Found an uncaught exception during this test", pending);
- } else {
- throw new IllegalStateException(
- "Found an uncaught exception before this test started", pending);
+ private static void onTestTimedOut() {
+ sStdErr.println("********* SLOW TEST DETECTED ********");
+ dumpStacks(null, null);
+ }
+
+ private static final Object sDumpStackLock = new Object();
+
+ /**
+ * Prints the stack trace from all threads.
+ */
+ private static void dumpStacks(
+ @Nullable Thread exceptionThread, @Nullable Throwable throwable) {
+ cancelTimeout();
+ synchronized (sDumpStackLock) {
+ final PrintStream out = sStdErr;
+ out.println("-----BEGIN ALL THREAD STACKS-----");
+
+ var stacks = Thread.getAllStackTraces();
+ var threads = stacks.keySet().stream().sorted(
+ Comparator.comparingLong(Thread::getId)).collect(Collectors.toList());
+
+ // Put the test and the main thread at the top.
+ var testThread = sTestThread;
+ var mainThread = sMainThread;
+ if (mainThread != null) {
+ threads.remove(mainThread);
+ threads.add(0, mainThread);
}
+ if (testThread != null) {
+ threads.remove(testThread);
+ threads.add(0, testThread);
+ }
+ // Put the exception thread at the top.
+ // Also inject the stacktrace from the exception.
+ if (exceptionThread != null) {
+ threads.remove(exceptionThread);
+ threads.add(0, exceptionThread);
+ stacks.put(exceptionThread, throwable.getStackTrace());
+ }
+ for (var th : threads) {
+ out.println();
+
+ out.print("Thread");
+ if (th == exceptionThread) {
+ out.print(" [** EXCEPTION THREAD **]");
+ }
+ out.print(": " + th.getName() + " / " + th);
+ out.println();
+
+ for (StackTraceElement e : stacks.get(th)) {
+ out.println("\tat " + e);
+ }
+ }
+ out.println("-----END ALL THREAD STACKS-----");
}
}
@@ -545,13 +670,17 @@
() -> Class.forName("org.mockito.Matchers"));
}
- // TODO: use the real UiAutomation class instead of a mock
- private static UiAutomation createMockUiAutomation() {
- sAdoptedPermissions = Collections.emptySet();
- var mock = mock(UiAutomation.class, inv -> {
+ static <T> T makeDefaultThrowMock(Class<T> clazz) {
+ return mock(clazz, inv -> {
HostTestUtils.onThrowMethodCalled();
return null;
});
+ }
+
+ // TODO: use the real UiAutomation class instead of a mock
+ private static UiAutomation createMockUiAutomation() {
+ sAdoptedPermissions = Collections.emptySet();
+ var mock = makeDefaultThrowMock(UiAutomation.class);
doAnswer(inv -> {
sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS;
return null;
@@ -586,6 +715,23 @@
}
}
+ private static void reportUncaughtExceptions(Thread th, Throwable e) {
+ sStdErr.printf("Uncaught exception detected: %s: %s\n",
+ th, RavenwoodCommonUtils.getStackTraceString(e));
+
+ doBugreport(th, e, DIE_ON_UNCAUGHT_EXCEPTION);
+ }
+
+ private static void doBugreport(
+ @Nullable Thread exceptionThread, @Nullable Throwable throwable,
+ boolean killSelf) {
+ // TODO: Print more information
+ dumpStacks(exceptionThread, throwable);
+ if (killSelf) {
+ System.exit(13);
+ }
+ }
+
private static void dumpJavaProperties() {
Log.v(TAG, "JVM properties:");
dumpMap(System.getProperties());
@@ -601,7 +747,6 @@
Log.v(TAG, " " + key + "=" + map.get(key));
}
}
-
private static void dumpOtherInfo() {
Log.v(TAG, "Other key information:");
var jloc = Locale.getDefault();
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
index 70c161c1..819d93a 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
@@ -26,6 +26,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
@@ -45,6 +46,9 @@
/** The default values. */
static final Map<String, String> sDefaultValues = new HashMap<>();
+ static final Set<String> sReadableKeys = new HashSet<>();
+ static final Set<String> sWritableKeys = new HashSet<>();
+
private static final String[] PARTITIONS = {
"bootimage",
"odm",
@@ -88,9 +92,24 @@
ravenwoodProps.forEach((key, origValue) -> {
final String value;
- // If a value starts with "$$$", then this is a reference to the device-side value.
if (origValue.startsWith("$$$")) {
+ // If a value starts with "$$$", then:
+ // - If it's "$$$r", the key is allowed to read.
+ // - If it's "$$$w", the key is allowed to write.
+ // - Otherwise, it's a reference to the device-side value.
+ // In case of $$$r and $$$w, if the key ends with a '.', then it'll be treaded
+ // as a prefix match.
var deviceKey = origValue.substring(3);
+ if ("r".equals(deviceKey)) {
+ sReadableKeys.add(key);
+ Log.v(TAG, key + " (readable)");
+ return;
+ } else if ("w".equals(deviceKey)) {
+ sWritableKeys.add(key);
+ Log.v(TAG, key + " (writable)");
+ return;
+ }
+
var deviceValue = deviceProps.get(deviceKey);
if (deviceValue == null) {
throw new RuntimeException("Failed to initialize system properties. Key '"
@@ -131,50 +150,38 @@
sDefaultValues.forEach(RavenwoodRuntimeNative::setSystemProperty);
}
+ private static boolean checkAllowedInner(String key, Set<String> allowed) {
+ if (allowed.contains(key)) {
+ return true;
+ }
+
+ // Also search for a prefix match.
+ for (var k : allowed) {
+ if (k.endsWith(".") && key.startsWith(k)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean checkAllowed(String key, Set<String> allowed) {
+ return checkAllowedInner(key, allowed) || checkAllowedInner(getKeyRoot(key), allowed);
+ }
+
private static boolean isKeyReadable(String key) {
- // All writable keys are also readable
- if (isKeyWritable(key)) return true;
-
- final String root = getKeyRoot(key);
-
- // This set is carefully curated to help identify situations where a test may
- // accidentally depend on a default value of an obscure property whose owner hasn't
- // decided how Ravenwood should behave.
- if (root.startsWith("boot.")) return true;
- if (root.startsWith("build.")) return true;
- if (root.startsWith("product.")) return true;
- if (root.startsWith("soc.")) return true;
- if (root.startsWith("system.")) return true;
-
// All core values should be readable
- if (sDefaultValues.containsKey(key)) return true;
-
- // Hardcoded allowlist
- return switch (key) {
- case "gsm.version.baseband",
- "no.such.thing",
- "qemu.sf.lcd_density",
- "ro.bootloader",
- "ro.hardware",
- "ro.hw_timeout_multiplier",
- "ro.odm.build.media_performance_class",
- "ro.sf.lcd_density",
- "ro.treble.enabled",
- "ro.vndk.version",
- "ro.icu.data.path" -> true;
- default -> false;
- };
+ if (sDefaultValues.containsKey(key)) {
+ return true;
+ }
+ if (checkAllowed(key, sReadableKeys)) {
+ return true;
+ }
+ // All writable keys are also readable
+ return isKeyWritable(key);
}
private static boolean isKeyWritable(String key) {
- final String root = getKeyRoot(key);
-
- if (root.startsWith("debug.")) return true;
-
- // For PropertyInvalidatedCache
- if (root.startsWith("cache_key.")) return true;
-
- return false;
+ return checkAllowed(key, sWritableKeys);
}
static boolean isKeyAccessible(String key, boolean write) {
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
index 19c1bff..3e2c405 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
@@ -15,7 +15,20 @@
*/
package android.platform.test.ravenwood;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.ReflectedMethod.reflectMethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+
import com.android.ravenwood.common.RavenwoodCommonUtils;
+import com.android.ravenwood.common.SneakyThrow;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
/**
* Utilities for writing (bivalent) ravenwood tests.
@@ -47,4 +60,129 @@
public static void loadJniLibrary(String libname) {
RavenwoodCommonUtils.loadJniLibrary(libname);
}
+
+ private class MainHandlerHolder {
+ static Handler sMainHandler = new Handler(Looper.getMainLooper());
+ }
+
+ /**
+ * Returns the main thread handler.
+ */
+ public static Handler getMainHandler() {
+ return MainHandlerHolder.sMainHandler;
+ }
+
+ /**
+ * Run a Callable on Handler and wait for it to complete.
+ */
+ @Nullable
+ public static <T> T runOnHandlerSync(@NonNull Handler h, @NonNull Callable<T> c) {
+ var result = new AtomicReference<T>();
+ var thrown = new AtomicReference<Throwable>();
+ var latch = new CountDownLatch(1);
+ h.post(() -> {
+ try {
+ result.set(c.call());
+ } catch (Throwable th) {
+ thrown.set(th);
+ }
+ latch.countDown();
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while waiting on the Runnable", e);
+ }
+ var th = thrown.get();
+ if (th != null) {
+ SneakyThrow.sneakyThrow(th);
+ }
+ return result.get();
+ }
+
+
+ /**
+ * Run a Runnable on Handler and wait for it to complete.
+ */
+ @Nullable
+ public static void runOnHandlerSync(@NonNull Handler h, @NonNull Runnable r) {
+ runOnHandlerSync(h, () -> {
+ r.run();
+ return null;
+ });
+ }
+
+ /**
+ * Run a Callable on main thread and wait for it to complete.
+ */
+ @Nullable
+ public static <T> T runOnMainThreadSync(@NonNull Callable<T> c) {
+ return runOnHandlerSync(getMainHandler(), c);
+ }
+
+ /**
+ * Run a Runnable on main thread and wait for it to complete.
+ */
+ @Nullable
+ public static void runOnMainThreadSync(@NonNull Runnable r) {
+ runOnHandlerSync(getMainHandler(), r);
+ }
+
+ public static class MockitoHelper {
+ private MockitoHelper() {
+ }
+
+ /**
+ * Allow verifyZeroInteractions to work on ravenwood. It was replaced with a different
+ * method on. (Maybe we should do it in Ravenizer.)
+ */
+ public static void verifyZeroInteractions(Object... mocks) {
+ if (RavenwoodRule.isOnRavenwood()) {
+ // Mockito 4 or later
+ reflectMethod("org.mockito.Mockito", "verifyNoInteractions", Object[].class)
+ .callStatic(new Object[]{mocks});
+ } else {
+ // Mockito 2
+ reflectMethod("org.mockito.Mockito", "verifyZeroInteractions", Object[].class)
+ .callStatic(new Object[]{mocks});
+ }
+ }
+ }
+
+
+ /**
+ * Wrap the given {@link Supplier} to become memoized.
+ *
+ * The underlying {@link Supplier} will only be invoked once, and that result will be cached
+ * and returned for any future requests.
+ */
+ static <T> Supplier<T> memoize(ThrowingSupplier<T> supplier) {
+ return new Supplier<>() {
+ private T mInstance;
+
+ @Override
+ public T get() {
+ synchronized (this) {
+ if (mInstance == null) {
+ mInstance = create();
+ }
+ return mInstance;
+ }
+ }
+
+ private T create() {
+ try {
+ return supplier.get();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ }
+
+ /** Used by {@link #memoize(ThrowingSupplier)} */
+ public interface ThrowingSupplier<T> {
+ /** */
+ T get() throws Exception;
+ }
}
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
index a967a3f..893b354 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
@@ -26,10 +26,12 @@
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
+import java.util.Objects;
import java.util.function.Supplier;
public class RavenwoodCommonUtils {
@@ -329,4 +331,70 @@
public static <T> T withDefault(@Nullable T value, @Nullable T def) {
return value != null ? value : def;
}
+
+ /**
+ * Utility for calling a method with reflections. Used to call a method by name.
+ * Note, this intentionally does _not_ support non-public methods, as we generally
+ * shouldn't violate java visibility in ravenwood.
+ *
+ * @param <TTHIS> class owning the method.
+ */
+ public static class ReflectedMethod<TTHIS> {
+ private final Class<TTHIS> mThisClass;
+ private final Method mMethod;
+
+ private ReflectedMethod(Class<TTHIS> thisClass, Method method) {
+ mThisClass = thisClass;
+ mMethod = method;
+ }
+
+ /** Factory method. */
+ @SuppressWarnings("unchecked")
+ public static <TTHIS> ReflectedMethod<TTHIS> reflectMethod(
+ @NonNull Class<TTHIS> clazz, @NonNull String methodName,
+ @NonNull Class<?>... argTypes) {
+ try {
+ return new ReflectedMethod(clazz, clazz.getMethod(methodName, argTypes));
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Factory method. */
+ @SuppressWarnings("unchecked")
+ public static <TTHIS> ReflectedMethod<TTHIS> reflectMethod(
+ @NonNull String className, @NonNull String methodName,
+ @NonNull Class<?>... argTypes) {
+ try {
+ return reflectMethod((Class<TTHIS>) Class.forName(className), methodName, argTypes);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Call the instance method */
+ @SuppressWarnings("unchecked")
+ public <RET> RET call(@NonNull TTHIS thisObject, @NonNull Object... args) {
+ try {
+ return (RET) mMethod.invoke(Objects.requireNonNull(thisObject), args);
+ } catch (InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Call the static method */
+ @SuppressWarnings("unchecked")
+ public <RET> RET callStatic(@NonNull Object... args) {
+ try {
+ return (RET) mMethod.invoke(null, args);
+ } catch (InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /** Handy method to create an array */
+ public static <T> T[] arr(@NonNull T... objects) {
+ return objects;
+ }
}
diff --git a/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java b/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java
index 7ab9cda..855a4ff 100644
--- a/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java
+++ b/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java
@@ -21,7 +21,6 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.RuntimeInit;
import com.android.ravenwood.RavenwoodRuntimeNative;
-import com.android.ravenwood.common.RavenwoodCommonUtils;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
@@ -164,7 +163,7 @@
* Return the "real" {@code System.out} if it's been swapped by {@code RavenwoodRuleImpl}, so
* that we don't end up in a recursive loop.
*/
- private static PrintStream getRealOut() {
+ public static PrintStream getRealOut() {
if (RuntimeInit.sOut$ravenwood != null) {
return RuntimeInit.sOut$ravenwood;
} else {
diff --git a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
index eaadac6..50cfd3b 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
@@ -57,4 +57,12 @@
public int getTargetSdkVersion() {
return RavenwoodRuntimeState.sTargetSdkLevel;
}
+
+ /** Ignored on ravenwood. */
+ public void registerNativeAllocation(long bytes) {
+ }
+
+ /** Ignored on ravenwood. */
+ public void registerNativeFree(long bytes) {
+ }
}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
index cf1a513..985e00e 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
@@ -97,6 +97,9 @@
if (referent == null) {
throw new IllegalArgumentException("referent is null");
}
+ if (mFreeFunction == 0) {
+ return () -> {}; // do nothing
+ }
if (nativePtr == 0) {
throw new IllegalArgumentException("nativePtr is null");
}
diff --git a/ravenwood/scripts/add-annotations.sh b/ravenwood/scripts/add-annotations.sh
index 3e86037..8c394f5 100755
--- a/ravenwood/scripts/add-annotations.sh
+++ b/ravenwood/scripts/add-annotations.sh
@@ -35,7 +35,7 @@
# We add this line to each methods found.
# Note, if we used a single @, that'd be handled as an at file. Use
# the double-at instead.
-annotation="@@android.platform.test.annotations.DisabledOnRavenwood"
+annotation="@@android.platform.test.annotations.DisabledOnRavenwood(reason = \"bulk-disabled by script\")"
while getopts "t:" opt; do
case "$opt" in
t)
diff --git a/ravenwood/tests/coretest/Android.bp b/ravenwood/tests/coretest/Android.bp
index 9dd7cc6..182a7cf 100644
--- a/ravenwood/tests/coretest/Android.bp
+++ b/ravenwood/tests/coretest/Android.bp
@@ -33,3 +33,34 @@
},
auto_gen_config: true,
}
+
+// Same as RavenwoodCoreTest, but it excludes tests using platform-parametric-runner-lib,
+// because that modules has too many dependencies and slow to build incrementally.
+android_ravenwood_test {
+ name: "RavenwoodCoreTest-light",
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+
+ // This library should be removed by Ravenizer
+ "mockito-target-minus-junit4",
+ ],
+ libs: [
+ // We access internal private classes
+ "ravenwood-junit-impl",
+ ],
+ srcs: [
+ "test/**/*.java",
+ "test/**/*.kt",
+ ],
+
+ exclude_srcs: [
+ "test/com/android/ravenwoodtest/runnercallbacktests/*",
+ ],
+ ravenizer: {
+ strip_mockito: true,
+ },
+ auto_gen_config: true,
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java
new file mode 100644
index 0000000..68387d7
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.coretest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.platform.test.ravenwood.RavenwoodUtils;
+
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+public class RavenwoodMainThreadTest {
+ private static final boolean RUN_UNSAFE_TESTS =
+ "1".equals(System.getenv("RAVENWOOD_RUN_UNSAFE_TESTS"));
+
+ @Test
+ public void testRunOnMainThread() {
+ AtomicReference<Thread> thr = new AtomicReference<>();
+ RavenwoodUtils.runOnMainThreadSync(() -> {
+ thr.set(Thread.currentThread());
+ });
+ var th = thr.get();
+ assertThat(th).isNotNull();
+ assertThat(th).isNotEqualTo(Thread.currentThread());
+ }
+
+ /**
+ * Sleep a long time on the main thread. This test would then "pass", but Ravenwood
+ * should show the stack traces.
+ *
+ * This is "unsafe" because this test is slow.
+ */
+ @Test
+ public void testUnsafeMainThreadHang() {
+ assumeTrue(RUN_UNSAFE_TESTS);
+
+ // The test should time out.
+ RavenwoodUtils.runOnMainThreadSync(() -> {
+ try {
+ Thread.sleep(30_000);
+ } catch (InterruptedException e) {
+ fail("Interrupted");
+ }
+ });
+ }
+
+ /**
+ * AssertionError on the main thread would be swallowed and reported "normally".
+ * (Other kinds of exceptions would be caught by the unhandled exception handler, and kills
+ * the process)
+ *
+ * This is "unsafe" only because this feature can be disabled via the env var.
+ */
+ @Test
+ public void testUnsafeAssertFailureOnMainThread() {
+ assumeTrue(RUN_UNSAFE_TESTS);
+
+ assertThrows(AssertionError.class, () -> {
+ RavenwoodUtils.runOnMainThreadSync(() -> {
+ fail();
+ });
+ });
+ }
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java
new file mode 100644
index 0000000..421fb50
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.coretest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.ravenwood.common.RavenwoodCommonUtils.ReflectedMethod;
+
+import org.junit.Test;
+
+/**
+ * Tests for {@link ReflectedMethod}.
+ */
+public class RavenwoodReflectorTest {
+ /** test target */
+ public class Target {
+ private final int mVar;
+
+ /** test target */
+ public Target(int var) {
+ mVar = var;
+ }
+
+ /** test target */
+ public int foo(int x) {
+ return x + mVar;
+ }
+
+ /** test target */
+ public static int bar(int x) {
+ return x + 1;
+ }
+ }
+
+ /** Test for a non-static method call */
+ @Test
+ public void testNonStatic() {
+ var obj = new Target(5);
+
+ var m = ReflectedMethod.reflectMethod(Target.class, "foo", int.class);
+ assertThat((int) m.call(obj, 2)).isEqualTo(7);
+ }
+
+ /** Test for a static method call */
+ @Test
+ public void testStatic() {
+ var m = ReflectedMethod.reflectMethod(Target.class, "bar", int.class);
+ assertThat((int) m.callStatic(1)).isEqualTo(2);
+ }
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java
new file mode 100644
index 0000000..454f5a9
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.coretest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.os.SystemProperties;
+
+import org.junit.Test;
+
+public class RavenwoodSystemPropertiesTest {
+ @Test
+ public void testRead() {
+ assertThat(SystemProperties.get("ro.board.first_api_level")).isEqualTo("1");
+ }
+
+ @Test
+ public void testWrite() {
+ SystemProperties.set("debug.xxx", "5");
+ assertThat(SystemProperties.get("debug.xxx")).isEqualTo("5");
+ }
+
+ private static void assertException(String expectedMessage, Runnable r) {
+ try {
+ r.run();
+ fail("Excepted exception with message '" + expectedMessage + "' but wasn't thrown");
+ } catch (RuntimeException e) {
+ if (e.getMessage().contains(expectedMessage)) {
+ return;
+ }
+ fail("Excepted exception with message '" + expectedMessage + "' but was '"
+ + e.getMessage() + "'");
+ }
+ }
+
+
+ @Test
+ public void testReadDisallowed() {
+ assertException("Read access to system property 'nonexisitent' denied", () -> {
+ SystemProperties.get("nonexisitent");
+ });
+ }
+
+ @Test
+ public void testWriteDisallowed() {
+ assertException("failed to set system property \"ro.board.first_api_level\" ", () -> {
+ SystemProperties.set("ro.board.first_api_level", "2");
+ });
+ }
+}
diff --git a/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java b/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
index 30abaa2..b1a40f0 100644
--- a/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
+++ b/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
@@ -16,28 +16,27 @@
package com.android.ravenwoodtest;
import android.platform.test.annotations.IgnoreUnderRavenwood;
-import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
-import org.junit.Rule;
+import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class RavenwoodMinimumTest {
- @Rule
- public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
- .setProcessApp()
- .build();
-
@Test
public void testSimple() {
Assert.assertTrue(android.os.Process.isApplicationUid(android.os.Process.myUid()));
}
@Test
+ public void testAssumeNot() {
+ Assume.assumeFalse(android.os.Process.isApplicationUid(android.os.Process.myUid()));
+ }
+
+ @Test
@IgnoreUnderRavenwood
public void testIgnored() {
throw new RuntimeException("Shouldn't be executed under ravenwood");
diff --git a/ravenwood/texts/ravenwood-build.prop b/ravenwood/texts/ravenwood-build.prop
index 37c50f1..512b459 100644
--- a/ravenwood/texts/ravenwood-build.prop
+++ b/ravenwood/texts/ravenwood-build.prop
@@ -8,9 +8,41 @@
ro.soc.model=Ravenwood
ro.debuggable=1
-# For the graphics stack
-ro.hwui.max_texture_allocation_size=104857600
persist.sys.locale=en-US
+ro.product.locale=en-US
+
+ro.hwui.max_texture_allocation_size=104857600
+
+# Allowlist control:
+# This set is carefully curated to help identify situations where a test may
+# accidentally depend on a default value of an obscure property whose owner hasn't
+# decided how Ravenwood should behave.
+
+boot.=$$$r
+build.=$$$r
+product.=$$$r
+soc.=$$$r
+system.=$$$r
+wm.debug.=$$$r
+wm.extensions.=$$$r
+
+gsm.version.baseband=$$$r
+no.such.thing=$$$r
+qemu.sf.lcd_density=$$$r
+ro.bootloader=$$$r
+ro.hardware=$$$r
+ro.hw_timeout_multiplier=$$$r
+ro.odm.build.media_performance_class=$$$r
+ro.sf.lcd_density=$$$r
+ro.treble.enabled=$$$r
+ro.vndk.version=$$$r
+ro.icu.data.path=$$$r
+
+# Writable keys
+debug.=$$$w
+
+# For PropertyInvalidatedCache
+cache_key.=$$$w
# The ones starting with "ro.product" or "ro.build" will be copied to all "partitions" too.
# See RavenwoodSystemProperties.
diff --git a/ravenwood/texts/ravenwood-services-jarjar-rules.txt b/ravenwood/texts/ravenwood-services-jarjar-rules.txt
index 8fdd340..64a0e25 100644
--- a/ravenwood/texts/ravenwood-services-jarjar-rules.txt
+++ b/ravenwood/texts/ravenwood-services-jarjar-rules.txt
@@ -5,7 +5,7 @@
# Rename all other service internals so that tests can continue to statically
# link services code when owners aren't ready to support on Ravenwood
-rule com.android.server.** repackaged.@0
+rule com.android.server.** repackaged.services.@0
# TODO: support AIDL generated Parcelables via hoststubgen
-rule android.hardware.power.stats.** repackaged.@0
+rule android.hardware.power.stats.** repackaged.services.@0
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 4b04248..47aa8f5 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -1115,14 +1115,12 @@
if (svcConnTracingEnabled()) {
logTraceSvcConn("performGlobalAction", "action=" + action);
}
- int currentUserId;
synchronized (mLock) {
if (!hasRightsToCurrentUserLocked()) {
return false;
}
- currentUserId = mSystemSupport.getCurrentUserIdLocked();
}
- enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId);
+ enforceCurrentUserIfVisibleBackgroundEnabled();
final long identity = Binder.clearCallingIdentity();
try {
return mSystemActionPerformer.performSystemAction(action);
@@ -2791,11 +2789,7 @@
@RequiresNoPermission
@Override
public void setAnimationScale(float scale) {
- int currentUserId;
- synchronized (mLock) {
- currentUserId = mSystemSupport.getCurrentUserIdLocked();
- }
- enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId);
+ enforceCurrentUserIfVisibleBackgroundEnabled();
final long identity = Binder.clearCallingIdentity();
try {
Settings.Global.putFloat(
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 9eb8442..1c95184 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -1402,11 +1402,7 @@
@EnforcePermission(MANAGE_ACCESSIBILITY)
public void registerSystemAction(RemoteAction action, int actionId) {
registerSystemAction_enforcePermission();
- int currentUserId;
- synchronized (mLock) {
- currentUserId = mCurrentUserId;
- }
- enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId);
+ enforceCurrentUserIfVisibleBackgroundEnabled();
if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
mTraceManager.logTrace(LOG_TAG + ".registerSystemAction",
FLAGS_ACCESSIBILITY_MANAGER, "action=" + action + ";actionId=" + actionId);
@@ -1423,11 +1419,7 @@
@EnforcePermission(MANAGE_ACCESSIBILITY)
public void unregisterSystemAction(int actionId) {
unregisterSystemAction_enforcePermission();
- int currentUserId;
- synchronized (mLock) {
- currentUserId = mCurrentUserId;
- }
- enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId);
+ enforceCurrentUserIfVisibleBackgroundEnabled();
if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
mTraceManager.logTrace(LOG_TAG + ".unregisterSystemAction",
FLAGS_ACCESSIBILITY_MANAGER, "actionId=" + actionId);
@@ -1759,7 +1751,7 @@
synchronized (mLock) {
currentUserId = mCurrentUserId;
}
- enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId);
+ enforceCurrentUserIfVisibleBackgroundEnabled();
if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
mTraceManager.logTrace(LOG_TAG + ".notifyAccessibilityButtonClicked",
FLAGS_ACCESSIBILITY_MANAGER,
@@ -1807,11 +1799,7 @@
@EnforcePermission(STATUS_BAR_SERVICE)
public void notifyAccessibilityButtonVisibilityChanged(boolean shown) {
notifyAccessibilityButtonVisibilityChanged_enforcePermission();
- int currentUserId;
- synchronized (mLock) {
- currentUserId = mCurrentUserId;
- }
- enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId);
+ enforceCurrentUserIfVisibleBackgroundEnabled();
if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
mTraceManager.logTrace(LOG_TAG + ".notifyAccessibilityButtonVisibilityChanged",
FLAGS_ACCESSIBILITY_MANAGER, "shown=" + shown);
@@ -5002,11 +4990,7 @@
throws RemoteException {
registerProxyForDisplay_enforcePermission();
mSecurityPolicy.checkForAccessibilityPermissionOrRole();
- int currentUserId;
- synchronized (mLock) {
- currentUserId = mCurrentUserId;
- }
- enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId);
+ enforceCurrentUserIfVisibleBackgroundEnabled();
if (client == null) {
return false;
}
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
index bd34f33..c182c26 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -149,7 +149,6 @@
OperationStorage mOperationStorage;
List<PackageInfo> mPackages;
- PackageInfo mCurrentPackage;
boolean mUpdateSchedule;
CountDownLatch mLatch;
FullBackupJob mJob; // if a scheduled job needs to be finished afterwards
@@ -207,10 +206,9 @@
for (String pkg : whichPackages) {
try {
PackageManager pm = backupManagerService.getPackageManager();
- PackageInfo info = pm.getPackageInfoAsUser(pkg,
+ PackageInfo packageInfo = pm.getPackageInfoAsUser(pkg,
PackageManager.GET_SIGNING_CERTIFICATES, mUserId);
- mCurrentPackage = info;
- if (!mBackupEligibilityRules.appIsEligibleForBackup(info.applicationInfo)) {
+ if (!mBackupEligibilityRules.appIsEligibleForBackup(packageInfo.applicationInfo)) {
// Cull any packages that have indicated that backups are not permitted,
// that run as system-domain uids but do not define their own backup agents,
// as well as any explicit mention of the 'special' shared-storage agent
@@ -220,13 +218,13 @@
}
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_INELIGIBLE,
- mCurrentPackage,
+ packageInfo,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
+ /* extras= */ null);
BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
BackupManager.ERROR_BACKUP_NOT_ALLOWED);
continue;
- } else if (!mBackupEligibilityRules.appGetsFullBackup(info)) {
+ } else if (!mBackupEligibilityRules.appGetsFullBackup(packageInfo)) {
// Cull any packages that are found in the queue but now aren't supposed
// to get full-data backup operations.
if (DEBUG) {
@@ -235,13 +233,13 @@
}
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_KEY_VALUE_PARTICIPANT,
- mCurrentPackage,
+ packageInfo,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
+ /* extras= */ null);
BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
BackupManager.ERROR_BACKUP_NOT_ALLOWED);
continue;
- } else if (mBackupEligibilityRules.appIsStopped(info.applicationInfo)) {
+ } else if (mBackupEligibilityRules.appIsStopped(packageInfo.applicationInfo)) {
// Cull any packages in the 'stopped' state: they've either just been
// installed or have explicitly been force-stopped by the user. In both
// cases we do not want to launch them for backup.
@@ -250,21 +248,21 @@
}
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_STOPPED,
- mCurrentPackage,
+ packageInfo,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
+ /* extras= */ null);
BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
BackupManager.ERROR_BACKUP_NOT_ALLOWED);
continue;
}
- mPackages.add(info);
+ mPackages.add(packageInfo);
} catch (NameNotFoundException e) {
Slog.i(TAG, "Requested package " + pkg + " not found; ignoring");
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_FOUND,
- mCurrentPackage,
+ /* pkg= */ null,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
+ /* extras= */ null);
}
}
@@ -352,10 +350,11 @@
} else {
monitoringEvent = BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED;
}
- mBackupManagerMonitorEventSender
- .monitorEvent(monitoringEvent, null,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
+ mBackupManagerMonitorEventSender.monitorEvent(
+ monitoringEvent,
+ /* pkg= */ null,
+ BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+ /* extras= */ null);
mUpdateSchedule = false;
backupRunStatus = BackupManager.ERROR_BACKUP_NOT_ALLOWED;
return;
@@ -367,8 +366,9 @@
backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_TRANSPORT_NOT_PRESENT,
- mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
- null);
+ /* pkg= */ null,
+ BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
+ /* extras= */ null);
return;
}
@@ -461,9 +461,10 @@
}
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT,
- mCurrentPackage,
+ currentPackage,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- mBackupManagerMonitorEventSender.putMonitoringExtra(null,
+ BackupManagerMonitorEventSender.putMonitoringExtra(
+ /* extras= */ null,
BackupManagerMonitor.EXTRA_LOG_PREFLIGHT_ERROR,
preflightResult));
backupPackageStatus = (int) preflightResult;
@@ -496,9 +497,9 @@
+ ": " + totalRead + " of " + quota);
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_QUOTA_HIT_PREFLIGHT,
- mCurrentPackage,
+ currentPackage,
BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
- null);
+ /* extras= */ null);
mBackupRunner.sendQuotaExceeded(totalRead, quota);
}
}
@@ -645,9 +646,9 @@
Slog.w(TAG, "Exception trying full transport backup", e);
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_EXCEPTION_FULL_BACKUP,
- mCurrentPackage,
+ /* pkg= */ null,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- mBackupManagerMonitorEventSender.putMonitoringExtra(null,
+ BackupManagerMonitorEventSender.putMonitoringExtra(/* extras= */ null,
BackupManagerMonitor.EXTRA_LOG_EXCEPTION_FULL_BACKUP,
Log.getStackTraceString(e)));
@@ -966,9 +967,6 @@
}
}
-
- // BackupRestoreTask interface: specifically, timeout detection
-
@Override
public void execute() { /* intentionally empty */ }
@@ -981,7 +979,9 @@
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_CANCEL,
- mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
+ mTarget,
+ BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT,
+ /* extras= */ null);
mIsCancelled = true;
// Cancel tasks spun off by this task.
mUserBackupManagerService.handleCancel(mEphemeralToken, cancelAll);
diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
index c4519b1..33668a6 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
@@ -71,6 +71,7 @@
mMonitor = monitor;
}
+ @Nullable
public IBackupManagerMonitor getMonitor() {
return mMonitor;
}
@@ -87,9 +88,9 @@
*/
public void monitorEvent(
int id,
- PackageInfo pkg,
+ @Nullable PackageInfo pkg,
int category,
- Bundle extras) {
+ @Nullable Bundle extras) {
try {
Bundle bundle = new Bundle();
bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID, id);
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index b6fe0ad..e46bbe2 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -160,6 +160,7 @@
import com.android.server.pm.Installer;
import com.android.server.pm.UserManagerInternal;
import com.android.server.storage.AppFuseBridge;
+import com.android.server.storage.ImmutableVolumeInfo;
import com.android.server.storage.StorageSessionController;
import com.android.server.storage.StorageSessionController.ExternalStorageServiceException;
import com.android.server.storage.WatchedVolumeInfo;
@@ -777,7 +778,7 @@
break;
}
case H_VOLUME_UNMOUNT: {
- final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj;
+ final ImmutableVolumeInfo vol = (ImmutableVolumeInfo) msg.obj;
unmount(vol);
break;
}
@@ -898,8 +899,14 @@
for (int i = 0; i < size; i++) {
final WatchedVolumeInfo vol = mVolumes.valueAt(i);
if (vol.getMountUserId() == userId) {
+ // Capture the volume before we set mount user id to null,
+ // so that StorageSessionController remove the session from
+ // the correct user (old mount user id)
+ final ImmutableVolumeInfo volToUnmount
+ = vol.getClonedImmutableVolumeInfo();
vol.setMountUserId(UserHandle.USER_NULL);
- mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget();
+ mHandler.obtainMessage(H_VOLUME_UNMOUNT, volToUnmount)
+ .sendToTarget();
}
}
}
@@ -1295,7 +1302,12 @@
}
private void maybeRemountVolumes(int userId) {
- List<WatchedVolumeInfo> volumesToRemount = new ArrayList<>();
+ // We need to keep 2 lists
+ // 1. List of volumes before we set the mount user Id so that
+ // StorageSessionController is able to remove the session from the correct user (old one)
+ // 2. List of volumes to mount which should have the up to date info
+ List<ImmutableVolumeInfo> volumesToUnmount = new ArrayList<>();
+ List<WatchedVolumeInfo> volumesToMount = new ArrayList<>();
synchronized (mLock) {
for (int i = 0; i < mVolumes.size(); i++) {
final WatchedVolumeInfo vol = mVolumes.valueAt(i);
@@ -1303,16 +1315,19 @@
&& vol.getMountUserId() != mCurrentUserId) {
// If there's a visible secondary volume mounted,
// we need to update the currentUserId and remount
+ // But capture the volume with the old user id first to use it in unmounting
+ volumesToUnmount.add(vol.getClonedImmutableVolumeInfo());
vol.setMountUserId(mCurrentUserId);
- volumesToRemount.add(vol);
+ volumesToMount.add(vol);
}
}
}
- for (WatchedVolumeInfo vol : volumesToRemount) {
- Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: " + vol);
- mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget();
- mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
+ for (int i = 0; i < volumesToMount.size(); i++) {
+ Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: "
+ + volumesToUnmount.get(i));
+ mHandler.obtainMessage(H_VOLUME_UNMOUNT, volumesToUnmount.get(i)).sendToTarget();
+ mHandler.obtainMessage(H_VOLUME_MOUNT, volumesToMount.get(i)).sendToTarget();
}
}
@@ -2430,10 +2445,10 @@
super.unmount_enforcePermission();
final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId);
- unmount(vol);
+ unmount(vol.getClonedImmutableVolumeInfo());
}
- private void unmount(WatchedVolumeInfo vol) {
+ private void unmount(ImmutableVolumeInfo vol) {
try {
try {
if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
@@ -2444,7 +2459,7 @@
}
extendWatchdogTimeout("#unmount might be slow");
mVold.unmount(vol.getId());
- mStorageSessionController.onVolumeUnmount(vol.getImmutableVolumeInfo());
+ mStorageSessionController.onVolumeUnmount(vol);
} catch (Exception e) {
Slog.wtf(TAG, e);
}
diff --git a/services/core/java/com/android/server/SystemTimeZone.java b/services/core/java/com/android/server/SystemTimeZone.java
index dd07081..c8810f6 100644
--- a/services/core/java/com/android/server/SystemTimeZone.java
+++ b/services/core/java/com/android/server/SystemTimeZone.java
@@ -133,6 +133,7 @@
boolean timeZoneChanged = false;
synchronized (SystemTimeZone.class) {
String currentTimeZoneId = getTimeZoneId();
+ @TimeZoneConfidence int currentConfidence = getTimeZoneConfidence();
if (currentTimeZoneId == null || !currentTimeZoneId.equals(timeZoneId)) {
SystemProperties.set(TIME_ZONE_SYSTEM_PROPERTY, timeZoneId);
if (DEBUG) {
@@ -145,6 +146,8 @@
String logMsg = "Time zone or confidence set: "
+ " (new) timeZoneId=" + timeZoneId
+ ", (new) confidence=" + confidence
+ + ", (old) timeZoneId=" + currentTimeZoneId
+ + ", (old) confidence=" + currentConfidence
+ ", logInfo=" + logInfo;
addDebugLogEntry(logMsg);
}
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index bd7a0ac..b75b7ddf 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -2816,13 +2816,11 @@
if (!checkNotifyPermission("notifyEmergencyNumberList()")) {
return;
}
- if (Flags.enforceTelephonyFeatureMappingForPublicApis()) {
- if (!mContext.getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_TELEPHONY_CALLING)) {
- // TelephonyManager.getEmergencyNumberList() throws an exception if
- // FEATURE_TELEPHONY_CALLING is not defined.
- return;
- }
+ if (!mContext.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_TELEPHONY_CALLING)) {
+ // TelephonyManager.getEmergencyNumberList() throws an exception if
+ // FEATURE_TELEPHONY_CALLING is not defined.
+ return;
}
synchronized (mRecords) {
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 8eda176..296f7cf 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -907,7 +907,7 @@
throw new IllegalArgumentException("Unknown mode: " + mode);
}
- enforceCurrentUserIfVisibleBackgroundEnabled(mCurrentUser);
+ enforceCurrentUserIfVisibleBackgroundEnabled();
final int user = UserHandle.getCallingUserId();
final long ident = Binder.clearCallingIdentity();
@@ -970,7 +970,7 @@
@AttentionModeThemeOverlayType int attentionModeThemeOverlayType) {
setAttentionModeThemeOverlay_enforcePermission();
- enforceCurrentUserIfVisibleBackgroundEnabled(mCurrentUser);
+ enforceCurrentUserIfVisibleBackgroundEnabled();
synchronized (mLock) {
if (mAttentionModeThemeOverlay != attentionModeThemeOverlayType) {
@@ -1070,7 +1070,7 @@
return false;
}
- enforceCurrentUserIfVisibleBackgroundEnabled(mCurrentUser);
+ enforceCurrentUserIfVisibleBackgroundEnabled();
// Store the last requested bedtime night mode state so that we don't need to notify
// anyone if the user decides to switch to the night mode to bedtime.
@@ -1124,7 +1124,7 @@
return;
}
- enforceCurrentUserIfVisibleBackgroundEnabled(mCurrentUser);
+ enforceCurrentUserIfVisibleBackgroundEnabled();
final int user = UserHandle.getCallingUserId();
final long ident = Binder.clearCallingIdentity();
@@ -1155,7 +1155,7 @@
return;
}
- enforceCurrentUserIfVisibleBackgroundEnabled(mCurrentUser);
+ enforceCurrentUserIfVisibleBackgroundEnabled();
final int user = UserHandle.getCallingUserId();
final long ident = Binder.clearCallingIdentity();
@@ -1178,7 +1178,7 @@
assertLegit(callingPackage);
assertSingleProjectionType(projectionType);
enforceProjectionTypePermissions(projectionType);
- enforceCurrentUserIfVisibleBackgroundEnabled(mCurrentUser);
+ enforceCurrentUserIfVisibleBackgroundEnabled();
synchronized (mLock) {
if (mProjectionHolders == null) {
@@ -1224,7 +1224,7 @@
assertLegit(callingPackage);
assertSingleProjectionType(projectionType);
enforceProjectionTypePermissions(projectionType);
- enforceCurrentUserIfVisibleBackgroundEnabled(mCurrentUser);
+ enforceCurrentUserIfVisibleBackgroundEnabled();
return releaseProjectionUnchecked(projectionType, callingPackage);
}
@@ -1266,7 +1266,7 @@
return;
}
- enforceCurrentUserIfVisibleBackgroundEnabled(mCurrentUser);
+ enforceCurrentUserIfVisibleBackgroundEnabled();
synchronized (mLock) {
if (mProjectionListeners == null) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 8b701f0..b0b34d0 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -19471,7 +19471,7 @@
/**
* @hide
*/
- @EnforcePermission("android.permission.INTERACT_ACROSS_USERS_FULL")
+ @EnforcePermission(INTERACT_ACROSS_USERS_FULL)
public IBinder refreshIntentCreatorToken(Intent intent) {
refreshIntentCreatorToken_enforcePermission();
IBinder binder = intent.getCreatorToken();
diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
index 36035bd..78beb18 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
@@ -832,7 +832,9 @@
// If this receiver is going to be skipped, skip it now itself and don't even enqueue
// it.
- final String skipReason = mSkipPolicy.shouldSkipMessage(r, receiver);
+ final String skipReason = Flags.avoidNoteOpAtEnqueue()
+ ? mSkipPolicy.shouldSkipAtEnqueueMessage(r, receiver)
+ : mSkipPolicy.shouldSkipMessage(r, receiver);
if (skipReason != null) {
setDeliveryState(null, null, r, i, receiver, BroadcastRecord.DELIVERY_SKIPPED,
"skipped by policy at enqueue: " + skipReason);
diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
index d2af84c..b0d5994 100644
--- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
+++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
@@ -71,10 +71,20 @@
* {@code null} if it can proceed.
*/
public @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, @NonNull Object target) {
+ return shouldSkipMessage(r, target, false /* preflight */);
+ }
+
+ public @Nullable String shouldSkipAtEnqueueMessage(@NonNull BroadcastRecord r,
+ @NonNull Object target) {
+ return shouldSkipMessage(r, target, true /* preflight */);
+ }
+
+ private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, @NonNull Object target,
+ boolean preflight) {
if (target instanceof BroadcastFilter) {
- return shouldSkipMessage(r, (BroadcastFilter) target);
+ return shouldSkipMessage(r, (BroadcastFilter) target, preflight);
} else {
- return shouldSkipMessage(r, (ResolveInfo) target);
+ return shouldSkipMessage(r, (ResolveInfo) target, preflight);
}
}
@@ -86,7 +96,7 @@
* {@code null} if it can proceed.
*/
private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r,
- @NonNull ResolveInfo info) {
+ @NonNull ResolveInfo info, boolean preflight) {
final BroadcastOptions brOptions = r.options;
final ComponentName component = new ComponentName(
info.activityInfo.applicationInfo.packageName,
@@ -134,15 +144,23 @@
+ " requires " + info.activityInfo.permission;
}
} else if (info.activityInfo.permission != null) {
- final int opCode = AppOpsManager.permissionToOpCode(info.activityInfo.permission);
- if (opCode != AppOpsManager.OP_NONE && mService.getAppOpsManager().noteOpNoThrow(opCode,
- r.callingUid, r.callerPackage, r.callerFeatureId,
- "Broadcast delivered to " + info.activityInfo.name)
- != AppOpsManager.MODE_ALLOWED) {
- return "Appop Denial: broadcasting "
- + broadcastDescription(r, component)
- + " requires appop " + AppOpsManager.permissionToOp(
- info.activityInfo.permission);
+ final String op = AppOpsManager.permissionToOp(info.activityInfo.permission);
+ if (op != null) {
+ final int mode;
+ if (preflight) {
+ mode = mService.getAppOpsManager().checkOpNoThrow(op,
+ r.callingUid, r.callerPackage, r.callerFeatureId);
+ } else {
+ mode = mService.getAppOpsManager().noteOpNoThrow(op,
+ r.callingUid, r.callerPackage, r.callerFeatureId,
+ "Broadcast delivered to " + info.activityInfo.name);
+ }
+ if (mode != AppOpsManager.MODE_ALLOWED) {
+ return "Appop Denial: broadcasting "
+ + broadcastDescription(r, component)
+ + " requires appop " + AppOpsManager.permissionToOp(
+ info.activityInfo.permission);
+ }
}
}
@@ -250,8 +268,8 @@
perm = PackageManager.PERMISSION_DENIED;
}
- int appOp = AppOpsManager.permissionToOpCode(excludedPermission);
- if (appOp != AppOpsManager.OP_NONE) {
+ final String appOp = AppOpsManager.permissionToOp(excludedPermission);
+ if (appOp != null) {
// When there is an app op associated with the permission,
// skip when both the permission and the app op are
// granted.
@@ -259,7 +277,7 @@
mService.getAppOpsManager().checkOpNoThrow(appOp,
info.activityInfo.applicationInfo.uid,
info.activityInfo.packageName)
- == AppOpsManager.MODE_ALLOWED)) {
+ == AppOpsManager.MODE_ALLOWED)) {
return "Skipping delivery to " + info.activityInfo.packageName
+ " due to excluded permission " + excludedPermission;
}
@@ -292,9 +310,10 @@
createAttributionSourcesForResolveInfo(info);
for (int i = 0; i < r.requiredPermissions.length; i++) {
String requiredPermission = r.requiredPermissions[i];
- perm = hasPermissionForDataDelivery(
+ perm = hasPermission(
requiredPermission,
"Broadcast delivered to " + info.activityInfo.name,
+ preflight,
attributionSources)
? PackageManager.PERMISSION_GRANTED
: PackageManager.PERMISSION_DENIED;
@@ -308,10 +327,14 @@
}
}
}
- if (r.appOp != AppOpsManager.OP_NONE) {
- if (!noteOpForManifestReceiver(r.appOp, r, info, component)) {
+ if (r.appOp != AppOpsManager.OP_NONE && AppOpsManager.isValidOp(r.appOp)) {
+ final String op = AppOpsManager.opToPublicName(r.appOp);
+ final boolean appOpAllowed = preflight
+ ? checkOpForManifestReceiver(r.appOp, op, r, info, component)
+ : noteOpForManifestReceiver(r.appOp, op, r, info, component);
+ if (!appOpAllowed) {
return "Skipping delivery to " + info.activityInfo.packageName
- + " due to required appop " + r.appOp;
+ + " due to required appop " + AppOpsManager.opToName(r.appOp);
}
}
@@ -338,7 +361,7 @@
* {@code null} if it can proceed.
*/
private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r,
- @NonNull BroadcastFilter filter) {
+ @NonNull BroadcastFilter filter, boolean preflight) {
if (r.options != null && !r.options.testRequireCompatChange(filter.owningUid)) {
return "Compat change filtered: broadcasting " + r.intent.toString()
+ " to uid " + filter.owningUid + " due to compat change "
@@ -372,18 +395,25 @@
+ " requires " + filter.requiredPermission
+ " due to registered receiver " + filter;
} else {
- final int opCode = AppOpsManager.permissionToOpCode(filter.requiredPermission);
- if (opCode != AppOpsManager.OP_NONE
- && mService.getAppOpsManager().noteOpNoThrow(opCode, r.callingUid,
- r.callerPackage, r.callerFeatureId, "Broadcast sent to protected receiver")
- != AppOpsManager.MODE_ALLOWED) {
- return "Appop Denial: broadcasting "
- + r.intent.toString()
- + " from " + r.callerPackage + " (pid="
- + r.callingPid + ", uid=" + r.callingUid + ")"
- + " requires appop " + AppOpsManager.permissionToOp(
- filter.requiredPermission)
- + " due to registered receiver " + filter;
+ final String op = AppOpsManager.permissionToOp(filter.requiredPermission);
+ if (op != null) {
+ final int mode;
+ if (preflight) {
+ mode = mService.getAppOpsManager().checkOpNoThrow(op,
+ r.callingUid, r.callerPackage, r.callerFeatureId);
+ } else {
+ mode = mService.getAppOpsManager().noteOpNoThrow(op, r.callingUid,
+ r.callerPackage, r.callerFeatureId,
+ "Broadcast sent to protected receiver");
+ }
+ if (mode != AppOpsManager.MODE_ALLOWED) {
+ return "Appop Denial: broadcasting "
+ + r.intent
+ + " from " + r.callerPackage + " (pid="
+ + r.callingPid + ", uid=" + r.callingUid + ")"
+ + " requires appop " + op
+ + " due to registered receiver " + filter;
+ }
}
}
}
@@ -433,9 +463,10 @@
.build();
for (int i = 0; i < r.requiredPermissions.length; i++) {
String requiredPermission = r.requiredPermissions[i];
- final int perm = hasPermissionForDataDelivery(
+ final int perm = hasPermission(
requiredPermission,
"Broadcast delivered to registered receiver " + filter.receiverId,
+ preflight,
attributionSource)
? PackageManager.PERMISSION_GRANTED
: PackageManager.PERMISSION_DENIED;
@@ -471,8 +502,8 @@
final int perm = checkComponentPermission(excludedPermission,
filter.receiverList.pid, filter.receiverList.uid, -1, true);
- int appOp = AppOpsManager.permissionToOpCode(excludedPermission);
- if (appOp != AppOpsManager.OP_NONE) {
+ final String appOp = AppOpsManager.permissionToOp(excludedPermission);
+ if (appOp != null) {
// When there is an app op associated with the permission,
// skip when both the permission and the app op are
// granted.
@@ -480,14 +511,13 @@
mService.getAppOpsManager().checkOpNoThrow(appOp,
filter.receiverList.uid,
filter.packageName)
- == AppOpsManager.MODE_ALLOWED)) {
+ == AppOpsManager.MODE_ALLOWED)) {
return "Appop Denial: receiving "
- + r.intent.toString()
+ + r.intent
+ " to " + filter.receiverList.app
+ " (pid=" + filter.receiverList.pid
+ ", uid=" + filter.receiverList.uid + ")"
- + " excludes appop " + AppOpsManager.permissionToOp(
- excludedPermission)
+ + " excludes appop " + appOp
+ " due to sender " + r.callerPackage
+ " (uid " + r.callingUid + ")";
}
@@ -496,7 +526,7 @@
// skip when permission is granted.
if (perm == PackageManager.PERMISSION_GRANTED) {
return "Permission Denial: receiving "
- + r.intent.toString()
+ + r.intent
+ " to " + filter.receiverList.app
+ " (pid=" + filter.receiverList.pid
+ ", uid=" + filter.receiverList.uid + ")"
@@ -523,19 +553,27 @@
}
// If the broadcast also requires an app op check that as well.
- if (r.appOp != AppOpsManager.OP_NONE
- && mService.getAppOpsManager().noteOpNoThrow(r.appOp,
- filter.receiverList.uid, filter.packageName, filter.featureId,
- "Broadcast delivered to registered receiver " + filter.receiverId)
- != AppOpsManager.MODE_ALLOWED) {
- return "Appop Denial: receiving "
- + r.intent.toString()
- + " to " + filter.receiverList.app
- + " (pid=" + filter.receiverList.pid
- + ", uid=" + filter.receiverList.uid + ")"
- + " requires appop " + AppOpsManager.opToName(r.appOp)
- + " due to sender " + r.callerPackage
- + " (uid " + r.callingUid + ")";
+ if (r.appOp != AppOpsManager.OP_NONE && AppOpsManager.isValidOp(r.appOp)) {
+ final String op = AppOpsManager.opToPublicName(r.appOp);
+ final int mode;
+ if (preflight) {
+ mode = mService.getAppOpsManager().checkOpNoThrow(op,
+ filter.receiverList.uid, filter.packageName, filter.featureId);
+ } else {
+ mode = mService.getAppOpsManager().noteOpNoThrow(op,
+ filter.receiverList.uid, filter.packageName, filter.featureId,
+ "Broadcast delivered to registered receiver " + filter.receiverId);
+ }
+ if (mode != AppOpsManager.MODE_ALLOWED) {
+ return "Appop Denial: receiving "
+ + r.intent
+ + " to " + filter.receiverList.app
+ + " (pid=" + filter.receiverList.pid
+ + ", uid=" + filter.receiverList.uid + ")"
+ + " requires appop " + AppOpsManager.opToName(r.appOp)
+ + " due to sender " + r.callerPackage
+ + " (uid " + r.callingUid + ")";
+ }
}
// Ensure that broadcasts are only sent to other apps if they are explicitly marked as
@@ -572,14 +610,14 @@
+ ", uid=" + r.callingUid + ") to " + component.flattenToShortString();
}
- private boolean noteOpForManifestReceiver(int appOp, BroadcastRecord r, ResolveInfo info,
- ComponentName component) {
+ private boolean noteOpForManifestReceiver(int opCode, String appOp, BroadcastRecord r,
+ ResolveInfo info, ComponentName component) {
if (ArrayUtils.isEmpty(info.activityInfo.attributionTags)) {
- return noteOpForManifestReceiverInner(appOp, r, info, component, null);
+ return noteOpForManifestReceiverInner(opCode, appOp, r, info, component, null);
} else {
// Attribution tags provided, noteOp each tag
for (String tag : info.activityInfo.attributionTags) {
- if (!noteOpForManifestReceiverInner(appOp, r, info, component, tag)) {
+ if (!noteOpForManifestReceiverInner(opCode, appOp, r, info, component, tag)) {
return false;
}
}
@@ -587,8 +625,8 @@
}
}
- private boolean noteOpForManifestReceiverInner(int appOp, BroadcastRecord r, ResolveInfo info,
- ComponentName component, String tag) {
+ private boolean noteOpForManifestReceiverInner(int opCode, String appOp, BroadcastRecord r,
+ ResolveInfo info, ComponentName component, String tag) {
if (mService.getAppOpsManager().noteOpNoThrow(appOp,
info.activityInfo.applicationInfo.uid,
info.activityInfo.packageName,
@@ -598,7 +636,37 @@
Slog.w(TAG, "Appop Denial: receiving "
+ r.intent + " to "
+ component.flattenToShortString()
- + " requires appop " + AppOpsManager.opToName(appOp)
+ + " requires appop " + AppOpsManager.opToName(opCode)
+ + " due to sender " + r.callerPackage
+ + " (uid " + r.callingUid + ")");
+ return false;
+ }
+ return true;
+ }
+
+ private boolean checkOpForManifestReceiver(int opCode, String appOp, BroadcastRecord r,
+ ResolveInfo info, ComponentName component) {
+ if (ArrayUtils.isEmpty(info.activityInfo.attributionTags)) {
+ return checkOpForManifestReceiverInner(opCode, appOp, r, info, component, null);
+ } else {
+ // Attribution tags provided, noteOp each tag
+ for (String tag : info.activityInfo.attributionTags) {
+ if (!checkOpForManifestReceiverInner(opCode, appOp, r, info, component, tag)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ private boolean checkOpForManifestReceiverInner(int opCode, String appOp, BroadcastRecord r,
+ ResolveInfo info, ComponentName component, String tag) {
+ if (mService.getAppOpsManager().checkOpNoThrow(appOp, info.activityInfo.applicationInfo.uid,
+ info.activityInfo.packageName, tag) != AppOpsManager.MODE_ALLOWED) {
+ Slog.w(TAG, "Appop Denial: receiving "
+ + r.intent + " to "
+ + component.flattenToShortString()
+ + " requires appop " + AppOpsManager.opToName(opCode)
+ " due to sender " + r.callerPackage
+ " (uid " + r.callingUid + ")");
return false;
@@ -694,9 +762,10 @@
return mPermissionManager;
}
- private boolean hasPermissionForDataDelivery(
+ private boolean hasPermission(
@NonNull String permission,
@NonNull String message,
+ boolean preflight,
@NonNull AttributionSource... attributionSources) {
final PermissionManager permissionManager = getPermissionManager();
if (permissionManager == null) {
@@ -704,9 +773,14 @@
}
for (AttributionSource attributionSource : attributionSources) {
- final int permissionCheckResult =
- permissionManager.checkPermissionForDataDelivery(
- permission, attributionSource, message);
+ final int permissionCheckResult;
+ if (preflight) {
+ permissionCheckResult = permissionManager.checkPermissionForPreflight(
+ permission, attributionSource);
+ } else {
+ permissionCheckResult = permissionManager.checkPermissionForDataDelivery(
+ permission, attributionSource, message);
+ }
if (permissionCheckResult != PackageManager.PERMISSION_GRANTED) {
return false;
}
diff --git a/services/core/java/com/android/server/am/broadcasts_flags.aconfig b/services/core/java/com/android/server/am/broadcasts_flags.aconfig
index 7f169db..68e21a3 100644
--- a/services/core/java/com/android/server/am/broadcasts_flags.aconfig
+++ b/services/core/java/com/android/server/am/broadcasts_flags.aconfig
@@ -15,4 +15,15 @@
description: "Limit the scope of receiver priorities to within a process"
is_fixed_read_only: true
bug: "369487976"
+}
+
+flag {
+ name: "avoid_note_op_at_enqueue"
+ namespace: "backstage_power"
+ description: "Avoid triggering noteOp while enqueueing a broadcast"
+ is_fixed_read_only: true
+ bug: "268016162"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 3cb2125..0f1228f 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -1164,9 +1164,11 @@
@GuardedBy("mAccessibilityServiceUidsLock")
private int[] mAccessibilityServiceUids;
- // Uid of the active input method service to check if caller is the one or not.
- private int mInputMethodServiceUid = android.os.Process.INVALID_UID;
+ // Input Method
private final Object mInputMethodServiceUidLock = new Object();
+ // Uid of the active input method service to check if caller is the one or not.
+ @GuardedBy("mInputMethodServiceUidLock")
+ private int mInputMethodServiceUid = android.os.Process.INVALID_UID;
private int mEncodedSurroundMode;
private String mEnabledSurroundFormats;
@@ -11405,7 +11407,7 @@
/** see {@link AudioManager#getFocusDuckedUidsForTest()} */
@Override
- @EnforcePermission("android.permission.QUERY_AUDIO_STATE")
+ @EnforcePermission(QUERY_AUDIO_STATE)
public @NonNull List<Integer> getFocusDuckedUidsForTest() {
super.getFocusDuckedUidsForTest_enforcePermission();
return mPlaybackMonitor.getFocusDuckedUids();
@@ -11432,7 +11434,7 @@
* @see AudioManager#getFocusFadeOutDurationForTest()
* @return the fade out duration, in ms
*/
- @EnforcePermission("android.permission.QUERY_AUDIO_STATE")
+ @EnforcePermission(QUERY_AUDIO_STATE)
public long getFocusFadeOutDurationForTest() {
super.getFocusFadeOutDurationForTest_enforcePermission();
return mMediaFocusControl.getFocusFadeOutDurationForTest();
@@ -11445,7 +11447,7 @@
* @return the time gap after a fade out completion on focus loss, and fade in start, in ms
*/
@Override
- @EnforcePermission("android.permission.QUERY_AUDIO_STATE")
+ @EnforcePermission(QUERY_AUDIO_STATE)
public long getFocusUnmuteDelayAfterFadeOutForTest() {
super.getFocusUnmuteDelayAfterFadeOutForTest_enforcePermission();
return mMediaFocusControl.getFocusUnmuteDelayAfterFadeOutForTest();
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
index 940bcb4..f40d0dd 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
@@ -43,7 +43,10 @@
import com.android.internal.annotations.GuardedBy;
import java.util.Collection;
+import java.util.HashSet;
import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
/**
* A class that represents a broker for the endpoint registered by the client app. This class
@@ -111,6 +114,11 @@
private final boolean mRemoteInitiated;
+ /**
+ * The set of seq # for pending reliable messages started by this endpoint for this session.
+ */
+ private final Set<Integer> mPendingSequenceNumbers = new HashSet<>();
+
SessionInfo(HubEndpointInfo remoteEndpointInfo, boolean remoteInitiated) {
mRemoteEndpointInfo = remoteEndpointInfo;
mRemoteInitiated = remoteInitiated;
@@ -131,6 +139,24 @@
public boolean isActive() {
return mSessionState == SessionState.ACTIVE;
}
+
+ public boolean isReliableMessagePending(int sequenceNumber) {
+ return mPendingSequenceNumbers.contains(sequenceNumber);
+ }
+
+ public void setReliableMessagePending(int sequenceNumber) {
+ mPendingSequenceNumbers.add(sequenceNumber);
+ }
+
+ public void setReliableMessageCompleted(int sequenceNumber) {
+ mPendingSequenceNumbers.remove(sequenceNumber);
+ }
+
+ public void forEachPendingReliableMessage(Consumer<Integer> consumer) {
+ for (int sequenceNumber : mPendingSequenceNumbers) {
+ consumer.accept(sequenceNumber);
+ }
+ }
}
/** A map between a session ID which maps to its current state. */
@@ -208,10 +234,7 @@
try {
mSessionInfoMap.put(sessionId, new SessionInfo(destination, false));
mHubInterface.openEndpointSession(
- sessionId,
- halEndpointInfo.id,
- mHalEndpointInfo.id,
- serviceDescriptor);
+ sessionId, halEndpointInfo.id, mHalEndpointInfo.id, serviceDescriptor);
} catch (RemoteException | IllegalArgumentException | UnsupportedOperationException e) {
Log.e(TAG, "Exception while calling HAL openEndpointSession", e);
cleanupSessionResources(sessionId);
@@ -286,34 +309,42 @@
public void sendMessage(
int sessionId, HubMessage message, IContextHubTransactionCallback callback) {
super.sendMessage_enforcePermission();
- Message halMessage = ContextHubServiceUtil.createHalMessage(message);
- if (!isSessionActive(sessionId)) {
- throw new SecurityException(
- "sendMessage called on inactive session (id= " + sessionId + ")");
- }
-
- if (callback == null) {
- try {
- mHubInterface.sendMessageToEndpoint(sessionId, halMessage);
- } catch (RemoteException e) {
- Log.w(TAG, "Exception while sending message on session " + sessionId, e);
+ synchronized (mOpenSessionLock) {
+ SessionInfo info = mSessionInfoMap.get(sessionId);
+ if (info == null) {
+ throw new IllegalArgumentException(
+ "sendMessage for invalid session id=" + sessionId);
}
- } else {
- ContextHubServiceTransaction transaction =
- mTransactionManager.createSessionMessageTransaction(
- mHubInterface, sessionId, halMessage, mPackageName, callback);
- try {
- mTransactionManager.addTransaction(transaction);
- } catch (IllegalStateException e) {
- Log.e(
- TAG,
- "Unable to add a transaction in sendMessageToEndpoint "
- + "(session ID = "
- + sessionId
- + ")",
- e);
- transaction.onTransactionComplete(
- ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE);
+ if (!info.isActive()) {
+ throw new SecurityException(
+ "sendMessage called on inactive session (id= " + sessionId + ")");
+ }
+
+ Message halMessage = ContextHubServiceUtil.createHalMessage(message);
+ if (callback == null) {
+ try {
+ mHubInterface.sendMessageToEndpoint(sessionId, halMessage);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Exception while sending message on session " + sessionId, e);
+ }
+ } else {
+ ContextHubServiceTransaction transaction =
+ mTransactionManager.createSessionMessageTransaction(
+ mHubInterface, sessionId, halMessage, mPackageName, callback);
+ try {
+ mTransactionManager.addTransaction(transaction);
+ info.setReliableMessagePending(transaction.getMessageSequenceNumber());
+ } catch (IllegalStateException e) {
+ Log.e(
+ TAG,
+ "Unable to add a transaction in sendMessageToEndpoint "
+ + "(session ID = "
+ + sessionId
+ + ")",
+ e);
+ transaction.onTransactionComplete(
+ ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE);
+ }
}
}
}
@@ -393,7 +424,9 @@
int id = mSessionInfoMap.keyAt(i);
int count = i + 1;
sb.append(
- " " + count + ". id="
+ " "
+ + count
+ + ". id="
+ id
+ ", remote:"
+ mSessionInfoMap.get(id).getRemoteEndpointInfo());
@@ -461,13 +494,24 @@
/* package */ void onMessageReceived(int sessionId, HubMessage message) {
byte code = onMessageReceivedInternal(sessionId, message);
if (code != ErrorCode.OK && message.isResponseRequired()) {
- sendMessageDeliveryStatus(
- sessionId, message.getMessageSequenceNumber(), code);
+ sendMessageDeliveryStatus(sessionId, message.getMessageSequenceNumber(), code);
}
}
/* package */ void onMessageDeliveryStatusReceived(
int sessionId, int sequenceNumber, byte errorCode) {
+ synchronized (mOpenSessionLock) {
+ SessionInfo info = mSessionInfoMap.get(sessionId);
+ if (info == null || !info.isActive()) {
+ Log.w(TAG, "Received delivery status for invalid session: id=" + sessionId);
+ return;
+ }
+ if (!info.isReliableMessagePending(sequenceNumber)) {
+ Log.w(TAG, "Received delivery status for unknown seq: " + sequenceNumber);
+ return;
+ }
+ info.setReliableMessageCompleted(sequenceNumber);
+ }
mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK);
}
@@ -492,7 +536,6 @@
onCloseEndpointSession(id, Reason.HUB_RESET);
}
}
- // TODO(b/390029594): Cancel any ongoing reliable communication transactions
}
private Optional<Byte> onEndpointSessionOpenRequestInternal(
@@ -515,9 +558,11 @@
mSessionInfoMap.put(sessionId, new SessionInfo(initiator, true));
}
- boolean success = invokeCallback(
- (consumer) ->
- consumer.onSessionOpenRequest(sessionId, initiator, serviceDescriptor));
+ boolean success =
+ invokeCallback(
+ (consumer) ->
+ consumer.onSessionOpenRequest(
+ sessionId, initiator, serviceDescriptor));
return success ? Optional.empty() : Optional.of(Reason.UNSPECIFIED);
}
@@ -590,8 +635,15 @@
private boolean cleanupSessionResources(int sessionId) {
synchronized (mOpenSessionLock) {
SessionInfo info = mSessionInfoMap.get(sessionId);
- if (info != null && !info.isRemoteInitiated()) {
- mEndpointManager.returnSessionId(sessionId);
+ if (info != null) {
+ if (!info.isRemoteInitiated()) {
+ mEndpointManager.returnSessionId(sessionId);
+ }
+ info.forEachPendingReliableMessage(
+ (sequenceNumber) -> {
+ mTransactionManager.onMessageDeliveryResponse(
+ sequenceNumber, /* success= */ false);
+ });
mSessionInfoMap.remove(sessionId);
}
return info != null;
@@ -646,10 +698,7 @@
try {
mWakeLock.release();
} catch (RuntimeException e) {
- Log.e(
- TAG,
- "Releasing the wakelock for all acquisitions fails - ",
- e);
+ Log.e(TAG, "Releasing the wakelock for all acquisitions fails - ", e);
break;
}
}
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
index a430a82..6a1db02 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
@@ -29,6 +29,7 @@
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.time.Duration;
import java.util.ArrayDeque;
@@ -165,52 +166,61 @@
/**
* Creates a transaction for loading a nanoapp.
*
- * @param contextHubId the ID of the hub to load the nanoapp to
- * @param nanoAppBinary the binary of the nanoapp to load
+ * @param contextHubId the ID of the hub to load the nanoapp to
+ * @param nanoAppBinary the binary of the nanoapp to load
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
*/
/* package */ ContextHubServiceTransaction createLoadTransaction(
- int contextHubId, NanoAppBinary nanoAppBinary,
- IContextHubTransactionCallback onCompleteCallback, String packageName) {
+ int contextHubId,
+ NanoAppBinary nanoAppBinary,
+ IContextHubTransactionCallback onCompleteCallback,
+ String packageName) {
return new ContextHubServiceTransaction(
- mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_LOAD_NANOAPP,
- nanoAppBinary.getNanoAppId(), packageName) {
+ mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_LOAD_NANOAPP,
+ nanoAppBinary.getNanoAppId(),
+ packageName) {
@Override
- /* package */ int onTransact() {
+ /* package */ int onTransact() {
try {
return mContextHubProxy.loadNanoapp(
contextHubId, nanoAppBinary, this.getTransactionId());
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while trying to load nanoapp with ID 0x" +
- Long.toHexString(nanoAppBinary.getNanoAppId()), e);
+ Log.e(
+ TAG,
+ "RemoteException while trying to load nanoapp with ID 0x"
+ + Long.toHexString(nanoAppBinary.getNanoAppId()),
+ e);
return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
}
}
@Override
- /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
ContextHubStatsLog.write(
ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED,
nanoAppBinary.getNanoAppId(),
nanoAppBinary.getNanoAppVersion(),
ContextHubStatsLog
- .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_LOAD,
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_LOAD,
toStatsTransactionResult(result));
- ContextHubEventLogger.getInstance().logNanoappLoad(
- contextHubId,
- nanoAppBinary.getNanoAppId(),
- nanoAppBinary.getNanoAppVersion(),
- nanoAppBinary.getBinary().length,
- result == ContextHubTransaction.RESULT_SUCCESS);
+ ContextHubEventLogger.getInstance()
+ .logNanoappLoad(
+ contextHubId,
+ nanoAppBinary.getNanoAppId(),
+ nanoAppBinary.getNanoAppVersion(),
+ nanoAppBinary.getBinary().length,
+ result == ContextHubTransaction.RESULT_SUCCESS);
if (result == ContextHubTransaction.RESULT_SUCCESS) {
// NOTE: The legacy JNI code used to do a query right after a load success
// to synchronize the service cache. Instead store the binary that was
// requested to load to update the cache later without doing a query.
mNanoAppStateManager.addNanoAppInstance(
- contextHubId, nanoAppBinary.getNanoAppId(),
+ contextHubId,
+ nanoAppBinary.getNanoAppId(),
nanoAppBinary.getNanoAppVersion());
}
try {
@@ -228,42 +238,51 @@
/**
* Creates a transaction for unloading a nanoapp.
*
- * @param contextHubId the ID of the hub to unload the nanoapp from
- * @param nanoAppId the ID of the nanoapp to unload
+ * @param contextHubId the ID of the hub to unload the nanoapp from
+ * @param nanoAppId the ID of the nanoapp to unload
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
*/
/* package */ ContextHubServiceTransaction createUnloadTransaction(
- int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback,
+ int contextHubId,
+ long nanoAppId,
+ IContextHubTransactionCallback onCompleteCallback,
String packageName) {
return new ContextHubServiceTransaction(
- mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_UNLOAD_NANOAPP,
- nanoAppId, packageName) {
+ mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_UNLOAD_NANOAPP,
+ nanoAppId,
+ packageName) {
@Override
- /* package */ int onTransact() {
+ /* package */ int onTransact() {
try {
return mContextHubProxy.unloadNanoapp(
contextHubId, nanoAppId, this.getTransactionId());
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while trying to unload nanoapp with ID 0x" +
- Long.toHexString(nanoAppId), e);
+ Log.e(
+ TAG,
+ "RemoteException while trying to unload nanoapp with ID 0x"
+ + Long.toHexString(nanoAppId),
+ e);
return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
}
}
@Override
- /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
ContextHubStatsLog.write(
- ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED, nanoAppId,
+ ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED,
+ nanoAppId,
0 /* nanoappVersion */,
ContextHubStatsLog
- .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_UNLOAD,
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_UNLOAD,
toStatsTransactionResult(result));
- ContextHubEventLogger.getInstance().logNanoappUnload(
- contextHubId,
- nanoAppId,
- result == ContextHubTransaction.RESULT_SUCCESS);
+ ContextHubEventLogger.getInstance()
+ .logNanoappUnload(
+ contextHubId,
+ nanoAppId,
+ result == ContextHubTransaction.RESULT_SUCCESS);
if (result == ContextHubTransaction.RESULT_SUCCESS) {
mNanoAppStateManager.removeNanoAppInstance(contextHubId, nanoAppId);
@@ -283,31 +302,37 @@
/**
* Creates a transaction for enabling a nanoapp.
*
- * @param contextHubId the ID of the hub to enable the nanoapp on
- * @param nanoAppId the ID of the nanoapp to enable
+ * @param contextHubId the ID of the hub to enable the nanoapp on
+ * @param nanoAppId the ID of the nanoapp to enable
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
*/
/* package */ ContextHubServiceTransaction createEnableTransaction(
- int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback,
+ int contextHubId,
+ long nanoAppId,
+ IContextHubTransactionCallback onCompleteCallback,
String packageName) {
return new ContextHubServiceTransaction(
- mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_ENABLE_NANOAPP,
+ mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_ENABLE_NANOAPP,
packageName) {
@Override
- /* package */ int onTransact() {
+ /* package */ int onTransact() {
try {
return mContextHubProxy.enableNanoapp(
contextHubId, nanoAppId, this.getTransactionId());
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while trying to enable nanoapp with ID 0x" +
- Long.toHexString(nanoAppId), e);
+ Log.e(
+ TAG,
+ "RemoteException while trying to enable nanoapp with ID 0x"
+ + Long.toHexString(nanoAppId),
+ e);
return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
}
}
@Override
- /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
try {
onCompleteCallback.onTransactionComplete(result);
} catch (RemoteException e) {
@@ -320,31 +345,37 @@
/**
* Creates a transaction for disabling a nanoapp.
*
- * @param contextHubId the ID of the hub to disable the nanoapp on
- * @param nanoAppId the ID of the nanoapp to disable
+ * @param contextHubId the ID of the hub to disable the nanoapp on
+ * @param nanoAppId the ID of the nanoapp to disable
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
*/
/* package */ ContextHubServiceTransaction createDisableTransaction(
- int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback,
+ int contextHubId,
+ long nanoAppId,
+ IContextHubTransactionCallback onCompleteCallback,
String packageName) {
return new ContextHubServiceTransaction(
- mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_DISABLE_NANOAPP,
+ mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_DISABLE_NANOAPP,
packageName) {
@Override
- /* package */ int onTransact() {
+ /* package */ int onTransact() {
try {
return mContextHubProxy.disableNanoapp(
contextHubId, nanoAppId, this.getTransactionId());
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while trying to disable nanoapp with ID 0x" +
- Long.toHexString(nanoAppId), e);
+ Log.e(
+ TAG,
+ "RemoteException while trying to disable nanoapp with ID 0x"
+ + Long.toHexString(nanoAppId),
+ e);
return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
}
}
@Override
- /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
try {
onCompleteCallback.onTransactionComplete(result);
} catch (RemoteException e) {
@@ -447,18 +478,20 @@
/**
* Creates a transaction for querying for a list of nanoapps.
*
- * @param contextHubId the ID of the hub to query
+ * @param contextHubId the ID of the hub to query
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
*/
/* package */ ContextHubServiceTransaction createQueryTransaction(
- int contextHubId, IContextHubTransactionCallback onCompleteCallback,
+ int contextHubId,
+ IContextHubTransactionCallback onCompleteCallback,
String packageName) {
return new ContextHubServiceTransaction(
- mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_QUERY_NANOAPPS,
+ mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_QUERY_NANOAPPS,
packageName) {
@Override
- /* package */ int onTransact() {
+ /* package */ int onTransact() {
try {
return mContextHubProxy.queryNanoapps(contextHubId);
} catch (RemoteException e) {
@@ -468,12 +501,12 @@
}
@Override
- /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
onQueryResponse(result, Collections.emptyList());
}
@Override
- /* package */ void onQueryResponse(
+ /* package */ void onQueryResponse(
@ContextHubTransaction.Result int result, List<NanoAppState> nanoAppStateList) {
try {
onCompleteCallback.onQueryResponse(result, nanoAppStateList);
@@ -539,6 +572,14 @@
}
}
+ @VisibleForTesting
+ /* package */
+ int numReliableMessageTransactionPending() {
+ synchronized (mReliableMessageLock) {
+ return mReliableMessageTransactionMap.size();
+ }
+ }
+
/**
* Handles a transaction response from a Context Hub.
*
@@ -585,18 +626,21 @@
void onMessageDeliveryResponse(int messageSequenceNumber, boolean success) {
if (!Flags.reliableMessageRetrySupportService()) {
TransactionAcceptConditions conditions =
- transaction -> transaction.getTransactionType()
- == ContextHubTransaction.TYPE_RELIABLE_MESSAGE
- && transaction.getMessageSequenceNumber()
- == messageSequenceNumber;
+ transaction ->
+ transaction.getTransactionType()
+ == ContextHubTransaction.TYPE_RELIABLE_MESSAGE
+ && transaction.getMessageSequenceNumber()
+ == messageSequenceNumber;
ContextHubServiceTransaction transaction = getTransactionAndHandleNext(conditions);
if (transaction == null) {
- Log.w(TAG, "Received unexpected message delivery response (expected"
- + " message sequence number = "
- + messageSequenceNumber
- + ", received messageSequenceNumber = "
- + messageSequenceNumber
- + ")");
+ Log.w(
+ TAG,
+ "Received unexpected message delivery response (expected"
+ + " message sequence number = "
+ + messageSequenceNumber
+ + ", received messageSequenceNumber = "
+ + messageSequenceNumber
+ + ")");
return;
}
@@ -640,8 +684,10 @@
*/
/* package */
void onQueryResponse(List<NanoAppState> nanoAppStateList) {
- TransactionAcceptConditions conditions = transaction ->
- transaction.getTransactionType() == ContextHubTransaction.TYPE_QUERY_NANOAPPS;
+ TransactionAcceptConditions conditions =
+ transaction ->
+ transaction.getTransactionType()
+ == ContextHubTransaction.TYPE_QUERY_NANOAPPS;
ContextHubServiceTransaction transaction = getTransactionAndHandleNext(conditions);
if (transaction == null) {
Log.w(TAG, "Received unexpected query response");
@@ -968,24 +1014,33 @@
private int toStatsTransactionResult(@ContextHubTransaction.Result int result) {
switch (result) {
case ContextHubTransaction.RESULT_SUCCESS:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_SUCCESS;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_SUCCESS;
case ContextHubTransaction.RESULT_FAILED_BAD_PARAMS:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BAD_PARAMS;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BAD_PARAMS;
case ContextHubTransaction.RESULT_FAILED_UNINITIALIZED:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNINITIALIZED;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNINITIALIZED;
case ContextHubTransaction.RESULT_FAILED_BUSY:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BUSY;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BUSY;
case ContextHubTransaction.RESULT_FAILED_AT_HUB:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_AT_HUB;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_AT_HUB;
case ContextHubTransaction.RESULT_FAILED_TIMEOUT:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_TIMEOUT;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_TIMEOUT;
case ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_SERVICE_INTERNAL_FAILURE;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_SERVICE_INTERNAL_FAILURE;
case ContextHubTransaction.RESULT_FAILED_HAL_UNAVAILABLE:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_HAL_UNAVAILABLE;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_HAL_UNAVAILABLE;
case ContextHubTransaction.RESULT_FAILED_UNKNOWN:
default: /* fall through */
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNKNOWN;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNKNOWN;
}
}
diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
index 7a96195..9937049 100644
--- a/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
+++ b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
@@ -21,6 +21,7 @@
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Environment;
@@ -204,6 +205,11 @@
return false;
}
final String clause = WhiteListReportContract.TIMESTAMP + "< " + untilTimestamp;
- return db.delete(WhiteListReportContract.TABLE, clause, null) != 0;
+ try {
+ return db.delete(WhiteListReportContract.TABLE, clause, null) != 0;
+ } catch (SQLiteDatabaseCorruptException e) {
+ Slog.e(TAG, "Error deleting records", e);
+ return false;
+ }
}
}
diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java
index 3f2c222..dd52cce 100644
--- a/services/core/java/com/android/server/notification/ConditionProviders.java
+++ b/services/core/java/com/android/server/notification/ConditionProviders.java
@@ -46,6 +46,7 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
public class ConditionProviders extends ManagedServices {
@@ -202,7 +203,14 @@
@Override
protected void loadDefaultsFromConfig() {
- String defaultDndAccess = mContext.getResources().getString(
+ for (String dndPackage : getDefaultDndAccessPackages(mContext)) {
+ addDefaultComponentOrPackage(dndPackage);
+ }
+ }
+
+ static List<String> getDefaultDndAccessPackages(Context context) {
+ ArrayList<String> packages = new ArrayList<>();
+ String defaultDndAccess = context.getResources().getString(
R.string.config_defaultDndAccessPackages);
if (defaultDndAccess != null) {
String[] dnds = defaultDndAccess.split(ManagedServices.ENABLED_SERVICES_SEPARATOR);
@@ -210,9 +218,10 @@
if (TextUtils.isEmpty(dnds[i])) {
continue;
}
- addDefaultComponentOrPackage(dnds[i]);
+ packages.add(dnds[i]);
}
}
+ return packages;
}
@Override
diff --git a/services/core/java/com/android/server/notification/ZenConfigTrimmer.java b/services/core/java/com/android/server/notification/ZenConfigTrimmer.java
new file mode 100644
index 0000000..d65954d
--- /dev/null
+++ b/services/core/java/com/android/server/notification/ZenConfigTrimmer.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.service.notification.SystemZenRules;
+import android.service.notification.ZenModeConfig;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+class ZenConfigTrimmer {
+
+ private static final String TAG = "ZenConfigTrimmer";
+ private static final int MAXIMUM_PARCELED_SIZE = 150_000; // bytes
+
+ private final HashSet<String> mTrustedPackages;
+
+ ZenConfigTrimmer(Context context) {
+ mTrustedPackages = new HashSet<>();
+ mTrustedPackages.add(SystemZenRules.PACKAGE_ANDROID);
+ mTrustedPackages.addAll(ConditionProviders.getDefaultDndAccessPackages(context));
+ }
+
+ void trimToMaximumSize(ZenModeConfig config) {
+ Map<String, PackageRules> rulesPerPackage = new HashMap<>();
+ for (ZenModeConfig.ZenRule rule : config.automaticRules.values()) {
+ PackageRules pkgRules = rulesPerPackage.computeIfAbsent(rule.pkg, PackageRules::new);
+ pkgRules.mRules.add(rule);
+ }
+
+ int totalSize = 0;
+ for (PackageRules pkgRules : rulesPerPackage.values()) {
+ totalSize += pkgRules.dataSize();
+ }
+
+ if (totalSize > MAXIMUM_PARCELED_SIZE) {
+ List<PackageRules> deletionCandidates = new ArrayList<>();
+ for (PackageRules pkgRules : rulesPerPackage.values()) {
+ if (!mTrustedPackages.contains(pkgRules.mPkg)) {
+ deletionCandidates.add(pkgRules);
+ }
+ }
+ deletionCandidates.sort(Comparator.comparingInt(PackageRules::dataSize).reversed());
+
+ evictPackagesFromConfig(config, deletionCandidates, totalSize);
+ }
+ }
+
+ private static void evictPackagesFromConfig(ZenModeConfig config,
+ List<PackageRules> deletionCandidates, int currentSize) {
+ while (currentSize > MAXIMUM_PARCELED_SIZE && !deletionCandidates.isEmpty()) {
+ PackageRules rulesToDelete = deletionCandidates.removeFirst();
+ Slog.w(TAG, String.format("Evicting %s zen rules from package '%s' (%s bytes)",
+ rulesToDelete.mRules.size(), rulesToDelete.mPkg, rulesToDelete.dataSize()));
+
+ for (ZenModeConfig.ZenRule rule : rulesToDelete.mRules) {
+ config.automaticRules.remove(rule.id);
+ }
+
+ currentSize -= rulesToDelete.dataSize();
+ }
+ }
+
+ private static class PackageRules {
+ private final String mPkg;
+ private final List<ZenModeConfig.ZenRule> mRules;
+ private int mParceledSize = -1;
+
+ PackageRules(String pkg) {
+ mPkg = pkg;
+ mRules = new ArrayList<>();
+ }
+
+ private int dataSize() {
+ if (mParceledSize >= 0) {
+ return mParceledSize;
+ }
+ Parcel parcel = Parcel.obtain();
+ try {
+ parcel.writeParcelableList(mRules, 0);
+ mParceledSize = parcel.dataSize();
+ return mParceledSize;
+ } finally {
+ parcel.recycle();
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 889df51..8b09c2a 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -48,6 +48,7 @@
import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.server.notification.Flags.preventZenDeviceEffectsWhileDriving;
+import static com.android.server.notification.Flags.limitZenConfigSize;
import static java.util.Objects.requireNonNull;
@@ -192,6 +193,7 @@
private final ConditionProviders.Config mServiceConfig;
private final SystemUiSystemPropertiesFlags.FlagResolver mFlagResolver;
private final ZenModeEventLogger mZenModeEventLogger;
+ private final ZenConfigTrimmer mConfigTrimmer;
@VisibleForTesting protected int mZenMode;
@VisibleForTesting protected NotificationManager.Policy mConsolidatedPolicy;
@@ -226,6 +228,7 @@
mClock = clock;
addCallback(mMetrics);
mAppOps = context.getSystemService(AppOpsManager.class);
+ mConfigTrimmer = new ZenConfigTrimmer(mContext);
mDefaultConfig = Flags.modesUi()
? ZenModeConfig.getDefaultConfig()
@@ -2061,20 +2064,20 @@
Log.w(TAG, "Invalid config in setConfigLocked; " + config);
return false;
}
+ if (limitZenConfigSize() && (origin == ORIGIN_APP || origin == ORIGIN_USER_IN_APP)) {
+ mConfigTrimmer.trimToMaximumSize(config);
+ }
+
if (config.user != mUser) {
// simply store away for background users
- synchronized (mConfigLock) {
- mConfigs.put(config.user, config);
- }
+ mConfigs.put(config.user, config);
if (DEBUG) Log.d(TAG, "setConfigLocked: store config for user " + config.user);
return true;
}
// handle CPS backed conditions - danger! may modify config
mConditions.evaluateConfig(config, null, false /*processSubscriptions*/);
- synchronized (mConfigLock) {
- mConfigs.put(config.user, config);
- }
+ mConfigs.put(config.user, config);
if (DEBUG) Log.d(TAG, "setConfigLocked reason=" + reason, new Throwable());
ZenLog.traceConfig(origin, reason, triggeringComponent, mConfig, config, callingUid);
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 76cd5c8..346d65a 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -212,6 +212,16 @@
}
flag {
+ name: "limit_zen_config_size"
+ namespace: "systemui"
+ description: "Enforce a maximum (serialized) size for the Zen configuration"
+ bug: "387498139"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "managed_services_concurrent_multiuser"
namespace: "systemui"
description: "Enables ManagedServices to support Concurrent multi user environment"
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 80c2d41..93837b3 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -2621,20 +2621,22 @@
* Valid user is the current user or the system or in the same profile group as the current
* user. Visible background users are not valid calling users.
*/
- public static void enforceCurrentUserIfVisibleBackgroundEnabled(@UserIdInt int currentUserId) {
+ public static void enforceCurrentUserIfVisibleBackgroundEnabled() {
if (!UserManager.isVisibleBackgroundUsersEnabled()) {
return;
}
final int callingUserId = UserHandle.getCallingUserId();
- if (DBG) {
- Slog.d(LOG_TAG, "enforceValidCallingUser: callingUserId=" + callingUserId
- + " isSystemUser=" + (callingUserId == USER_SYSTEM)
- + " currentUserId=" + currentUserId
- + " callingPid=" + Binder.getCallingPid()
- + " callingUid=" + Binder.getCallingUid());
- }
final long ident = Binder.clearCallingIdentity();
try {
+ final int currentUserId = ActivityManager.getCurrentUser();
+ if (DBG) {
+ Slog.d(LOG_TAG, "enforceCurrentUserIfVisibleBackgroundEnabled:"
+ + " callingUserId=" + callingUserId
+ + " isSystemUser=" + (callingUserId == USER_SYSTEM)
+ + " currentUserId=" + currentUserId
+ + " callingPid=" + Binder.getCallingPid()
+ + " callingUid=" + Binder.getCallingUid());
+ }
if (callingUserId != USER_SYSTEM && callingUserId != currentUserId
&& !UserManagerService.getInstance()
.isSameProfileGroup(callingUserId, currentUserId)) {
@@ -2937,28 +2939,20 @@
int flags = UserManager.SWITCHABILITY_STATUS_OK;
- t.traceBegin("TM.isInCall");
- final long identity = Binder.clearCallingIdentity();
- try {
- final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
- if (com.android.internal.telephony.flags
- .Flags.enforceTelephonyFeatureMappingForPublicApis()) {
- if (mContext.getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_TELECOM)) {
- if (telecomManager != null && telecomManager.isInCall()) {
- flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
- }
- }
- } else {
+ if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM)) {
+ t.traceBegin("TM.isInCall");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ final TelecomManager telecomManager = mContext.getSystemService(
+ TelecomManager.class);
if (telecomManager != null && telecomManager.isInCall()) {
flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
}
+ } finally {
+ Binder.restoreCallingIdentity(identity);
}
- } finally {
- Binder.restoreCallingIdentity(identity);
+ t.traceEnd();
}
- t.traceEnd();
-
t.traceBegin("hasUserRestriction-DISALLOW_USER_SWITCH");
if (mLocalService.hasUserRestriction(DISALLOW_USER_SWITCH, userId)) {
flags |= UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED;
diff --git a/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java
index adf308a..29cc9ea 100644
--- a/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java
+++ b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java
@@ -143,6 +143,7 @@
/**
* Returns the maximum storage size allocated to battery history.
*/
+ @Override
public int getMaxHistorySize() {
return mMaxHistorySize;
}
diff --git a/services/core/java/com/android/server/power/stats/processor/AggregatedPowerStats.java b/services/core/java/com/android/server/power/stats/processor/AggregatedPowerStats.java
index 53894a1..f2bbdfc 100644
--- a/services/core/java/com/android/server/power/stats/processor/AggregatedPowerStats.java
+++ b/services/core/java/com/android/server/power/stats/processor/AggregatedPowerStats.java
@@ -120,16 +120,18 @@
* {@link com.android.internal.os.MonotonicClock}
* @param currentTime current time in milliseconds, see {@link System#currentTimeMillis()}
*/
- void addClockUpdate(long monotonicTime, @CurrentTimeMillisLong long currentTime) {
+ boolean addClockUpdate(long monotonicTime, @CurrentTimeMillisLong long currentTime) {
ClockUpdate clockUpdate = new ClockUpdate();
clockUpdate.monotonicTime = monotonicTime;
clockUpdate.currentTime = currentTime;
if (mClockUpdates.size() < MAX_CLOCK_UPDATES) {
mClockUpdates.add(clockUpdate);
+ return true;
} else {
Slog.i(TAG, "Too many clock updates. Replacing the previous update with "
+ DateFormat.format("yyyy-MM-dd-HH-mm-ss", currentTime));
mClockUpdates.set(mClockUpdates.size() - 1, clockUpdate);
+ return false;
}
}
diff --git a/services/core/java/com/android/server/power/stats/processor/PowerStatsAggregator.java b/services/core/java/com/android/server/power/stats/processor/PowerStatsAggregator.java
index 8461a54..b9862aa 100644
--- a/services/core/java/com/android/server/power/stats/processor/PowerStatsAggregator.java
+++ b/services/core/java/com/android/server/power/stats/processor/PowerStatsAggregator.java
@@ -98,14 +98,18 @@
if (!startedSession) {
mStats.start(item.time);
- mStats.addClockUpdate(item.time, item.currentTime);
+ if (!mStats.addClockUpdate(item.time, item.currentTime)) {
+ break;
+ }
if (baseTime == UNINITIALIZED) {
baseTime = item.time;
}
startedSession = true;
} else if (item.cmd == BatteryStats.HistoryItem.CMD_CURRENT_TIME
|| item.cmd == BatteryStats.HistoryItem.CMD_RESET) {
- mStats.addClockUpdate(item.time, item.currentTime);
+ if (!mStats.addClockUpdate(item.time, item.currentTime)) {
+ break;
+ }
}
lastTime = item.time;
@@ -164,7 +168,9 @@
consumer.accept(mStats);
}
mStats.reset();
- mStats.addClockUpdate(item.time, item.currentTime);
+ if (!mStats.addClockUpdate(item.time, item.currentTime)) {
+ break;
+ }
baseTime = lastTime = item.time;
}
mStats.addPowerStats(item.powerStats, item.time);
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index fab19b6..1afbb34 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -160,8 +160,10 @@
* @param displayId The changed display Id.
* @param rootDisplayAreaId The changed display area Id.
* @param isImmersiveMode {@code true} if the display area get into immersive mode.
+ * @param windowType The window type of the controlling window.
*/
- void immersiveModeChanged(int displayId, int rootDisplayAreaId, boolean isImmersiveMode);
+ void immersiveModeChanged(int displayId, int rootDisplayAreaId, boolean isImmersiveMode,
+ int windowType);
/**
* Show a rotation suggestion that a user may approve to rotate the screen.
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index da9d016..798c794 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -732,7 +732,7 @@
@Override
public void immersiveModeChanged(int displayId, int rootDisplayAreaId,
- boolean isImmersiveMode) {
+ boolean isImmersiveMode, int windowType) {
if (mBar == null) {
return;
}
@@ -746,7 +746,7 @@
if (!CLIENT_TRANSIENT) {
// Only call from here when the client transient is not enabled.
try {
- mBar.immersiveModeChanged(rootDisplayAreaId, isImmersiveMode);
+ mBar.immersiveModeChanged(rootDisplayAreaId, isImmersiveMode, windowType);
} catch (RemoteException ex) {
}
}
diff --git a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
index 94e52cd..d4b20fb 100644
--- a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
+++ b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
@@ -68,6 +68,10 @@
return ImmutableVolumeInfo.fromVolumeInfo(mVolumeInfo);
}
+ public ImmutableVolumeInfo getClonedImmutableVolumeInfo() {
+ return ImmutableVolumeInfo.fromVolumeInfo(mVolumeInfo.clone());
+ }
+
public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) {
return mVolumeInfo.buildStorageVolume(context, userId, reportUnmounted);
}
diff --git a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
index bda3d44..621a128 100644
--- a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
@@ -51,6 +51,7 @@
final class VendorVibrationSession extends IVibrationSession.Stub
implements VibrationSession, CancellationSignal.OnCancelListener, IBinder.DeathRecipient {
private static final String TAG = "VendorVibrationSession";
+ private static final boolean DEBUG = false;
/** Calls into VibratorManager functionality needed for playing an {@link ExternalVibration}. */
interface VibratorManagerHooks {
@@ -73,8 +74,8 @@
private final ICancellationSignal mCancellationSignal = CancellationSignal.createTransport();
private final int[] mVibratorIds;
private final long mCreateUptime;
- private final long mCreateTime; // for debugging
- private final IVibrationSessionCallback mCallback;
+ private final long mCreateTime;
+ private final VendorCallbackWrapper mCallback;
private final CallerInfo mCallerInfo;
private final VibratorManagerHooks mManagerHooks;
private final DeviceAdapter mDeviceAdapter;
@@ -88,11 +89,11 @@
@GuardedBy("mLock")
private boolean mEndedByVendor;
@GuardedBy("mLock")
- private long mStartTime; // for debugging
+ private long mStartTime;
@GuardedBy("mLock")
private long mEndUptime;
@GuardedBy("mLock")
- private long mEndTime; // for debugging
+ private long mEndTime;
@GuardedBy("mLock")
private VibrationStepConductor mConductor;
@@ -103,7 +104,7 @@
mCreateTime = System.currentTimeMillis();
mVibratorIds = deviceAdapter.getAvailableVibratorIds();
mHandler = handler;
- mCallback = callback;
+ mCallback = new VendorCallbackWrapper(callback, handler);
mCallerInfo = callerInfo;
mManagerHooks = managerHooks;
mDeviceAdapter = deviceAdapter;
@@ -119,7 +120,9 @@
@Override
public void finishSession() {
- Slog.d(TAG, "Session finish requested, ending vibration session...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session finish requested, ending vibration session...");
+ }
// Do not abort session in HAL, wait for ongoing vibration requests to complete.
// This might take a while to end the session, but it can be aborted by cancelSession.
requestEndSession(Status.FINISHED, /* shouldAbort= */ false, /* isVendorRequest= */ true);
@@ -127,7 +130,9 @@
@Override
public void cancelSession() {
- Slog.d(TAG, "Session cancel requested, aborting vibration session...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session cancel requested, aborting vibration session...");
+ }
// Always abort session in HAL while cancelling it.
// This might be triggered after finishSession was already called.
requestEndSession(Status.CANCELLED_BY_USER, /* shouldAbort= */ true,
@@ -156,7 +161,7 @@
@Override
public IBinder getCallerToken() {
- return mCallback.asBinder();
+ return mCallback.getBinderToken();
}
@Override
@@ -176,36 +181,30 @@
@Override
public void onCancel() {
- Slog.d(TAG, "Session cancellation signal received, aborting vibration session...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session cancellation signal received, aborting vibration session...");
+ }
requestEndSession(Status.CANCELLED_BY_USER, /* shouldAbort= */ true,
/* isVendorRequest= */ true);
}
@Override
public void binderDied() {
- Slog.d(TAG, "Session binder died, aborting vibration session...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session binder died, aborting vibration session...");
+ }
requestEndSession(Status.CANCELLED_BINDER_DIED, /* shouldAbort= */ true,
/* isVendorRequest= */ false);
}
@Override
public boolean linkToDeath() {
- try {
- mCallback.asBinder().linkToDeath(this, 0);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error linking session to token death", e);
- return false;
- }
- return true;
+ return mCallback.linkToDeath(this);
}
@Override
public void unlinkToDeath() {
- try {
- mCallback.asBinder().unlinkToDeath(this, 0);
- } catch (NoSuchElementException e) {
- Slog.wtf(TAG, "Failed to unlink session to token death", e);
- }
+ mCallback.unlinkToDeath(this);
}
@Override
@@ -219,26 +218,37 @@
@Override
public void notifyVibratorCallback(int vibratorId, long vibrationId, long stepId) {
- Slog.d(TAG, "Vibration callback received for vibration " + vibrationId + " step " + stepId
- + " on vibrator " + vibratorId + ", ignoring...");
+ if (DEBUG) {
+ Slog.d(TAG, "Vibration callback received for vibration " + vibrationId
+ + " step " + stepId + " on vibrator " + vibratorId + ", ignoring...");
+ }
}
@Override
public void notifySyncedVibratorsCallback(long vibrationId) {
- Slog.d(TAG, "Synced vibration callback received for vibration " + vibrationId
- + ", ignoring...");
+ if (DEBUG) {
+ Slog.d(TAG, "Synced vibration callback received for vibration " + vibrationId
+ + ", ignoring...");
+ }
}
@Override
public void notifySessionCallback() {
- Slog.d(TAG, "Session callback received, ending vibration session...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session callback received, ending vibration session...");
+ }
synchronized (mLock) {
// If end was not requested then the HAL has cancelled the session.
- maybeSetEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON,
+ notifyEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON,
/* isVendorRequest= */ false);
maybeSetStatusToRequestedLocked();
clearVibrationConductor();
- mHandler.post(() -> mManagerHooks.onSessionReleased(mSessionId));
+ final Status endStatus = mStatus;
+ mHandler.post(() -> {
+ mManagerHooks.onSessionReleased(mSessionId);
+ // Only trigger client callback after session is released in the manager.
+ mCallback.notifyFinished(endStatus);
+ });
}
}
@@ -271,7 +281,7 @@
public boolean isEnded() {
synchronized (mLock) {
- return mStatus != Status.RUNNING;
+ return mEndTime > 0;
}
}
@@ -297,19 +307,17 @@
// Session already ended, skip start callbacks.
isAlreadyEnded = true;
} else {
+ if (DEBUG) {
+ Slog.d(TAG, "Session started at the HAL");
+ }
mStartTime = System.currentTimeMillis();
- // Run client callback in separate thread.
- mHandler.post(() -> {
- try {
- mCallback.onStarted(this);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error notifying vendor session started", e);
- }
- });
+ mCallback.notifyStarted(this);
}
}
if (isAlreadyEnded) {
- Slog.d(TAG, "Session already ended after starting the HAL, aborting...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session already ended after starting the HAL, aborting...");
+ }
mHandler.post(() -> mManagerHooks.endSession(mSessionId, /* shouldAbort= */ true));
}
}
@@ -337,8 +345,10 @@
public boolean maybeSetVibrationConductor(VibrationStepConductor conductor) {
synchronized (mLock) {
if (mConductor != null) {
- Slog.d(TAG, "Session still dispatching previous vibration, new vibration "
- + conductor.getVibration().id + " ignored");
+ if (DEBUG) {
+ Slog.d(TAG, "Session still dispatching previous vibration, new vibration "
+ + conductor.getVibration().id + " ignored");
+ }
return false;
}
mConductor = conductor;
@@ -347,53 +357,45 @@
}
private void requestEndSession(Status status, boolean shouldAbort, boolean isVendorRequest) {
- Slog.d(TAG, "Session end request received with status " + status);
- boolean shouldTriggerSessionHook = false;
+ if (DEBUG) {
+ Slog.d(TAG, "Session end request received with status " + status);
+ }
synchronized (mLock) {
- maybeSetEndRequestLocked(status, isVendorRequest);
+ notifyEndRequestLocked(status, isVendorRequest);
if (!isEnded() && isStarted()) {
// Trigger session hook even if it was already triggered, in case a second request
// is aborting the ongoing/ending session. This might cause it to end right away.
// Wait for HAL callback before setting the end status.
- shouldTriggerSessionHook = true;
+ if (DEBUG) {
+ Slog.d(TAG, "Requesting HAL session end with abort=" + shouldAbort);
+ }
+ mHandler.post(() -> mManagerHooks.endSession(mSessionId, shouldAbort));
} else {
- // Session not active in the HAL, set end status right away.
+ // Session not active in the HAL, try to set end status right away.
maybeSetStatusToRequestedLocked();
+ // Use status used to end this session, which might be different from requested.
+ mCallback.notifyFinished(mStatus);
}
}
- if (shouldTriggerSessionHook) {
- Slog.d(TAG, "Requesting HAL session end with abort=" + shouldAbort);
- mHandler.post(() -> mManagerHooks.endSession(mSessionId, shouldAbort));
- }
}
@GuardedBy("mLock")
- private void maybeSetEndRequestLocked(Status status, boolean isVendorRequest) {
+ private void notifyEndRequestLocked(Status status, boolean isVendorRequest) {
if (mEndStatusRequest != null) {
- // End already requested, keep first requested status and time.
+ // End already requested, keep first requested status.
return;
}
- Slog.d(TAG, "Session end request accepted for status " + status);
+ if (DEBUG) {
+ Slog.d(TAG, "Session end request accepted for status " + status);
+ }
mEndStatusRequest = status;
mEndedByVendor = isVendorRequest;
- mEndTime = System.currentTimeMillis();
- mEndUptime = SystemClock.uptimeMillis();
+ mCallback.notifyFinishing();
if (mConductor != null) {
// Vibration is being dispatched when session end was requested, cancel it.
mConductor.notifyCancelled(new Vibration.EndInfo(status),
/* immediate= */ status != Status.FINISHED);
}
- if (isStarted()) {
- // Only trigger "finishing" callback if session started.
- // Run client callback in separate thread.
- mHandler.post(() -> {
- try {
- mCallback.onFinishing();
- } catch (RemoteException e) {
- Slog.e(TAG, "Error notifying vendor session is finishing", e);
- }
- });
- }
}
@GuardedBy("mLock")
@@ -406,40 +408,123 @@
// No end status was requested, nothing to set.
return;
}
- Slog.d(TAG, "Session end request applied for status " + mEndStatusRequest);
+ if (DEBUG) {
+ Slog.d(TAG, "Session end request applied for status " + mEndStatusRequest);
+ }
mStatus = mEndStatusRequest;
- // Run client callback in separate thread.
- final Status endStatus = mStatus;
- mHandler.post(() -> {
- try {
- mCallback.onFinished(toSessionStatus(endStatus));
- } catch (RemoteException e) {
- Slog.e(TAG, "Error notifying vendor session finished", e);
- }
- });
+ mEndTime = System.currentTimeMillis();
+ mEndUptime = SystemClock.uptimeMillis();
}
- @android.os.vibrator.VendorVibrationSession.Status
- private static int toSessionStatus(Status status) {
- // Exhaustive switch to cover all possible internal status.
- return switch (status) {
- case FINISHED
- -> android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS;
- case IGNORED_UNSUPPORTED
- -> STATUS_UNSUPPORTED;
- case CANCELLED_BINDER_DIED, CANCELLED_BY_APP_OPS, CANCELLED_BY_USER,
- CANCELLED_SUPERSEDED, CANCELLED_BY_FOREGROUND_USER, CANCELLED_BY_SCREEN_OFF,
- CANCELLED_BY_SETTINGS_UPDATE, CANCELLED_BY_UNKNOWN_REASON
- -> android.os.vibrator.VendorVibrationSession.STATUS_CANCELED;
- case IGNORED_APP_OPS, IGNORED_BACKGROUND, IGNORED_FOR_EXTERNAL, IGNORED_FOR_ONGOING,
- IGNORED_FOR_POWER, IGNORED_FOR_SETTINGS, IGNORED_FOR_HIGHER_IMPORTANCE,
- IGNORED_FOR_RINGER_MODE, IGNORED_FROM_VIRTUAL_DEVICE, IGNORED_SUPERSEDED,
- IGNORED_MISSING_PERMISSION, IGNORED_ON_WIRELESS_CHARGER
- -> android.os.vibrator.VendorVibrationSession.STATUS_IGNORED;
- case UNKNOWN, IGNORED_ERROR_APP_OPS, IGNORED_ERROR_CANCELLING, IGNORED_ERROR_SCHEDULING,
- IGNORED_ERROR_TOKEN, FORWARDED_TO_INPUT_DEVICES, FINISHED_UNEXPECTED, RUNNING
- -> android.os.vibrator.VendorVibrationSession.STATUS_UNKNOWN_ERROR;
- };
+ /**
+ * Wrapper class to handle client callbacks asynchronously.
+ *
+ * <p>This class is also responsible for link/unlink to the client process binder death, and for
+ * making sure the callbacks are only triggered once. The conversion between session status and
+ * the API status code is also defined here.
+ */
+ private static final class VendorCallbackWrapper {
+ private final IVibrationSessionCallback mCallback;
+ private final Handler mHandler;
+
+ private boolean mIsStarted;
+ private boolean mIsFinishing;
+ private boolean mIsFinished;
+
+ VendorCallbackWrapper(@NonNull IVibrationSessionCallback callback,
+ @NonNull Handler handler) {
+ mCallback = callback;
+ mHandler = handler;
+ }
+
+ synchronized IBinder getBinderToken() {
+ return mCallback.asBinder();
+ }
+
+ synchronized boolean linkToDeath(DeathRecipient recipient) {
+ try {
+ mCallback.asBinder().linkToDeath(recipient, 0);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error linking session to token death", e);
+ return false;
+ }
+ return true;
+ }
+
+ synchronized void unlinkToDeath(DeathRecipient recipient) {
+ try {
+ mCallback.asBinder().unlinkToDeath(recipient, 0);
+ } catch (NoSuchElementException e) {
+ Slog.wtf(TAG, "Failed to unlink session to token death", e);
+ }
+ }
+
+ synchronized void notifyStarted(IVibrationSession session) {
+ if (mIsStarted) {
+ return;
+ }
+ mIsStarted = true;
+ mHandler.post(() -> {
+ try {
+ mCallback.onStarted(session);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error notifying vendor session started", e);
+ }
+ });
+ }
+
+ synchronized void notifyFinishing() {
+ if (!mIsStarted || mIsFinishing || mIsFinished) {
+ // Ignore if never started or if already finishing or finished.
+ return;
+ }
+ mIsFinishing = true;
+ mHandler.post(() -> {
+ try {
+ mCallback.onFinishing();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error notifying vendor session is finishing", e);
+ }
+ });
+ }
+
+ synchronized void notifyFinished(Status status) {
+ if (mIsFinished) {
+ return;
+ }
+ mIsFinished = true;
+ mHandler.post(() -> {
+ try {
+ mCallback.onFinished(toSessionStatus(status));
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error notifying vendor session finished", e);
+ }
+ });
+ }
+
+ @android.os.vibrator.VendorVibrationSession.Status
+ private static int toSessionStatus(Status status) {
+ // Exhaustive switch to cover all possible internal status.
+ return switch (status) {
+ case FINISHED
+ -> android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS;
+ case IGNORED_UNSUPPORTED
+ -> STATUS_UNSUPPORTED;
+ case CANCELLED_BINDER_DIED, CANCELLED_BY_APP_OPS, CANCELLED_BY_USER,
+ CANCELLED_SUPERSEDED, CANCELLED_BY_FOREGROUND_USER, CANCELLED_BY_SCREEN_OFF,
+ CANCELLED_BY_SETTINGS_UPDATE, CANCELLED_BY_UNKNOWN_REASON
+ -> android.os.vibrator.VendorVibrationSession.STATUS_CANCELED;
+ case IGNORED_APP_OPS, IGNORED_BACKGROUND, IGNORED_FOR_EXTERNAL, IGNORED_FOR_ONGOING,
+ IGNORED_FOR_POWER, IGNORED_FOR_SETTINGS, IGNORED_FOR_HIGHER_IMPORTANCE,
+ IGNORED_FOR_RINGER_MODE, IGNORED_FROM_VIRTUAL_DEVICE, IGNORED_SUPERSEDED,
+ IGNORED_MISSING_PERMISSION, IGNORED_ON_WIRELESS_CHARGER
+ -> android.os.vibrator.VendorVibrationSession.STATUS_IGNORED;
+ case UNKNOWN, IGNORED_ERROR_APP_OPS, IGNORED_ERROR_CANCELLING,
+ IGNORED_ERROR_SCHEDULING, IGNORED_ERROR_TOKEN, FORWARDED_TO_INPUT_DEVICES,
+ FINISHED_UNEXPECTED, RUNNING
+ -> android.os.vibrator.VendorVibrationSession.STATUS_UNKNOWN_ERROR;
+ };
+ }
}
/**
@@ -499,7 +584,7 @@
@Override
public void logMetrics(VibratorFrameworkStatsLogger statsLogger) {
if (mStartTime > 0) {
- // Only log sessions that have started.
+ // Only log sessions that have started in the HAL.
statsLogger.logVibrationVendorSessionStarted(mCallerInfo.uid);
statsLogger.logVibrationVendorSessionVibrations(mCallerInfo.uid,
mVibrations.size());
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 12409793..57b82c3 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2326,13 +2326,16 @@
if (isActivityTypeHome()) {
// The snapshot of home is only used once because it won't be updated while screen
// is on (see {@link TaskSnapshotController#screenTurningOff}).
- mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId);
final Transition transition = mTransitionController.getCollectingTransition();
if (transition != null && (transition.getFlags()
& WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION) == 0) {
+ mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId);
// Only use snapshot of home as starting window when unlocking directly.
return false;
}
+ // Add a reference before removing snapshot from cache.
+ snapshot.addReference(TaskSnapshot.REFERENCE_WRITE_TO_PARCEL);
+ mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId);
}
return createSnapshot(snapshot, typeParameter);
}
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index aed5e14..f51e60c 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -144,9 +144,9 @@
.setPendingIntentCreatorBackgroundActivityStartMode(
MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
- private ActivityTaskManagerService mService;
+ private final ActivityTaskManagerService mService;
- private ActivityTaskSupervisor mSupervisor;
+ private final ActivityTaskSupervisor mSupervisor;
@GuardedBy("mStrictModeBalCallbacks")
private final SparseArray<ArrayMap<IBinder, IBackgroundActivityLaunchCallback>>
mStrictModeBalCallbacks = new SparseArray<>();
@@ -279,16 +279,24 @@
mSupervisor = supervisor;
}
+ private ActivityTaskManagerService getService() {
+ return mService;
+ }
+
+ private ActivityTaskSupervisor getSupervisor() {
+ return mSupervisor;
+ }
+
private boolean isHomeApp(int uid, @Nullable String packageName) {
- if (mService.mHomeProcess != null) {
+ if (getService().mHomeProcess != null) {
// Fast check
- return uid == mService.mHomeProcess.mUid;
+ return uid == getService().mHomeProcess.mUid;
}
if (packageName == null) {
return false;
}
ComponentName activity =
- mService.getPackageManagerInternalLocked()
+ getService().getPackageManagerInternalLocked()
.getDefaultHomeActivity(UserHandle.getUserId(uid));
return activity != null && packageName.equals(activity.getPackageName());
}
@@ -342,7 +350,8 @@
mAllowBalExemptionForSystemProcess = allowBalExemptionForSystemProcess;
mOriginatingPendingIntent = originatingPendingIntent;
mIntent = intent;
- mRealCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid);
+ mRealCallingPackage = getService().getPackageNameIfUnique(realCallingUid,
+ realCallingPid);
mIsCallForResult = resultRecord != null;
mCheckedOptions = checkedOptions;
@BackgroundActivityStartMode int callerBackgroundActivityStartMode =
@@ -401,13 +410,13 @@
checkedOptions, realCallingUid, mRealCallingPackage);
}
- mAppSwitchState = mService.getBalAppSwitchesState();
- mCallingUidProcState = mService.mActiveUids.getUidState(callingUid);
+ mAppSwitchState = getService().getBalAppSwitchesState();
+ mCallingUidProcState = getService().mActiveUids.getUidState(callingUid);
mIsCallingUidPersistentSystemProcess =
mCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
mCallingUidHasVisibleActivity =
- mService.mVisibleActivityProcessTracker.hasVisibleActivity(callingUid);
- mCallingUidHasNonAppVisibleWindow = mService.mActiveUids.hasNonAppVisibleWindow(
+ getService().mVisibleActivityProcessTracker.hasVisibleActivity(callingUid);
+ mCallingUidHasNonAppVisibleWindow = getService().mActiveUids.hasNonAppVisibleWindow(
callingUid);
if (realCallingUid == NO_PROCESS_UID) {
// no process provided
@@ -422,16 +431,17 @@
mRealCallingUidHasNonAppVisibleWindow = mCallingUidHasNonAppVisibleWindow;
// In the PendingIntent case callerApp is not passed in, so resolve it ourselves.
mRealCallerApp = callerApp == null
- ? mService.getProcessController(realCallingPid, realCallingUid)
+ ? getService().getProcessController(realCallingPid, realCallingUid)
: callerApp;
mIsRealCallingUidPersistentSystemProcess = mIsCallingUidPersistentSystemProcess;
} else {
- mRealCallingUidProcState = mService.mActiveUids.getUidState(realCallingUid);
+ mRealCallingUidProcState = getService().mActiveUids.getUidState(realCallingUid);
mRealCallingUidHasVisibleActivity =
- mService.mVisibleActivityProcessTracker.hasVisibleActivity(realCallingUid);
+ getService().mVisibleActivityProcessTracker.hasVisibleActivity(
+ realCallingUid);
mRealCallingUidHasNonAppVisibleWindow =
- mService.mActiveUids.hasNonAppVisibleWindow(realCallingUid);
- mRealCallerApp = mService.getProcessController(realCallingPid, realCallingUid);
+ getService().mActiveUids.hasNonAppVisibleWindow(realCallingUid);
+ mRealCallerApp = getService().getProcessController(realCallingPid, realCallingUid);
mIsRealCallingUidPersistentSystemProcess =
mRealCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
}
@@ -481,7 +491,7 @@
if (uid == 0) {
return "root[debugOnly]";
}
- String name = mService.getPackageManagerInternalLocked().getNameForUid(uid);
+ String name = getService().getPackageManagerInternalLocked().getNameForUid(uid);
if (name == null) {
name = "uid=" + uid;
}
@@ -783,7 +793,7 @@
Process.getAppUidForSdkSandboxUid(state.mRealCallingUid);
// realCallingSdkSandboxUidToAppUid should probably just be used instead (or in addition
// to realCallingUid when calculating resultForRealCaller below.
- if (mService.hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) {
+ if (getService().hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) {
state.setResultForRealCaller(new BalVerdict(BAL_ALLOW_SDK_SANDBOX,
/*background*/ false,
"uid in SDK sandbox has visible (non-toast) window"));
@@ -1000,30 +1010,28 @@
* or {@link #BAL_BLOCK} if the launch should be blocked
*/
BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) {
+ boolean evaluateVisibleOnly = balAdditionalStartModes()
+ && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
+ == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE;
+ if (evaluateVisibleOnly) {
+ return evaluateChain(state, mCheckCallerVisible, mCheckCallerNonAppVisible,
+ mCheckCallerProcessAllowsForeground);
+ }
if (state.isPendingIntent()) {
// PendingIntents should mostly be allowed by the sender (real caller) or a permission
// the creator of the PendingIntent has. Visibility should be the exceptional case, so
// test it last (this does not change the result, just the bal code).
- BalVerdict result = BalVerdict.BLOCK;
- if (!(balAdditionalStartModes()
- && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
- == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) {
- result = checkBackgroundActivityStartAllowedByCallerInBackground(state);
- }
- if (result == BalVerdict.BLOCK) {
- result = checkBackgroundActivityStartAllowedByCallerInForeground(state);
-
- }
- return result;
- } else {
- BalVerdict result = checkBackgroundActivityStartAllowedByCallerInForeground(state);
- if (result == BalVerdict.BLOCK && !(balAdditionalStartModes()
- && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
- == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) {
- result = checkBackgroundActivityStartAllowedByCallerInBackground(state);
- }
- return result;
+ return evaluateChain(state, mCheckCallerIsAllowlistedUid,
+ mCheckCallerIsAllowlistedComponent, mCheckCallerHasBackgroundPermission,
+ mCheckCallerHasSawPermission, mCheckCallerHasBgStartAppOp,
+ mCheckCallerProcessAllowsBackground, mCheckCallerVisible,
+ mCheckCallerNonAppVisible, mCheckCallerProcessAllowsForeground);
}
+ return evaluateChain(state, mCheckCallerVisible, mCheckCallerNonAppVisible,
+ mCheckCallerProcessAllowsForeground, mCheckCallerIsAllowlistedUid,
+ mCheckCallerIsAllowlistedComponent, mCheckCallerHasBackgroundPermission,
+ mCheckCallerHasSawPermission, mCheckCallerHasBgStartAppOp,
+ mCheckCallerProcessAllowsBackground);
}
interface BalExemptionCheck {
@@ -1061,7 +1069,7 @@
if (state.mCallingUidHasNonAppVisibleWindow) {
return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
/*background*/ false, "callingUid has non-app visible window "
- + mService.mActiveUids.getNonAppVisibleWindowDetails(state.mCallingUid));
+ + getService().mActiveUids.getNonAppVisibleWindowDetails(state.mCallingUid));
}
return BalVerdict.BLOCK;
};
@@ -1090,7 +1098,7 @@
final int callingAppId = UserHandle.getAppId(state.mCallingUid);
// IME should always be allowed to start activity, like IME settings.
final WindowState imeWindow =
- mService.mRootWindowContainer.getCurrentInputMethodWindow();
+ getService().mRootWindowContainer.getCurrentInputMethodWindow();
if (imeWindow != null && callingAppId == imeWindow.mOwnerUid) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ false,
@@ -1104,23 +1112,23 @@
}
// don't abort if the caller has the same uid as the recents component
- if (mSupervisor.mRecentTasks.isCallerRecents(state.mCallingUid)) {
+ if (getSupervisor().mRecentTasks.isCallerRecents(state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Recents Component");
}
// don't abort if the callingUid is the device owner
- if (mService.isDeviceOwner(state.mCallingUid)) {
+ if (getService().isDeviceOwner(state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Device Owner");
}
// don't abort if the callingUid is a affiliated profile owner
- if (mService.isAffiliatedProfileOwner(state.mCallingUid)) {
+ if (getService().isAffiliatedProfileOwner(state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Affiliated Profile Owner");
}
// don't abort if the callingUid has companion device
final int callingUserId = UserHandle.getUserId(state.mCallingUid);
- if (mService.isAssociatedCompanionApp(callingUserId, state.mCallingUid)) {
+ if (getService().isAssociatedCompanionApp(callingUserId, state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Companion App");
}
@@ -1138,7 +1146,7 @@
};
private final BalExemptionCheck mCheckCallerHasSawPermission = state -> {
// don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission
- if (mService.hasSystemAlertWindowPermission(state.mCallingUid, state.mCallingPid,
+ if (getService().hasSystemAlertWindowPermission(state.mCallingUid, state.mCallingPid,
state.mCallingPackage)) {
return new BalVerdict(BAL_ALLOW_SAW_PERMISSION,
/*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted");
@@ -1148,7 +1156,7 @@
private final BalExemptionCheck mCheckCallerHasBgStartAppOp = state -> {
// don't abort if the callingUid and callingPackage have the
// OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop
- if (isSystemExemptFlagEnabled() && mService.getAppOpsManager().checkOpNoThrow(
+ if (isSystemExemptFlagEnabled() && getService().getAppOpsManager().checkOpNoThrow(
AppOpsManager.OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION,
state.mCallingUid, state.mCallingPackage) == AppOpsManager.MODE_ALLOWED) {
return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true,
@@ -1170,34 +1178,18 @@
* @return A code denoting which BAL rule allows an activity to be started,
* or {@link #BAL_BLOCK} if the launch should be blocked
*/
- BalVerdict checkBackgroundActivityStartAllowedByCallerInForeground(BalState state) {
- return evaluateChain(state, mCheckCallerVisible, mCheckCallerNonAppVisible,
- mCheckCallerProcessAllowsForeground);
- }
-
- /**
- * @return A code denoting which BAL rule allows an activity to be started,
- * or {@link #BAL_BLOCK} if the launch should be blocked
- */
- BalVerdict checkBackgroundActivityStartAllowedByCallerInBackground(BalState state) {
- return evaluateChain(state, mCheckCallerIsAllowlistedUid,
- mCheckCallerIsAllowlistedComponent, mCheckCallerHasBackgroundPermission,
- mCheckCallerHasSawPermission, mCheckCallerHasBgStartAppOp,
- mCheckCallerProcessAllowsBackground);
- }
-
- /**
- * @return A code denoting which BAL rule allows an activity to be started,
- * or {@link #BAL_BLOCK} if the launch should be blocked
- */
BalVerdict checkBackgroundActivityStartAllowedByRealCaller(BalState state) {
- BalVerdict result = checkBackgroundActivityStartAllowedByRealCallerInForeground(state);
- if (result == BalVerdict.BLOCK && !(balAdditionalStartModes()
+ boolean evaluateVisibleOnly = balAdditionalStartModes()
&& state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
- == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) {
- result = checkBackgroundActivityStartAllowedByRealCallerInBackground(state);
+ == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE;
+ if (evaluateVisibleOnly) {
+ return evaluateChain(state, mCheckRealCallerVisible, mCheckRealCallerNonAppVisible,
+ mCheckRealCallerProcessAllowsBalForeground);
}
- return result;
+ return evaluateChain(state, mCheckRealCallerVisible, mCheckRealCallerNonAppVisible,
+ mCheckRealCallerProcessAllowsBalForeground, mCheckRealCallerBalPermission,
+ mCheckRealCallerSawPermission, mCheckRealCallerAllowlistedUid,
+ mCheckRealCallerAllowlistedComponent, mCheckRealCallerProcessAllowsBalBackground);
}
private final BalExemptionCheck mCheckRealCallerVisible = state -> {
@@ -1218,21 +1210,20 @@
if (state.mRealCallingUidHasNonAppVisibleWindow) {
return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
/*background*/ false, "realCallingUid has non-app visible window "
- + mService.mActiveUids.getNonAppVisibleWindowDetails(state.mRealCallingUid));
+ + getService().mActiveUids.getNonAppVisibleWindowDetails(
+ state.mRealCallingUid));
}
return BalVerdict.BLOCK;
};
- private final BalExemptionCheck mCheckRealCallerProcessAllowsBalForeground = state -> {
- // Don't abort if the realCallerApp or other processes of that uid are considered to be in
- // the foreground.
- return checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_FOREGROUND);
- };
+ // Don't abort if the realCallerApp or other processes of that uid are considered to be in
+ // the foreground.
+ private final BalExemptionCheck mCheckRealCallerProcessAllowsBalForeground =
+ state -> checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_FOREGROUND);
- private final BalExemptionCheck mCheckRealCallerProcessAllowsBalBackground = state -> {
- // don't abort if the callerApp or other processes of that uid are allowed in any way
- return checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_BACKGROUND);
- };
+ // don't abort if the callerApp or other processes of that uid are allowed in any way
+ private final BalExemptionCheck mCheckRealCallerProcessAllowsBalBackground =
+ state -> checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_BACKGROUND);
private final BalExemptionCheck mCheckRealCallerBalPermission = state -> {
boolean allowAlways = state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
@@ -1251,7 +1242,7 @@
== MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
// don't abort if the realCallingUid has SYSTEM_ALERT_WINDOW permission
if (allowAlways
- && mService.hasSystemAlertWindowPermission(state.mRealCallingUid,
+ && getService().hasSystemAlertWindowPermission(state.mRealCallingUid,
state.mRealCallingPid, state.mRealCallingPackage)) {
return new BalVerdict(BAL_ALLOW_SAW_PERMISSION,
/*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted");
@@ -1276,7 +1267,7 @@
private final BalExemptionCheck mCheckRealCallerAllowlistedComponent = state -> {
// don't abort if the realCallingUid is an associated companion app
- if (mService.isAssociatedCompanionApp(
+ if (getService().isAssociatedCompanionApp(
UserHandle.getUserId(state.mRealCallingUid), state.mRealCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ false,
@@ -1285,25 +1276,6 @@
return BalVerdict.BLOCK;
};
- /**
- * @return A code denoting which BAL rule allows an activity to be started,
- * or {@link #BAL_BLOCK} if the launch should be blocked
- */
- BalVerdict checkBackgroundActivityStartAllowedByRealCallerInForeground(BalState state) {
- return evaluateChain(state, mCheckRealCallerVisible, mCheckRealCallerNonAppVisible,
- mCheckRealCallerProcessAllowsBalForeground);
- }
-
- /**
- * @return A code denoting which BAL rule allows an activity to be started,
- * or {@link #BAL_BLOCK} if the launch should be blocked
- */
- BalVerdict checkBackgroundActivityStartAllowedByRealCallerInBackground(BalState state) {
- return evaluateChain(state, mCheckRealCallerBalPermission, mCheckRealCallerSawPermission,
- mCheckRealCallerAllowlistedUid, mCheckRealCallerAllowlistedComponent,
- mCheckRealCallerProcessAllowsBalBackground);
- }
-
@VisibleForTesting boolean hasBalPermission(int uid, int pid) {
return ActivityTaskManagerService.checkPermission(START_ACTIVITIES_FROM_BACKGROUND,
pid, uid) == PERMISSION_GRANTED;
@@ -1329,7 +1301,7 @@
} else {
// only if that one wasn't allowed, check the other ones
final ArraySet<WindowProcessController> uidProcesses =
- mService.mProcessMap.getProcesses(app.mUid);
+ getService().mProcessMap.getProcesses(app.mUid);
if (uidProcesses != null) {
for (int i = uidProcesses.size() - 1; i >= 0; i--) {
final WindowProcessController proc = uidProcesses.valueAt(i);
@@ -1500,7 +1472,7 @@
if (ActivitySecurityModelFeatureFlags.shouldShowToast(callingUid)) {
String toastText = ActivitySecurityModelFeatureFlags.DOC_LINK
+ (enforceBlock ? " blocked " : " would block ")
- + getApplicationLabel(mService.mContext.getPackageManager(),
+ + getApplicationLabel(getService().mContext.getPackageManager(),
launchedFromPackageName);
showToast(toastText);
@@ -1522,7 +1494,7 @@
}
@VisibleForTesting void showToast(String toastText) {
- UiThread.getHandler().post(() -> Toast.makeText(mService.mContext,
+ UiThread.getHandler().post(() -> Toast.makeText(getService().mContext,
toastText, Toast.LENGTH_LONG).show());
}
@@ -1599,7 +1571,7 @@
return;
}
- String packageName = mService.mContext.getPackageManager().getNameForUid(callingUid);
+ String packageName = getService().mContext.getPackageManager().getNameForUid(callingUid);
BalState state = new BalState(callingUid, callingPid, packageName, INVALID_UID,
INVALID_PID, null, null, false, null, null, ActivityOptions.makeBasic());
@BalCode int balCode = checkBackgroundActivityStartAllowedByCaller(state).mCode;
@@ -1660,7 +1632,7 @@
boolean restrictActivitySwitch = ActivitySecurityModelFeatureFlags
.shouldRestrictActivitySwitch(callingUid) && bas.mTopActivityOptedIn;
- PackageManager pm = mService.mContext.getPackageManager();
+ PackageManager pm = getService().mContext.getPackageManager();
String callingPackage = pm.getNameForUid(callingUid);
final CharSequence callingLabel;
if (callingPackage == null) {
@@ -1821,7 +1793,7 @@
return bas.optedIn(ar);
}
- PackageManager pm = mService.mContext.getPackageManager();
+ PackageManager pm = getService().mContext.getPackageManager();
ApplicationInfo applicationInfo;
final int sourceUserId = UserHandle.getUserId(sourceUid);
@@ -1878,7 +1850,7 @@
if (sourceRecord == null) {
joiner.add(prefix + "Source Package: " + targetRecord.launchedFromPackage);
- String realCallingPackage = mService.mContext.getPackageManager().getNameForUid(
+ String realCallingPackage = getService().mContext.getPackageManager().getNameForUid(
realCallingUid);
joiner.add(prefix + "Real Calling Uid Package: " + realCallingPackage);
} else {
@@ -1913,7 +1885,7 @@
joiner.add(prefix + "BalCode: " + balCodeToString(balCode));
joiner.add(prefix + "Allowed By Grace Period: " + allowedByGracePeriod);
joiner.add(prefix + "LastResumedActivity: "
- + recordToString.apply(mService.mLastResumedActivity));
+ + recordToString.apply(getService().mLastResumedActivity));
joiner.add(prefix + "System opted into enforcement: " + asmOptSystemIntoEnforcement());
if (mTopFinishedActivity != null) {
@@ -1986,7 +1958,7 @@
}
private BalVerdict statsLog(BalVerdict finalVerdict, BalState state) {
- if (finalVerdict.blocks() && mService.isActivityStartsLoggingEnabled()) {
+ if (finalVerdict.blocks() && getService().isActivityStartsLoggingEnabled()) {
// log aborted activity start to TRON
mSupervisor
.getActivityMetricsLogger()
@@ -2222,7 +2194,7 @@
return -1;
}
try {
- PackageManager pm = mService.mContext.getPackageManager();
+ PackageManager pm = getService().mContext.getPackageManager();
return pm.getTargetSdkVersion(packageName);
} catch (Exception e) {
return -1;
@@ -2243,8 +2215,8 @@
this.mLaunchCount = entry == null || !ar.isUid(entry.mUid) ? 1 : entry.mLaunchCount + 1;
this.mDebugInfo = getDebugStringForActivityRecord(ar);
- mService.mH.postDelayed(() -> {
- synchronized (mService.mGlobalLock) {
+ getService().mH.postDelayed(() -> {
+ synchronized (getService().mGlobalLock) {
if (mTaskIdToFinishedActivity.get(taskId) == this) {
mTaskIdToFinishedActivity.remove(taskId);
}
diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java
index c2255d8..dc42b32 100644
--- a/services/core/java/com/android/server/wm/DesktopModeHelper.java
+++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java
@@ -79,7 +79,7 @@
}
@VisibleForTesting
- static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
+ public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
if (!shouldEnforceDeviceRestrictions()) {
return true;
}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index a874ef6..50f12c3 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -157,6 +157,7 @@
import static com.android.server.wm.utils.RegionUtils.forEachRectReverse;
import static com.android.server.wm.utils.RegionUtils.rectListToRegion;
import static com.android.window.flags.Flags.enablePersistingDensityScaleForConnectedDisplays;
+import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -3835,13 +3836,18 @@
/**
* Looking for the focused window on this display if the top focused display hasn't been
- * found yet (topFocusedDisplayId is INVALID_DISPLAY) or per-display focused was allowed.
+ * found yet (topFocusedDisplayId is INVALID_DISPLAY), per-display focused was allowed, or
+ * the display is presenting. The last one is needed to update system bar visibility in response
+ * to presentation visibility because per-display focus is needed to change system bar
+ * visibility, but the display shouldn't get global focus when a presentation gets shown.
*
* @param topFocusedDisplayId Id of the top focused display.
* @return The focused window or null if there isn't any or no need to seek.
*/
WindowState findFocusedWindowIfNeeded(int topFocusedDisplayId) {
- return (hasOwnFocus() || topFocusedDisplayId == INVALID_DISPLAY)
+ return (hasOwnFocus() || topFocusedDisplayId == INVALID_DISPLAY
+ || (enablePresentationForConnectedDisplays()
+ && mWmService.mPresentationController.isPresentationVisible(mDisplayId)))
? findFocusedWindow() : null;
}
@@ -6932,6 +6938,8 @@
/** The actual requested visible inset types for this display */
private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
+ private @InsetsType int mAnimatingTypes = 0;
+
/** The component name of the top focused window on this display */
private ComponentName mTopFocusedComponentName = null;
@@ -7069,6 +7077,18 @@
}
return 0;
}
+
+ @Override
+ public @InsetsType int getAnimatingTypes() {
+ return mAnimatingTypes;
+ }
+
+ @Override
+ public void setAnimatingTypes(@InsetsType int animatingTypes) {
+ if (mAnimatingTypes != animatingTypes) {
+ mAnimatingTypes = animatingTypes;
+ }
+ }
}
MagnificationSpec getMagnificationSpec() {
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 4908df0..ec5b503f 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -2564,7 +2564,7 @@
final int rootDisplayAreaId = root == null ? FEATURE_UNDEFINED : root.mFeatureId;
// TODO(b/277290737): Move this to the client side, instead of using a proxy.
callStatusBarSafely(statusBar -> statusBar.immersiveModeChanged(getDisplayId(),
- rootDisplayAreaId, isImmersiveMode));
+ rootDisplayAreaId, isImmersiveMode, win.getWindowType()));
}
// Show transient bars for panic if needed.
diff --git a/services/core/java/com/android/server/wm/InsetsControlTarget.java b/services/core/java/com/android/server/wm/InsetsControlTarget.java
index cee4967..6462a37 100644
--- a/services/core/java/com/android/server/wm/InsetsControlTarget.java
+++ b/services/core/java/com/android/server/wm/InsetsControlTarget.java
@@ -97,6 +97,20 @@
@NonNull ImeTracker.Token statsToken) {
}
+ /**
+ * @return {@link WindowInsets.Type.InsetsType}s which are currently animating (showing or
+ * hiding).
+ */
+ default @InsetsType int getAnimatingTypes() {
+ return 0;
+ }
+
+ /**
+ * @param animatingTypes the {@link InsetsType}s, that are currently animating
+ */
+ default void setAnimatingTypes(@InsetsType int animatingTypes) {
+ }
+
/** Returns {@code target.getWindow()}, or null if {@code target} is {@code null}. */
static WindowState asWindowOrNull(InsetsControlTarget target) {
return target != null ? target.getWindow() : null;
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 009d482..2872214 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -790,8 +790,6 @@
private final Handler mHandler;
private final String mName;
- private boolean mInsetsAnimationRunning;
-
Host(Handler handler, String name) {
mHandler = handler;
mName = name;
@@ -901,10 +899,5 @@
public IBinder getWindowToken() {
return null;
}
-
- @Override
- public void notifyAnimationRunningStateChanged(boolean running) {
- mInsetsAnimationRunning = running;
- }
}
}
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 164abab..5e0395f 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -225,13 +225,16 @@
for (int i = mProviders.size() - 1; i >= 0; i--) {
final InsetsSourceProvider provider = mProviders.valueAt(i);
final @InsetsType int type = provider.getSource().getType();
+ final boolean isImeProvider = type == WindowInsets.Type.ime();
if ((type & changedTypes) != 0) {
- final boolean isImeProvider = type == WindowInsets.Type.ime();
changed |= provider.updateClientVisibility(
- caller, isImeProvider ? statsToken : null)
+ caller, isImeProvider ? statsToken : null)
// Fake control target cannot change the client visibility, but it should
// change the insets with its newly requested visibility.
|| (caller == provider.getFakeControlTarget());
+ } else if (isImeProvider && android.view.inputmethod.Flags.refactorInsetsController()) {
+ ImeTracker.forLogging().onCancelled(statsToken,
+ ImeTracker.PHASE_WM_SET_REMOTE_TARGET_IME_VISIBILITY);
}
}
if (changed) {
diff --git a/services/core/java/com/android/server/wm/PresentationController.java b/services/core/java/com/android/server/wm/PresentationController.java
index b3cff9c..acc658b 100644
--- a/services/core/java/com/android/server/wm/PresentationController.java
+++ b/services/core/java/com/android/server/wm/PresentationController.java
@@ -16,10 +16,17 @@
package com.android.server.wm;
+import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
+import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;
+
+import static com.android.internal.protolog.WmProtoLogGroups.WM_ERROR;
import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
import android.annotation.NonNull;
-import android.util.IntArray;
+import android.annotation.Nullable;
+import android.hardware.display.DisplayManager;
+import android.util.SparseArray;
+import android.view.WindowManager.LayoutParams.WindowType;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.protolog.WmProtoLogGroups;
@@ -27,15 +34,125 @@
/**
* Manages presentation windows.
*/
-class PresentationController {
+class PresentationController implements DisplayManager.DisplayListener {
- // TODO(b/395475549): Add support for display add/remove, and activity move across displays.
- private final IntArray mPresentingDisplayIds = new IntArray();
+ private static class Presentation {
+ @NonNull final WindowState mWin;
+ @NonNull final WindowContainerListener mPresentationListener;
+ // This is the task which started this presentation. This shouldn't be null in most cases
+ // because the intended usage of the Presentation API is that an activity that started a
+ // presentation should control the UI and lifecycle of the presentation window.
+ // However, the API doesn't necessarily requires a host activity to exist (e.g. a background
+ // service can launch a presentation), so this can be null.
+ @Nullable final Task mHostTask;
+ @Nullable final WindowContainerListener mHostTaskListener;
- PresentationController() {}
+ Presentation(@NonNull WindowState win,
+ @NonNull WindowContainerListener presentationListener,
+ @Nullable Task hostTask,
+ @Nullable WindowContainerListener hostTaskListener) {
+ mWin = win;
+ mPresentationListener = presentationListener;
+ mHostTask = hostTask;
+ mHostTaskListener = hostTaskListener;
+ }
- private boolean isPresenting(int displayId) {
- return mPresentingDisplayIds.contains(displayId);
+ @Override
+ public String toString() {
+ return "{win: " + mWin.getName() + ", display: " + mWin.getDisplayId()
+ + ", hostTask: " + (mHostTask != null ? mHostTask.getName() : null) + "}";
+ }
+ }
+
+ private final SparseArray<Presentation> mPresentations = new SparseArray();
+
+ @Nullable
+ private Presentation getPresentation(@Nullable WindowState win) {
+ if (win == null) return null;
+ for (int i = 0; i < mPresentations.size(); i++) {
+ final Presentation presentation = mPresentations.valueAt(i);
+ if (win == presentation.mWin) return presentation;
+ }
+ return null;
+ }
+
+ private boolean hasPresentationWindow(int displayId) {
+ return mPresentations.contains(displayId);
+ }
+
+ boolean isPresentationVisible(int displayId) {
+ final Presentation presentation = mPresentations.get(displayId);
+ return presentation != null && presentation.mWin.mToken.isVisibleRequested();
+ }
+
+ boolean canPresent(@NonNull WindowState win, @NonNull DisplayContent displayContent) {
+ return canPresent(win, displayContent, win.mAttrs.type, win.getUid());
+ }
+
+ /**
+ * Checks if a presentation window can be shown on the given display.
+ * If the given |win| is empty, a new presentation window is being created.
+ * If the given |win| is not empty, the window already exists as presentation, and we're
+ * revalidate if the |win| is still qualified to be shown.
+ */
+ boolean canPresent(@Nullable WindowState win, @NonNull DisplayContent displayContent,
+ @WindowType int type, int uid) {
+ if (type == TYPE_PRIVATE_PRESENTATION) {
+ // Private presentations can only be created on private displays.
+ return displayContent.isPrivate();
+ }
+
+ if (type != TYPE_PRESENTATION) {
+ return false;
+ }
+
+ if (!enablePresentationForConnectedDisplays()) {
+ return displayContent.getDisplay().isPublicPresentation();
+ }
+
+ boolean allDisplaysArePresenting = true;
+ for (int i = 0; i < displayContent.mWmService.mRoot.mChildren.size(); i++) {
+ final DisplayContent dc = displayContent.mWmService.mRoot.mChildren.get(i);
+ if (displayContent.mDisplayId != dc.mDisplayId
+ && !mPresentations.contains(dc.mDisplayId)) {
+ allDisplaysArePresenting = false;
+ break;
+ }
+ }
+ if (allDisplaysArePresenting) {
+ // All displays can't present simultaneously.
+ return false;
+ }
+
+ final int displayId = displayContent.mDisplayId;
+ if (hasPresentationWindow(displayId)
+ && win != null && win != mPresentations.get(displayId).mWin) {
+ // A display can't have multiple presentations.
+ return false;
+ }
+
+ Task hostTask = null;
+ final Presentation presentation = getPresentation(win);
+ if (presentation != null) {
+ hostTask = presentation.mHostTask;
+ } else if (win == null) {
+ final Task globallyFocusedTask =
+ displayContent.mWmService.mRoot.getTopDisplayFocusedRootTask();
+ if (globallyFocusedTask != null && uid == globallyFocusedTask.effectiveUid) {
+ hostTask = globallyFocusedTask;
+ }
+ }
+ if (hostTask != null && displayId == hostTask.getDisplayId()) {
+ // A presentation can't cover its own host task.
+ return false;
+ }
+ if (hostTask == null && !displayContent.getDisplay().isPublicPresentation()) {
+ // A globally focused host task on a different display is needed to show a
+ // presentation on a non-presenting display.
+ return false;
+ }
+
+ return true;
}
boolean shouldOccludeActivities(int displayId) {
@@ -45,32 +162,87 @@
// be shown on them.
// TODO(b/390481621): Disallow a presentation from covering its controlling activity so that
// the presentation won't stop its controlling activity.
- return enablePresentationForConnectedDisplays() && isPresenting(displayId);
+ return enablePresentationForConnectedDisplays() && isPresentationVisible(displayId);
}
- void onPresentationAdded(@NonNull WindowState win) {
+ void onPresentationAdded(@NonNull WindowState win, int uid) {
final int displayId = win.getDisplayId();
- if (isPresenting(displayId)) {
- return;
- }
ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Presentation added to display %d: %s",
- win.getDisplayId(), win);
- mPresentingDisplayIds.add(win.getDisplayId());
+ displayId, win);
win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ true);
+
+ final WindowContainerListener presentationWindowListener = new WindowContainerListener() {
+ @Override
+ public void onRemoved() {
+ if (!hasPresentationWindow(displayId)) {
+ ProtoLog.e(WM_ERROR, "Failed to remove presentation on"
+ + "non-presenting display %d: %s", displayId, win);
+ return;
+ }
+ final Presentation presentation = mPresentations.get(displayId);
+ win.mToken.unregisterWindowContainerListener(presentation.mPresentationListener);
+ if (presentation.mHostTask != null) {
+ presentation.mHostTask.unregisterWindowContainerListener(
+ presentation.mHostTaskListener);
+ }
+ mPresentations.remove(displayId);
+ win.mWmService.mDisplayManagerInternal.onPresentation(displayId, false /*isShown*/);
+ }
+ };
+ win.mToken.registerWindowContainerListener(presentationWindowListener);
+
+ Task hostTask = null;
+ if (enablePresentationForConnectedDisplays()) {
+ final Task globallyFocusedTask =
+ win.mWmService.mRoot.getTopDisplayFocusedRootTask();
+ if (globallyFocusedTask != null && uid == globallyFocusedTask.effectiveUid) {
+ hostTask = globallyFocusedTask;
+ }
+ }
+ WindowContainerListener hostTaskListener = null;
+ if (hostTask != null) {
+ hostTaskListener = new WindowContainerListener() {
+ public void onDisplayChanged(DisplayContent dc) {
+ final Presentation presentation = mPresentations.get(dc.getDisplayId());
+ if (presentation != null && !canPresent(presentation.mWin, dc)) {
+ removePresentation(dc.mDisplayId, "host task moved to display "
+ + dc.getDisplayId());
+ }
+ }
+
+ public void onRemoved() {
+ removePresentation(win.getDisplayId(), "host task removed");
+ }
+ };
+ hostTask.registerWindowContainerListener(hostTaskListener);
+ }
+
+ mPresentations.put(displayId, new Presentation(win, presentationWindowListener, hostTask,
+ hostTaskListener));
}
- void onPresentationRemoved(@NonNull WindowState win) {
- final int displayId = win.getDisplayId();
- if (!isPresenting(displayId)) {
- return;
+ void removePresentation(int displayId, @NonNull String reason) {
+ final Presentation presentation = mPresentations.get(displayId);
+ if (enablePresentationForConnectedDisplays() && presentation != null) {
+ ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Removing Presentation %s for "
+ + "reason %s", mPresentations.get(displayId), reason);
+ final WindowState win = presentation.mWin;
+ win.mWmService.mAtmService.mH.post(() -> {
+ synchronized (win.mWmService.mGlobalLock) {
+ win.removeIfPossible();
+ }
+ });
}
- ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION,
- "Presentation removed from display %d: %s", win.getDisplayId(), win);
- // TODO(b/393945496): Make sure that there's one presentation at most per display.
- final int displayIdIndex = mPresentingDisplayIds.indexOf(displayId);
- if (displayIdIndex != -1) {
- mPresentingDisplayIds.remove(displayIdIndex);
- }
- win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ false);
}
+
+ @Override
+ public void onDisplayAdded(int displayId) {}
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ removePresentation(displayId, "display removed " + displayId);
+ }
+
+ @Override
+ public void onDisplayChanged(int displayId) {}
}
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 8d198b2..3ed16db 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -737,6 +737,17 @@
}
}
+ @Override
+ public void updateAnimatingTypes(IWindow window, @InsetsType int animatingTypes) {
+ synchronized (mService.mGlobalLock) {
+ final WindowState win = mService.windowForClientLocked(this, window,
+ false /* throwOnError */);
+ if (win != null) {
+ win.setAnimatingTypes(animatingTypes);
+ }
+ }
+ }
+
void onWindowAdded(WindowState w) {
if (mPackageName == null) {
mPackageName = mProcess.mInfo.packageName;
@@ -1015,15 +1026,4 @@
}
}
}
-
- @Override
- public void notifyInsetsAnimationRunningStateChanged(IWindow window, boolean running) {
- synchronized (mService.mGlobalLock) {
- final WindowState win = mService.windowForClientLocked(this, window,
- false /* throwOnError */);
- if (win != null) {
- win.notifyInsetsAnimationRunningStateChanged(running);
- }
- }
- }
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 821c040..28f2825 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1583,14 +1583,18 @@
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
- if (type == TYPE_PRIVATE_PRESENTATION && !displayContent.isPrivate()) {
+ if (type == TYPE_PRIVATE_PRESENTATION
+ && !mPresentationController.canPresent(null /*win*/, displayContent, type,
+ callingUid)) {
ProtoLog.w(WM_ERROR,
"Attempted to add private presentation window to a non-private display. "
+ "Aborting.");
return WindowManagerGlobal.ADD_PERMISSION_DENIED;
}
- if (type == TYPE_PRESENTATION && !displayContent.getDisplay().isPublicPresentation()) {
+ if (type == TYPE_PRESENTATION
+ && !mPresentationController.canPresent(null /*win*/, displayContent, type,
+ callingUid)) {
ProtoLog.w(WM_ERROR,
"Attempted to add presentation window to a non-suitable display. "
+ "Aborting.");
@@ -1830,7 +1834,8 @@
}
win.mTransitionController.collect(win.mToken);
res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState,
- outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs);
+ outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs,
+ callingUid);
// A presentation hides all activities behind on the same display.
win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null,
/*notifyClients=*/ true);
@@ -1841,7 +1846,8 @@
}
} else {
res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState,
- outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs);
+ outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs,
+ callingUid);
}
}
@@ -1854,7 +1860,7 @@
@NonNull ActivityRecord activity, @NonNull DisplayContent displayContent,
@NonNull InsetsState outInsetsState, @NonNull Rect outAttachedFrame,
@NonNull InsetsSourceControl.Array outActiveControls, @NonNull IWindow client,
- @NonNull float[] outSizeCompatScale, @NonNull LayoutParams attrs) {
+ @NonNull float[] outSizeCompatScale, @NonNull LayoutParams attrs, int uid) {
int res = 0;
final int type = attrs.type;
boolean imMayMove = true;
@@ -1971,7 +1977,7 @@
outSizeCompatScale[0] = win.getCompatScaleForClient();
if (res >= ADD_OKAY && win.isPresentation()) {
- mPresentationController.onPresentationAdded(win);
+ mPresentationController.onPresentationAdded(win, uid);
}
return res;
@@ -4767,6 +4773,26 @@
}
}
+ @EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS)
+ @Override
+ public void updateDisplayWindowAnimatingTypes(int displayId, @InsetsType int animatingTypes) {
+ updateDisplayWindowAnimatingTypes_enforcePermission();
+ if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ synchronized (mGlobalLock) {
+ final DisplayContent dc = mRoot.getDisplayContent(displayId);
+ if (dc == null || dc.mRemoteInsetsControlTarget == null) {
+ return;
+ }
+ dc.mRemoteInsetsControlTarget.setAnimatingTypes(animatingTypes);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+ }
+ }
+
@Override
public int watchRotation(IRotationWatcher watcher, int displayId) {
final DisplayContent displayContent;
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index ec67dd87..3b7d312 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -736,6 +736,8 @@
private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
+ private @InsetsType int mAnimatingTypes = 0;
+
/**
* Freeze the insets state in some cases that not necessarily keeps up-to-date to the client.
* (e.g app exiting transition)
@@ -842,6 +844,27 @@
mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask);
}
+ @Override
+ public @InsetsType int getAnimatingTypes() {
+ return mAnimatingTypes;
+ }
+
+ @Override
+ public void setAnimatingTypes(@InsetsType int animatingTypes) {
+ if (mAnimatingTypes != animatingTypes) {
+ if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+ Trace.instant(TRACE_TAG_WINDOW_MANAGER,
+ TextUtils.formatSimple("%s: setAnimatingTypes(%s)",
+ getName(),
+ animatingTypes));
+ }
+ mInsetsAnimationRunning = animatingTypes != 0;
+ mWmService.scheduleAnimationLocked();
+
+ mAnimatingTypes = animatingTypes;
+ }
+ }
+
/**
* Set a freeze state for the window to ignore dispatching its insets state to the client.
*
@@ -2435,7 +2458,6 @@
mAnimatingExit = true;
mRemoveOnExit = true;
mToken.setVisibleRequested(false);
- mWmService.mPresentationController.onPresentationRemoved(this);
// A presentation hides all activities behind on the same display.
mDisplayContent.ensureActivitiesVisible(/*starting=*/ null,
/*notifyClients=*/ true);
@@ -2656,7 +2678,7 @@
// The client gave us a touchable region and so first
// we calculate the untouchable region, then punch that out of our
// expanded modal region.
- mTmpRegion.set(0, 0, frame.right, frame.bottom);
+ mTmpRegion.set(0, 0, frame.width(), frame.height());
mTmpRegion.op(mGivenTouchableRegion, Region.Op.DIFFERENCE);
region.op(mTmpRegion, Region.Op.DIFFERENCE);
}
@@ -6079,17 +6101,6 @@
mWmService.scheduleAnimationLocked();
}
- void notifyInsetsAnimationRunningStateChanged(boolean running) {
- if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
- Trace.instant(TRACE_TAG_WINDOW_MANAGER,
- TextUtils.formatSimple("%s: notifyInsetsAnimationRunningStateChanged(%s)",
- getName(),
- Boolean.toString(running)));
- }
- mInsetsAnimationRunning = running;
- mWmService.scheduleAnimationLocked();
- }
-
boolean isInsetsAnimationRunning() {
return mInsetsAnimationRunning;
}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index e158310..860b6fb 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1814,7 +1814,7 @@
t.traceEnd();
}
- if (!isWatch && !isTv && !isAutomotive
+ if (!isWatch && !isTv && !isAutomotive && !isDesktop
&& android.security.Flags.aapmApi()) {
t.traceBegin("StartAdvancedProtectionService");
mSystemServiceManager.startService(AdvancedProtectionService.Lifecycle.class);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
index 5eb23a2..1286648 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
@@ -16,29 +16,43 @@
package com.android.server.am;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import android.annotation.NonNull;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
+import android.app.BackgroundStartPrivileges;
+import android.app.BroadcastOptions;
+import android.app.SystemServiceRegistry;
import android.app.usage.UsageStatsManagerInternal;
import android.content.ComponentName;
import android.content.Context;
+import android.content.IIntentReceiver;
+import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
+import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.TestLooperManager;
import android.os.UserHandle;
+import android.permission.IPermissionManager;
+import android.permission.PermissionManager;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.platform.test.flag.junit.SetFlagsRule;
@@ -47,7 +61,6 @@
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.internal.util.FrameworkStatsLog;
import com.android.modules.utils.testing.ExtendedMockitoRule;
import com.android.server.AlarmManagerInternal;
@@ -55,6 +68,7 @@
import com.android.server.LocalServices;
import com.android.server.appop.AppOpsService;
import com.android.server.compat.PlatformCompat;
+import com.android.server.firewall.IntentFirewall;
import com.android.server.wm.ActivityTaskManagerService;
import org.junit.Rule;
@@ -63,8 +77,11 @@
import org.mockito.MockitoAnnotations;
import java.io.File;
+import java.util.Collections;
+import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
public abstract class BaseBroadcastQueueTest {
@@ -97,6 +114,8 @@
public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
.spyStatic(FrameworkStatsLog.class)
.spyStatic(ProcessList.class)
+ .spyStatic(SystemServiceRegistry.class)
+ .mockStatic(AppGlobals.class)
.build();
@@ -119,6 +138,16 @@
ProcessList mProcessList;
@Mock
PlatformCompat mPlatformCompat;
+ @Mock
+ IntentFirewall mIntentFirewall;
+ @Mock
+ IPackageManager mIPackageManager;
+ @Mock
+ AppOpsManager mAppOpsManager;
+ @Mock
+ IPermissionManager mIPermissionManager;
+ @Mock
+ PermissionManager mPermissionManager;
@Mock
AppStartInfoTracker mAppStartInfoTracker;
@@ -167,22 +196,22 @@
return getUidForPackage(invocation.getArgument(0));
}).when(mPackageManagerInt).getPackageUid(any(), anyLong(), eq(UserHandle.USER_SYSTEM));
+ final Context spyContext = spy(mContext);
+ doReturn(mPermissionManager).when(spyContext).getSystemService(PermissionManager.class);
final ActivityManagerService realAms = new ActivityManagerService(
- new TestInjector(mContext), mServiceThreadRule.getThread());
+ new TestInjector(spyContext), mServiceThreadRule.getThread());
realAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
realAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
realAms.mAtmInternal = spy(realAms.mActivityTaskManager.getAtmInternal());
realAms.mOomAdjuster.mCachedAppOptimizer = mock(CachedAppOptimizer.class);
realAms.mOomAdjuster = spy(realAms.mOomAdjuster);
- ExtendedMockito.doNothing().when(() -> ProcessList.setOomAdj(anyInt(), anyInt(), anyInt()));
+ doNothing().when(() -> ProcessList.setOomAdj(anyInt(), anyInt(), anyInt()));
realAms.mPackageManagerInt = mPackageManagerInt;
realAms.mUsageStatsService = mUsageStatsManagerInt;
realAms.mProcessesReady = true;
mAms = spy(realAms);
- mSkipPolicy = spy(new BroadcastSkipPolicy(mAms));
- doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any());
- doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any());
+ mSkipPolicy = createBroadcastSkipPolicy();
doReturn(mAppStartInfoTracker).when(mProcessList).getAppStartInfoTracker();
@@ -198,6 +227,14 @@
}
}
+ public BroadcastSkipPolicy createBroadcastSkipPolicy() {
+ final BroadcastSkipPolicy skipPolicy = spy(new BroadcastSkipPolicy(mAms));
+ doReturn(null).when(skipPolicy).shouldSkipAtEnqueueMessage(any(), any());
+ doReturn(null).when(skipPolicy).shouldSkipMessage(any(), any());
+ doReturn(false).when(skipPolicy).disallowBackgroundStart(any());
+ return skipPolicy;
+ }
+
static int getUidForPackage(@NonNull String packageName) {
switch (packageName) {
case PACKAGE_ANDROID: return android.os.Process.SYSTEM_UID;
@@ -240,6 +277,11 @@
public BroadcastQueue getBroadcastQueue(ActivityManagerService service) {
return null;
}
+
+ @Override
+ public IntentFirewall getIntentFirewall() {
+ return mIntentFirewall;
+ }
}
abstract String getTag();
@@ -281,24 +323,35 @@
ri.activityInfo.packageName = packageName;
ri.activityInfo.processName = processName;
ri.activityInfo.name = name;
+ ri.activityInfo.exported = true;
ri.activityInfo.applicationInfo = makeApplicationInfo(packageName, processName, userId);
return ri;
}
+ // TODO: Reuse BroadcastQueueTest.makeActiveProcessRecord()
+ @SuppressWarnings("GuardedBy")
+ ProcessRecord makeProcessRecord(ApplicationInfo info) {
+ final ProcessRecord r = spy(new ProcessRecord(mAms, info, info.processName, info.uid));
+ r.setPid(mNextPid.incrementAndGet());
+ ProcessRecord.updateProcessRecordNodes(r);
+ return r;
+ }
+
BroadcastFilter makeRegisteredReceiver(ProcessRecord app) {
return makeRegisteredReceiver(app, 0);
}
BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) {
final ReceiverList receiverList = mRegisteredReceivers.get(app.getPid());
- return makeRegisteredReceiver(receiverList, priority);
+ return makeRegisteredReceiver(receiverList, priority, null);
}
- static BroadcastFilter makeRegisteredReceiver(ReceiverList receiverList, int priority) {
+ static BroadcastFilter makeRegisteredReceiver(ReceiverList receiverList, int priority,
+ String requiredPermission) {
final IntentFilter filter = new IntentFilter();
filter.setPriority(priority);
final BroadcastFilter res = new BroadcastFilter(filter, receiverList,
- receiverList.app.info.packageName, null, null, null, receiverList.uid,
+ receiverList.app.info.packageName, null, null, requiredPermission, receiverList.uid,
receiverList.userId, false, false, true, receiverList.app.info,
mock(PlatformCompat.class));
receiverList.add(res);
@@ -313,4 +366,62 @@
ArgumentMatcher<ApplicationInfo> appInfoEquals(int uid) {
return test -> (test.uid == uid);
}
+
+ static final class BroadcastRecordBuilder {
+ private BroadcastQueue mQueue = mock(BroadcastQueue.class);
+ private Intent mIntent = mock(Intent.class);
+ private ProcessRecord mProcessRecord = mock(ProcessRecord.class);
+ private String mCallerPackage;
+ private String mCallerFeatureId;
+ private int mCallingPid;
+ private int mCallingUid;
+ private boolean mCallerInstantApp;
+ private String mResolvedType;
+ private String[] mRequiredPermissions;
+ private String[] mExcludedPermissions;
+ private String[] mExcludedPackages;
+ private int mAppOp;
+ private BroadcastOptions mOptions = BroadcastOptions.makeBasic();
+ private List mReceivers = Collections.emptyList();
+ private ProcessRecord mResultToApp;
+ private IIntentReceiver mResultTo;
+ private int mResultCode = Activity.RESULT_OK;
+ private String mResultData;
+ private Bundle mResultExtras;
+ private boolean mSerialized;
+ private boolean mSticky;
+ private boolean mInitialSticky;
+ private int mUserId = UserHandle.USER_SYSTEM;
+ private BackgroundStartPrivileges mBackgroundStartPrivileges =
+ BackgroundStartPrivileges.NONE;
+ private boolean mTimeoutExempt;
+ private BiFunction<Integer, Bundle, Bundle> mFilterExtrasForReceiver;
+ private int mCallerAppProcState = ActivityManager.PROCESS_STATE_UNKNOWN;
+ private PlatformCompat mPlatformCompat = mock(PlatformCompat.class);
+
+ public BroadcastRecordBuilder setIntent(Intent intent) {
+ mIntent = intent;
+ return this;
+ }
+
+ public BroadcastRecordBuilder setRequiredPermissions(String[] requiredPermissions) {
+ mRequiredPermissions = requiredPermissions;
+ return this;
+ }
+
+ public BroadcastRecordBuilder setAppOp(int appOp) {
+ mAppOp = appOp;
+ return this;
+ }
+
+ public BroadcastRecord build() {
+ return new BroadcastRecord(mQueue, mIntent, mProcessRecord, mCallerPackage,
+ mCallerFeatureId, mCallingPid, mCallingUid, mCallerInstantApp, mResolvedType,
+ mRequiredPermissions, mExcludedPermissions, mExcludedPackages, mAppOp,
+ mOptions, mReceivers, mResultToApp, mResultTo, mResultCode, mResultData,
+ mResultExtras, mSerialized, mSticky, mInitialSticky, mUserId,
+ mBackgroundStartPrivileges, mTimeoutExempt, mFilterExtrasForReceiver,
+ mCallerAppProcState, mPlatformCompat);
+ }
+ }
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
index 409706b..b32ce49 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
@@ -1803,6 +1803,46 @@
assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked());
}
+ @SuppressWarnings("GuardedBy")
+ @DisableFlags(Flags.FLAG_AVOID_NOTE_OP_AT_ENQUEUE)
+ @Test
+ public void testSkipPolicy_atEnqueueTime_flagDisabled() throws Exception {
+ final Intent userPresent = new Intent(Intent.ACTION_USER_PRESENT);
+ final Object greenReceiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN);
+ final Object redReceiver = makeManifestReceiver(PACKAGE_RED, CLASS_RED);
+
+ final BroadcastRecord userPresentRecord = makeBroadcastRecord(userPresent,
+ List.of(greenReceiver, redReceiver));
+
+ final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+ final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick,
+ List.of(greenReceiver, redReceiver));
+
+ doAnswer(invocation -> {
+ final BroadcastRecord r = invocation.getArgument(0);
+ final Object o = invocation.getArgument(1);
+ if (userPresent.getAction().equals(r.intent.getAction())
+ && isReceiverEquals(o, greenReceiver)) {
+ return "receiver skipped by test";
+ }
+ return null;
+ }).when(mSkipPolicy).shouldSkipMessage(any(BroadcastRecord.class), any());
+
+ mImpl.enqueueBroadcastLocked(userPresentRecord);
+ mImpl.enqueueBroadcastLocked(timeTickRecord);
+
+ final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+ getUidForPackage(PACKAGE_GREEN));
+ // There should be only one broadcast for green process as the other would have
+ // been skipped.
+ verifyPendingRecords(greenQueue, List.of(timeTick));
+ final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+ getUidForPackage(PACKAGE_RED));
+ verifyPendingRecords(redQueue, List.of(userPresent, timeTick));
+ }
+
+ @SuppressWarnings("GuardedBy")
+ @EnableFlags(Flags.FLAG_AVOID_NOTE_OP_AT_ENQUEUE)
@Test
public void testSkipPolicy_atEnqueueTime() throws Exception {
final Intent userPresent = new Intent(Intent.ACTION_USER_PRESENT);
@@ -1824,7 +1864,7 @@
return "receiver skipped by test";
}
return null;
- }).when(mSkipPolicy).shouldSkipMessage(any(BroadcastRecord.class), any());
+ }).when(mSkipPolicy).shouldSkipAtEnqueueMessage(any(BroadcastRecord.class), any());
mImpl.enqueueBroadcastLocked(userPresentRecord);
mImpl.enqueueBroadcastLocked(timeTickRecord);
@@ -2270,19 +2310,11 @@
assertFalse(mImpl.isProcessFreezable(greenProcess));
}
- // TODO: Reuse BroadcastQueueTest.makeActiveProcessRecord()
- private ProcessRecord makeProcessRecord(ApplicationInfo info) {
- final ProcessRecord r = spy(new ProcessRecord(mAms, info, info.processName, info.uid));
- r.setPid(mNextPid.incrementAndGet());
- ProcessRecord.updateProcessRecordNodes(r);
- return r;
- }
-
BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) {
final IIntentReceiver receiver = mock(IIntentReceiver.class);
final ReceiverList receiverList = new ReceiverList(mAms, app, app.getPid(), app.info.uid,
UserHandle.getUserId(app.info.uid), receiver);
- return makeRegisteredReceiver(receiverList, priority);
+ return makeRegisteredReceiver(receiverList, priority, null /* requiredPermission */);
}
private Intent createPackageChangedIntent(int uid, List<String> componentNameList) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index ad35b25..3a9c99d 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -2301,6 +2301,52 @@
}
/**
+ * Verify that we skip broadcasts at enqueue if {@link BroadcastSkipPolicy} decides it
+ * should be skipped.
+ */
+ @EnableFlags(Flags.FLAG_AVOID_NOTE_OP_AT_ENQUEUE)
+ @Test
+ public void testSkipPolicy_atEnqueueTime() throws Exception {
+ final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+ final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
+ final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+
+ final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ final Object greenReceiver = makeRegisteredReceiver(receiverGreenApp);
+ final Object blueReceiver = makeRegisteredReceiver(receiverBlueApp);
+ final Object yellowReceiver = makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW);
+ final Object orangeReceiver = makeManifestReceiver(PACKAGE_ORANGE, CLASS_ORANGE);
+
+ doAnswer(invocation -> {
+ final BroadcastRecord r = invocation.getArgument(0);
+ final Object o = invocation.getArgument(1);
+ if (airplane.getAction().equals(r.intent.getAction())
+ && (isReceiverEquals(o, greenReceiver)
+ || isReceiverEquals(o, orangeReceiver))) {
+ return "test skipped receiver";
+ }
+ return null;
+ }).when(mSkipPolicy).shouldSkipAtEnqueueMessage(any(BroadcastRecord.class), any());
+ enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+ List.of(greenReceiver, blueReceiver, yellowReceiver, orangeReceiver)));
+
+ waitForIdle();
+ // Verify that only blue and yellow receiver apps received the broadcast.
+ verifyScheduleRegisteredReceiver(never(), receiverGreenApp, USER_SYSTEM);
+ verify(mSkipPolicy, never()).shouldSkipMessage(any(BroadcastRecord.class),
+ eq(greenReceiver));
+ verifyScheduleRegisteredReceiver(receiverBlueApp, airplane);
+ final ProcessRecord receiverYellowApp = mAms.getProcessRecordLocked(PACKAGE_YELLOW,
+ getUidForPackage(PACKAGE_YELLOW));
+ verifyScheduleReceiver(receiverYellowApp, airplane);
+ final ProcessRecord receiverOrangeApp = mAms.getProcessRecordLocked(PACKAGE_ORANGE,
+ getUidForPackage(PACKAGE_ORANGE));
+ assertNull(receiverOrangeApp);
+ verify(mSkipPolicy, never()).shouldSkipMessage(any(BroadcastRecord.class),
+ eq(orangeReceiver));
+ }
+
+ /**
* Verify broadcasts to runtime receivers in cached processes are deferred
* until that process leaves the cached state.
*/
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java
new file mode 100644
index 0000000..c8aad79e
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.never;
+
+import android.Manifest;
+import android.app.ActivityManager;
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
+import android.content.IIntentReceiver;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@SmallTest
+public class BroadcastSkipPolicyTest extends BaseBroadcastQueueTest {
+ private static final String TAG = "BroadcastSkipPolicyTest";
+
+ BroadcastSkipPolicy mBroadcastSkipPolicy;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mBroadcastSkipPolicy = new BroadcastSkipPolicy(mAms);
+
+ doReturn(true).when(mIntentFirewall).checkBroadcast(any(Intent.class),
+ anyInt(), anyInt(), nullable(String.class), anyInt());
+
+ doReturn(mIPackageManager).when(AppGlobals::getPackageManager);
+ doReturn(true).when(mIPackageManager).isPackageAvailable(anyString(), anyInt());
+
+ doReturn(ActivityManager.APP_START_MODE_NORMAL).when(mAms).getAppStartModeLOSP(anyInt(),
+ anyString(), anyInt(), anyInt(), eq(true), eq(false), eq(false));
+
+ doReturn(mAppOpsManager).when(mAms).getAppOpsManager();
+ doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager).checkOpNoThrow(anyString(),
+ anyInt(), anyString(), nullable(String.class));
+ doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager).noteOpNoThrow(anyString(),
+ anyInt(), anyString(), nullable(String.class), anyString());
+
+ doReturn(mIPermissionManager).when(AppGlobals::getPermissionManager);
+ doReturn(PackageManager.PERMISSION_GRANTED).when(mIPermissionManager).checkUidPermission(
+ anyInt(), anyString(), anyInt());
+ }
+
+ @Override
+ public String getTag() {
+ return TAG;
+ }
+
+ @Override
+ public BroadcastSkipPolicy createBroadcastSkipPolicy() {
+ return new BroadcastSkipPolicy(mAms);
+ }
+
+ @Test
+ public void testShouldSkipMessage_withManifestRcvr_withCompPerm_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .build();
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+ makeManifestReceiverWithPermission(PACKAGE_GREEN, CLASS_GREEN,
+ Manifest.permission.PACKAGE_USAGE_STATS));
+ assertNull(msg);
+ verify(mAppOpsManager).noteOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId),
+ anyString());
+ verify(mAppOpsManager, never()).checkOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class));
+ }
+
+ @Test
+ public void testShouldSkipMessage_withRegRcvr_withCompPerm_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+ makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ Manifest.permission.PACKAGE_USAGE_STATS));
+ assertNull(msg);
+ verify(mAppOpsManager).noteOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId),
+ anyString());
+ verify(mAppOpsManager, never()).checkOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class));
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withManifestRcvr_withCompPerm_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .build();
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+ makeManifestReceiverWithPermission(PACKAGE_GREEN, CLASS_GREEN,
+ Manifest.permission.PACKAGE_USAGE_STATS));
+ assertNull(msg);
+ verify(mAppOpsManager).checkOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId));
+ verify(mAppOpsManager, never()).noteOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withRegRcvr_withCompPerm_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+ makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ Manifest.permission.PACKAGE_USAGE_STATS));
+ assertNull(msg);
+ verify(mAppOpsManager).checkOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId));
+ verify(mAppOpsManager, never()).noteOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+ }
+
+ @Test
+ public void testShouldSkipMessage_withManifestRcvr_withAppOp_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+ .build();
+ final ResolveInfo receiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN);
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record, receiver);
+ assertNull(msg);
+ verify(mAppOpsManager).noteOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(receiver.activityInfo.applicationInfo.uid),
+ eq(receiver.activityInfo.packageName), nullable(String.class), anyString());
+ verify(mAppOpsManager, never()).checkOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class));
+ }
+
+ @Test
+ public void testShouldSkipMessage_withRegRcvr_withAppOp_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final BroadcastFilter filter = makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ null /* requiredPermission */);
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record, filter);
+ assertNull(msg);
+ verify(mAppOpsManager).noteOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(filter.receiverList.uid),
+ eq(filter.packageName), nullable(String.class), anyString());
+ verify(mAppOpsManager, never()).checkOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class));
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withManifestRcvr_withAppOp_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+ .build();
+ final ResolveInfo receiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN);
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record, receiver);
+ assertNull(msg);
+ verify(mAppOpsManager).checkOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(receiver.activityInfo.applicationInfo.uid),
+ eq(receiver.activityInfo.applicationInfo.packageName), nullable(String.class));
+ verify(mAppOpsManager, never()).noteOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withRegRcvr_withAppOp_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final BroadcastFilter filter = makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ null /* requiredPermission */);
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record, filter);
+ assertNull(msg);
+ verify(mAppOpsManager).checkOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(filter.receiverList.uid),
+ eq(filter.packageName), nullable(String.class));
+ verify(mAppOpsManager, never()).noteOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+ }
+
+ @Test
+ public void testShouldSkipMessage_withManifestRcvr_withRequiredPerms_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+ .build();
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+ makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN));
+ assertNull(msg);
+ verify(mPermissionManager).checkPermissionForDataDelivery(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+ verify(mPermissionManager, never()).checkPermissionForPreflight(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+ }
+
+ @Test
+ public void testShouldSkipMessage_withRegRcvr_withRequiredPerms_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+ makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ null /* requiredPermission */));
+ assertNull(msg);
+ verify(mPermissionManager).checkPermissionForDataDelivery(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+ verify(mPermissionManager, never()).checkPermissionForPreflight(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withManifestRcvr_withRequiredPerms_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+ .build();
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+ makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN));
+ assertNull(msg);
+ verify(mPermissionManager, never()).checkPermissionForDataDelivery(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+ verify(mPermissionManager).checkPermissionForPreflight(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withRegRcvr_withRequiredPerms_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+ makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ null /* requiredPermission */));
+ assertNull(msg);
+ verify(mPermissionManager, never()).checkPermissionForDataDelivery(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+ verify(mPermissionManager).checkPermissionForPreflight(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+ }
+
+ private ResolveInfo makeManifestReceiverWithPermission(String packageName, String name,
+ String permission) {
+ final ResolveInfo resolveInfo = makeManifestReceiver(packageName, name);
+ resolveInfo.activityInfo.permission = permission;
+ return resolveInfo;
+ }
+
+ private BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority,
+ String requiredPermission) {
+ final IIntentReceiver receiver = mock(IIntentReceiver.class);
+ final ReceiverList receiverList = new ReceiverList(mAms, app, app.getPid(), app.info.uid,
+ UserHandle.getUserId(app.info.uid), receiver);
+ return makeRegisteredReceiver(receiverList, priority, requiredPermission);
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index bada337..6b8ef88 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -64,7 +64,6 @@
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.pm.ServiceInfo;
-import android.content.res.Resources;
import android.graphics.Color;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
@@ -95,6 +94,7 @@
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.LocalServices;
+import com.android.server.wm.DesktopModeHelper;
import com.android.server.wm.WindowManagerInternal;
import org.hamcrest.CoreMatchers;
@@ -155,8 +155,6 @@
private IPackageManager mIpm = AppGlobals.getPackageManager();
- private Resources mResources = sContext.getResources();
-
@Mock
private DisplayManager mDisplayManager;
@@ -178,6 +176,7 @@
.spyStatic(WallpaperUtils.class)
.spyStatic(LocalServices.class)
.spyStatic(WallpaperManager.class)
+ .spyStatic(DesktopModeHelper.class)
.startMocking();
sWindowManagerInternal = mock(WindowManagerInternal.class);
@@ -246,6 +245,8 @@
int userId = (invocation.getArgument(0));
return getWallpaperTestDir(userId);
}).when(() -> WallpaperUtils.getWallpaperDir(anyInt()));
+ ExtendedMockito.doAnswer(invocation -> true).when(
+ () -> DesktopModeHelper.isDeviceEligibleForDesktopMode(any()));
sContext.addMockSystemService(DisplayManager.class, mDisplayManager);
@@ -257,10 +258,6 @@
doReturn(displays).when(mDisplayManager).getDisplays();
spyOn(mIpm);
- spyOn(mResources);
- doReturn(true).when(mResources).getBoolean(eq(R.bool.config_isDesktopModeSupported));
- doReturn(true).when(mResources).getBoolean(
- eq(R.bool.config_canInternalDisplayHostDesktops));
mService = new TestWallpaperManagerService(sContext);
spyOn(mService);
mService.systemReady();
diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
index a4e77c0..1de864c 100644
--- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
+++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
@@ -17,9 +17,9 @@
package com.android.server.location.contexthub;
import static com.google.common.truth.Truth.assertThat;
-
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -33,15 +33,21 @@
import android.hardware.contexthub.IContextHubEndpoint;
import android.hardware.contexthub.IContextHubEndpointCallback;
import android.hardware.contexthub.IEndpointCommunication;
+import android.hardware.contexthub.Message;
import android.hardware.contexthub.MessageDeliveryStatus;
import android.hardware.contexthub.Reason;
+import android.hardware.location.IContextHubTransactionCallback;
+import android.hardware.location.NanoAppState;
import android.os.Binder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
-
+import android.util.Log;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
+import java.util.Collections;
+import java.util.List;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -51,11 +57,11 @@
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
-import java.util.Collections;
-
@RunWith(AndroidJUnit4.class)
@Presubmit
public class ContextHubEndpointTest {
+ private static final String TAG = "ContextHubEndpointTest";
+
private static final int SESSION_ID_RANGE = ContextHubEndpointManager.SERVICE_SESSION_RANGE;
private static final int MIN_SESSION_ID = 0;
private static final int MAX_SESSION_ID = MIN_SESSION_ID + SESSION_ID_RANGE - 1;
@@ -206,6 +212,68 @@
assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
}
+ @Test
+ public void testMessageTransaction() throws RemoteException {
+ IContextHubEndpoint endpoint = registerExampleEndpoint();
+ testMessageTransactionInternal(endpoint, /* deliverMessageStatus= */ true);
+
+ unregisterExampleEndpoint(endpoint);
+ }
+
+ @Test
+ public void testMessageTransactionCleanupOnUnregistration() throws RemoteException {
+ IContextHubEndpoint endpoint = registerExampleEndpoint();
+ testMessageTransactionInternal(endpoint, /* deliverMessageStatus= */ false);
+
+ unregisterExampleEndpoint(endpoint);
+ assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(0);
+ }
+
+ /** A helper method to create a session and validates reliable message sending. */
+ private void testMessageTransactionInternal(
+ IContextHubEndpoint endpoint, boolean deliverMessageStatus) throws RemoteException {
+ HubEndpointInfo targetInfo =
+ new HubEndpointInfo(
+ TARGET_ENDPOINT_NAME,
+ TARGET_ENDPOINT_ID,
+ ENDPOINT_PACKAGE_NAME,
+ Collections.emptyList());
+ int sessionId = endpoint.openSession(targetInfo, /* serviceDescriptor= */ null);
+ mEndpointManager.onEndpointSessionOpenComplete(sessionId);
+
+ final int messageType = 1234;
+ HubMessage message =
+ new HubMessage.Builder(messageType, new byte[] {1, 2, 3, 4, 5})
+ .setResponseRequired(true)
+ .build();
+ IContextHubTransactionCallback callback =
+ new IContextHubTransactionCallback.Stub() {
+ @Override
+ public void onQueryResponse(int result, List<NanoAppState> nanoappList) {
+ Log.i(TAG, "Received onQueryResponse callback, result=" + result);
+ }
+
+ @Override
+ public void onTransactionComplete(int result) {
+ Log.i(TAG, "Received onTransactionComplete callback, result=" + result);
+ }
+ };
+ endpoint.sendMessage(sessionId, message, callback);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+ verify(mMockEndpointCommunications, timeout(1000))
+ .sendMessageToEndpoint(eq(sessionId), messageCaptor.capture());
+ Message halMessage = messageCaptor.getValue();
+ assertThat(halMessage.type).isEqualTo(message.getMessageType());
+ assertThat(halMessage.content).isEqualTo(message.getMessageBody());
+ assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(1);
+
+ if (deliverMessageStatus) {
+ mEndpointManager.onMessageDeliveryStatusReceived(
+ sessionId, halMessage.sequenceNumber, ErrorCode.OK);
+ assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(0);
+ }
+ }
+
private IContextHubEndpoint registerExampleEndpoint() throws RemoteException {
HubEndpointInfo info =
new HubEndpointInfo(
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
index b332331..6b989cb 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
@@ -38,6 +38,7 @@
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.Condition;
+import com.android.internal.R;
import com.android.server.UiServiceTestCase;
import org.junit.Before;
@@ -46,6 +47,8 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.List;
+
public class ConditionProvidersTest extends UiServiceTestCase {
private ConditionProviders mProviders;
@@ -169,4 +172,15 @@
assertTrue(mProviders.getApproved(userId, true).isEmpty());
}
+
+ @Test
+ public void getDefaultDndAccessPackages_returnsPackages() {
+ mContext.getOrCreateTestableResources().addOverride(
+ R.string.config_defaultDndAccessPackages,
+ "com.example.a:com.example.b::::com.example.c");
+
+ List<String> packages = ConditionProviders.getDefaultDndAccessPackages(mContext);
+
+ assertThat(packages).containsExactly("com.example.a", "com.example.b", "com.example.c");
+ }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java
new file mode 100644
index 0000000..154a905
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.ZenRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class ZenConfigTrimmerTest extends UiServiceTestCase {
+
+ private static final String TRUSTED_PACKAGE = "com.trust.me";
+ private static final int ONE_PERCENT = 1_500;
+
+ private ZenConfigTrimmer mTrimmer;
+
+ @Before
+ public void setUp() {
+ mContext.getOrCreateTestableResources().addOverride(
+ R.string.config_defaultDndAccessPackages, TRUSTED_PACKAGE);
+
+ mTrimmer = new ZenConfigTrimmer(mContext);
+ }
+
+ @Test
+ public void trimToMaximumSize_belowMax_untouched() {
+ ZenModeConfig config = new ZenModeConfig();
+ addZenRule(config, "1", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "2", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "3", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "4", "pkg2", 20 * ONE_PERCENT);
+ addZenRule(config, "5", "pkg2", 20 * ONE_PERCENT);
+
+ mTrimmer.trimToMaximumSize(config);
+
+ assertThat(config.automaticRules.keySet()).containsExactly("1", "2", "3", "4", "5");
+ }
+
+ @Test
+ public void trimToMaximumSize_exceedsMax_removesAllRulesOfLargestPackages() {
+ ZenModeConfig config = new ZenModeConfig();
+ addZenRule(config, "1", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "2", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "3", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "4", "pkg2", 20 * ONE_PERCENT);
+ addZenRule(config, "5", "pkg2", 20 * ONE_PERCENT);
+ addZenRule(config, "6", "pkg3", 35 * ONE_PERCENT);
+ addZenRule(config, "7", "pkg4", 38 * ONE_PERCENT);
+
+ mTrimmer.trimToMaximumSize(config);
+
+ assertThat(config.automaticRules.keySet()).containsExactly("1", "2", "3", "6");
+ assertThat(config.automaticRules.values().stream().map(r -> r.pkg).distinct())
+ .containsExactly("pkg1", "pkg3");
+ }
+
+ @Test
+ public void trimToMaximumSize_keepsRulesFromTrustedPackages() {
+ ZenModeConfig config = new ZenModeConfig();
+ addZenRule(config, "1", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "2", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "3", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "4", TRUSTED_PACKAGE, 60 * ONE_PERCENT);
+ addZenRule(config, "5", "pkg2", 20 * ONE_PERCENT);
+ addZenRule(config, "6", "pkg3", 35 * ONE_PERCENT);
+
+ mTrimmer.trimToMaximumSize(config);
+
+ assertThat(config.automaticRules.keySet()).containsExactly("4", "5");
+ assertThat(config.automaticRules.values().stream().map(r -> r.pkg).distinct())
+ .containsExactly(TRUSTED_PACKAGE, "pkg2");
+ }
+
+ /**
+ * Create a ZenRule that, when serialized to a Parcel, will take <em>approximately</em>
+ * {@code desiredSize} bytes (within 100 bytes). Try to make the tests not rely on a very tight
+ * fit.
+ */
+ private static void addZenRule(ZenModeConfig config, String id, String pkg, int desiredSize) {
+ ZenRule rule = new ZenRule();
+ rule.id = id;
+ rule.pkg = pkg;
+ config.automaticRules.put(id, rule);
+
+ // Make the ZenRule as large as desired. Not to the exact byte, because otherwise this
+ // test would have to be adjusted whenever we change the parceling of ZenRule in any way.
+ // (Still might need adjustment if we change the serialization _significantly_).
+ int nameLength = desiredSize - id.length() - pkg.length() - 232;
+ rule.name = "A".repeat(nameLength);
+
+ Parcel verification = Parcel.obtain();
+ try {
+ verification.writeParcelable(rule, 0);
+ assertThat(verification.dataSize()).isWithin(100).of(desiredSize);
+ } finally {
+ verification.recycle();
+ }
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index f8387a4..51891ef 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -90,6 +90,7 @@
import static com.android.os.dnd.DNDProtoEnums.ROOT_CONFIG;
import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW;
import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW;
+import static com.android.server.notification.Flags.FLAG_LIMIT_ZEN_CONFIG_SIZE;
import static com.android.server.notification.Flags.FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING;
import static com.android.server.notification.ZenModeEventLogger.ACTIVE_RULE_TYPE_MANUAL;
import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE;
@@ -236,6 +237,7 @@
@SmallTest
@SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service.
@RunWith(ParameterizedAndroidJunit4.class)
+@EnableFlags(FLAG_LIMIT_ZEN_CONFIG_SIZE) // Should be parameterization, but off path does nothing.
@TestableLooper.RunWithLooper
public class ZenModeHelperTest extends UiServiceTestCase {
@@ -7480,6 +7482,45 @@
assertThat(getZenRule(ruleId).lastActivation).isNull();
}
+ @Test
+ @EnableFlags(FLAG_LIMIT_ZEN_CONFIG_SIZE)
+ public void addAutomaticZenRule_trimsConfiguration() {
+ mZenModeHelper.mConfig.automaticRules.clear();
+ AutomaticZenRule smallRule = new AutomaticZenRule.Builder("Reasonable", CONDITION_ID)
+ .setConfigurationActivity(new ComponentName(mPkg, "cls"))
+ .build();
+ AutomaticZenRule systemRule = new AutomaticZenRule.Builder("System", CONDITION_ID)
+ .setOwner(new ComponentName("android", "ScheduleConditionProvider"))
+ .build();
+
+ AutomaticZenRule bigRule = new AutomaticZenRule.Builder("Yuge", CONDITION_ID)
+ .setConfigurationActivity(new ComponentName("evil.package", "cls"))
+ .setTriggerDescription("0123456789".repeat(6000)) // ~60k bytes utf16.
+ .build();
+
+ String systemRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "android",
+ systemRule, ORIGIN_SYSTEM, "add", SYSTEM_UID);
+ String smallRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, smallRule,
+ ORIGIN_APP, "add", CUSTOM_PKG_UID);
+ String bigRuleId1 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "evil.package",
+ bigRule, ORIGIN_APP, "add", CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(
+ systemRuleId, smallRuleId, bigRuleId1);
+
+ String bigRuleId2 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "evil.package",
+ bigRule, ORIGIN_APP, "add", CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(
+ systemRuleId, smallRuleId, bigRuleId1, bigRuleId2);
+
+ // This should go over the threshold
+ String bigRuleId3 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "evil.package",
+ bigRule, ORIGIN_APP, "add", CUSTOM_PKG_UID);
+
+ // Rules from evil.package are gone.
+ assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(
+ systemRuleId, smallRuleId);
+ }
+
private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode,
@Nullable ZenPolicy zenPolicy) {
ZenRule rule = new ZenRule();
diff --git a/services/tests/wmtests/src/com/android/server/TransitionSubject.java b/services/tests/wmtests/src/com/android/server/TransitionSubject.java
new file mode 100644
index 0000000..07026b9
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/TransitionSubject.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.Nullable;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.Subject;
+import com.google.common.truth.Truth;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TransitionSubject extends Subject {
+
+ @Nullable
+ private final Transition actual;
+
+ /**
+ * Internal constructor.
+ *
+ * @see TransitionSubject#assertThat(Transition)
+ */
+ private TransitionSubject(FailureMetadata metadata, @Nullable Transition actual) {
+ super(metadata, actual);
+ this.actual = actual;
+ }
+
+ /**
+ * In a fluent assertion chain, the argument to the "custom" overload of {@link
+ * StandardSubjectBuilder#about(CustomSubjectBuilder.Factory) about}, the method that specifies
+ * what kind of {@link Subject} to create.
+ */
+ public static Factory<TransitionSubject, Transition> transitions() {
+ return TransitionSubject::new;
+ }
+
+ /**
+ * Typical entry point for making assertions about Transitions.
+ *
+ * @see @Truth#assertThat(Object)
+ */
+ public static TransitionSubject assertThat(Transition transition) {
+ return Truth.assertAbout(transitions()).that(transition);
+ }
+
+ /**
+ * Converts to a {@link IterableSubject} containing {@link Transition#getFlags()} separated into
+ * a list of individual flags for assertions such as {@code flags().contains(TRANSIT_FLAG_XYZ)}.
+ *
+ * <p>If the subject is null, this will fail instead of returning a null subject.
+ */
+ public IterableSubject flags() {
+ isNotNull();
+
+ final List<Integer> sortedFlags = new ArrayList<>();
+ for (int i = 0; i < 32; i++) {
+ if ((actual.getFlags() & (1 << i)) != 0) {
+ sortedFlags.add((1 << i));
+ }
+ }
+ return com.google.common.truth.Truth.assertThat(sortedFlags);
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index cfd501a..61ed0b5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -63,6 +63,9 @@
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
import static android.window.DisplayAreaOrganizer.FEATURE_WINDOWED_MAGNIFICATION;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
@@ -80,6 +83,7 @@
import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
import static com.android.server.wm.WindowContainer.POSITION_TOP;
import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
+import static com.android.server.wm.TransitionSubject.assertThat;
import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE;
import static com.android.server.display.feature.flags.Flags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT;
@@ -147,6 +151,7 @@
import com.android.server.LocalServices;
import com.android.server.policy.WindowManagerPolicy;
import com.android.server.wm.utils.WmDisplayCutout;
+import com.android.window.flags.Flags;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -2620,6 +2625,7 @@
final KeyguardController keyguard = mAtm.mKeyguardController;
final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
final int displayId = mDisplayContent.getDisplayId();
+ final TestTransitionPlayer transitions = registerTestTransitionPlayer();
final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId);
final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId);
@@ -2629,21 +2635,40 @@
keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */);
assertFalse(keyguardGoingAway.getAsBoolean());
assertFalse(appVisible.getAsBoolean());
+ transitions.flush();
// Start unlocking from AOD.
keyguard.keyguardGoingAway(displayId, 0x0 /* flags */);
assertTrue(keyguardGoingAway.getAsBoolean());
assertTrue(appVisible.getAsBoolean());
+ if (Flags.ensureKeyguardDoesTransitionStarting()) {
+ assertThat(transitions.mLastTransit).isNull();
+ } else {
+ assertThat(transitions.mLastTransit).flags()
+ .containsExactly(TRANSIT_FLAG_KEYGUARD_GOING_AWAY);
+ }
+ transitions.flush();
+
// Clear AOD. This does *not* clear the going-away status.
keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
assertTrue(keyguardGoingAway.getAsBoolean());
assertTrue(appVisible.getAsBoolean());
+ if (Flags.aodTransition()) {
+ assertThat(transitions.mLastTransit).flags()
+ .containsExactly(TRANSIT_FLAG_AOD_APPEARING);
+ } else {
+ assertThat(transitions.mLastTransit).isNull();
+ }
+ transitions.flush();
+
// Finish unlock
keyguard.setKeyguardShown(displayId, false /* keyguard */, false /* aod */);
assertFalse(keyguardGoingAway.getAsBoolean());
assertTrue(appVisible.getAsBoolean());
+
+ assertThat(transitions.mLastTransit).isNull();
}
@Test
@@ -2653,6 +2678,7 @@
final KeyguardController keyguard = mAtm.mKeyguardController;
final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
final int displayId = mDisplayContent.getDisplayId();
+ final TestTransitionPlayer transitions = registerTestTransitionPlayer();
final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId);
final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId);
@@ -2662,22 +2688,44 @@
keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */);
assertFalse(keyguardGoingAway.getAsBoolean());
assertFalse(appVisible.getAsBoolean());
+ transitions.flush();
// Start unlocking from AOD.
keyguard.keyguardGoingAway(displayId, 0x0 /* flags */);
assertTrue(keyguardGoingAway.getAsBoolean());
assertTrue(appVisible.getAsBoolean());
+ if (!Flags.ensureKeyguardDoesTransitionStarting()) {
+ assertThat(transitions.mLastTransit).flags()
+ .containsExactly(TRANSIT_FLAG_KEYGUARD_GOING_AWAY);
+ }
+ transitions.flush();
+
// Clear AOD. This does *not* clear the going-away status.
keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
assertTrue(keyguardGoingAway.getAsBoolean());
assertTrue(appVisible.getAsBoolean());
+ if (Flags.aodTransition()) {
+ assertThat(transitions.mLastTransit).flags()
+ .containsExactly(TRANSIT_FLAG_AOD_APPEARING);
+ } else {
+ assertThat(transitions.mLastTransit).isNull();
+ }
+ transitions.flush();
+
// Same API call a second time cancels the unlock, because AOD isn't changing.
keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
assertTrue(keyguardShowing.getAsBoolean());
assertFalse(keyguardGoingAway.getAsBoolean());
assertFalse(appVisible.getAsBoolean());
+
+ if (Flags.ensureKeyguardDoesTransitionStarting()) {
+ assertThat(transitions.mLastTransit).isNull();
+ } else {
+ assertThat(transitions.mLastTransit).flags()
+ .containsExactly(TRANSIT_FLAG_KEYGUARD_APPEARING);
+ }
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
index 2d4101e..6e0f7fb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
@@ -16,9 +16,12 @@
package com.android.server.wm;
+import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.FLAG_PRESENTATION;
+import static android.view.Display.FLAG_TRUSTED;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_WAKE;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS;
@@ -30,6 +33,7 @@
import android.annotation.NonNull;
import android.graphics.Rect;
+import android.os.Binder;
import android.os.UserHandle;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
@@ -118,6 +122,112 @@
assertFalse(window.isAttached());
}
+ @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testPresentationCannotCoverHostTask() {
+ int uid = Binder.getCallingUid();
+ final DisplayContent presentationDisplay = createPresentationDisplay();
+ final Task task = createTask(presentationDisplay);
+ task.effectiveUid = uid;
+ final ActivityRecord activity = createActivityRecord(task);
+ assertTrue(activity.isVisible());
+
+ // Adding a presentation window over its host task must fail.
+ assertAddPresentationWindowFails(uid, presentationDisplay.mDisplayId);
+
+ // Adding a presentation window on the other display must succeed.
+ final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY);
+ final Transition addTransition = window.mTransitionController.getCollectingTransition();
+ completeTransition(addTransition, /*abortSync=*/ true);
+ assertTrue(window.isVisible());
+
+ // Moving the host task to the presenting display will remove the presentation.
+ task.reparent(mDefaultDisplay.getDefaultTaskDisplayArea(), true);
+ waitHandlerIdle(window.mWmService.mAtmService.mH);
+ final Transition removeTransition = window.mTransitionController.getCollectingTransition();
+ assertEquals(TRANSIT_CLOSE, removeTransition.mType);
+ completeTransition(removeTransition, /*abortSync=*/ false);
+ assertFalse(window.isVisible());
+ }
+
+ @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testPresentationCannotLaunchOnAllDisplays() {
+ final int uid = Binder.getCallingUid();
+ final DisplayContent presentationDisplay = createPresentationDisplay();
+ final Task task = createTask(presentationDisplay);
+ task.effectiveUid = uid;
+ final ActivityRecord activity = createActivityRecord(task);
+ assertTrue(activity.isVisible());
+
+ // Add a presentation window on the default display.
+ final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY);
+ final Transition addTransition = window.mTransitionController.getCollectingTransition();
+ completeTransition(addTransition, /*abortSync=*/ true);
+ assertTrue(window.isVisible());
+
+ // Adding another presentation window over the task even if it's a different UID because
+ // it would end up showing presentations on all displays.
+ assertAddPresentationWindowFails(uid + 1, presentationDisplay.mDisplayId);
+ }
+
+ @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testPresentationCannotLaunchOnNonPresentationDisplayWithoutHostHavingGlobalFocus() {
+ final int uid = Binder.getCallingUid();
+ // Adding a presentation window on an internal display requires a host task
+ // with global focus on another display.
+ assertAddPresentationWindowFails(uid, DEFAULT_DISPLAY);
+
+ final DisplayContent presentationDisplay = createPresentationDisplay();
+ final Task taskWiSameUid = createTask(presentationDisplay);
+ taskWiSameUid.effectiveUid = uid;
+ final ActivityRecord activity = createActivityRecord(taskWiSameUid);
+ assertTrue(activity.isVisible());
+ final Task taskWithDifferentUid = createTask(presentationDisplay);
+ taskWithDifferentUid.effectiveUid = uid + 1;
+ createActivityRecord(taskWithDifferentUid);
+ assertEquals(taskWithDifferentUid, presentationDisplay.getFocusedRootTask());
+
+ // The task with the same UID is covered by another task with a different UID, so this must
+ // also fail.
+ assertAddPresentationWindowFails(uid, DEFAULT_DISPLAY);
+
+ // Moving the task with the same UID to front and giving it global focus allows a
+ // presentation to show on the default display.
+ taskWiSameUid.moveToFront("test");
+ final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY);
+ final Transition addTransition = window.mTransitionController.getCollectingTransition();
+ completeTransition(addTransition, /*abortSync=*/ true);
+ assertTrue(window.isVisible());
+ }
+
+ @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testReparentingActivityToSameDisplayClosesPresentation() {
+ final int uid = Binder.getCallingUid();
+ final Task task = createTask(mDefaultDisplay);
+ task.effectiveUid = uid;
+ final ActivityRecord activity = createActivityRecord(task);
+ assertTrue(activity.isVisible());
+
+ // Add a presentation window on a presentation display.
+ final DisplayContent presentationDisplay = createPresentationDisplay();
+ final WindowState window = addPresentationWindow(uid, presentationDisplay.getDisplayId());
+ final Transition addTransition = window.mTransitionController.getCollectingTransition();
+ completeTransition(addTransition, /*abortSync=*/ true);
+ assertTrue(window.isVisible());
+
+ // Reparenting the host task below the presentation must close the presentation.
+ task.reparent(presentationDisplay.getDefaultTaskDisplayArea(), true);
+ waitHandlerIdle(window.mWmService.mAtmService.mH);
+ final Transition removeTransition = window.mTransitionController.getCollectingTransition();
+ // It's a WAKE transition instead of CLOSE because
+ assertEquals(TRANSIT_WAKE, removeTransition.mType);
+ completeTransition(removeTransition, /*abortSync=*/ false);
+ assertFalse(window.isVisible());
+ }
+
private WindowState addPresentationWindow(int uid, int displayId) {
final Session session = createTestSession(mAtm, 1234 /* pid */, uid);
final int userId = UserHandle.getUserId(uid);
@@ -134,10 +244,29 @@
return window;
}
+ private void assertAddPresentationWindowFails(int uid, int displayId) {
+ final Session session = createTestSession(mAtm, 1234 /* pid */, uid);
+ final IWindow clientWindow = new TestIWindow();
+ final int res = addPresentationWindowInner(uid, displayId, session, clientWindow);
+ assertEquals(WindowManagerGlobal.ADD_INVALID_DISPLAY, res);
+ }
+
+ private int addPresentationWindowInner(int uid, int displayId, Session session,
+ IWindow clientWindow) {
+ final int userId = UserHandle.getUserId(uid);
+ doReturn(true).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId));
+ final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.TYPE_PRESENTATION);
+ return mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, userId,
+ WindowInsets.Type.defaultVisible(), null, new InsetsState(),
+ new InsetsSourceControl.Array(), new Rect(), new float[1]);
+ }
+
private DisplayContent createPresentationDisplay() {
final DisplayInfo displayInfo = new DisplayInfo();
displayInfo.copyFrom(mDisplayInfo);
- displayInfo.flags = FLAG_PRESENTATION;
+ displayInfo.flags = FLAG_PRESENTATION | FLAG_TRUSTED;
+ displayInfo.displayId = DEFAULT_DISPLAY + 1;
final DisplayContent dc = createNewDisplay(displayInfo);
final int displayId = dc.getDisplayId();
doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
index 45436e4..d3f3269 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
@@ -33,6 +33,7 @@
import android.platform.test.annotations.Presubmit;
import android.view.Display.Mode;
import android.view.Surface;
+import android.view.WindowInsets;
import android.view.WindowManager.LayoutParams;
import androidx.test.filters.SmallTest;
@@ -283,7 +284,7 @@
assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
- overrideWindow.notifyInsetsAnimationRunningStateChanged(true);
+ overrideWindow.setAnimatingTypes(WindowInsets.Type.statusBars());
assertEquals(LOW_MODE_ID, mPolicy.getPreferredModeId(overrideWindow));
assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote);
@@ -303,7 +304,7 @@
assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
- overrideWindow.notifyInsetsAnimationRunningStateChanged(true);
+ overrideWindow.setAnimatingTypes(WindowInsets.Type.statusBars());
assertEquals(0, mPolicy.getPreferredModeId(overrideWindow));
assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index c0642f5..57ab13f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -2151,6 +2151,14 @@
mLastRequest = null;
}
+ void flush() {
+ if (mLastTransit != null) {
+ start();
+ finish();
+ clear();
+ }
+ }
+
@Override
public void onTransitionReady(IBinder transitToken, TransitionInfo transitionInfo,
SurfaceControl.Transaction transaction, SurfaceControl.Transaction finishT)
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 6e0304b..fbba999 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -10579,9 +10579,19 @@
public boolean hasCarrierPrivileges(int subId) {
try {
ITelephony telephony = getITelephony();
- if (telephony != null) {
- return telephony.getCarrierPrivilegeStatus(subId)
- == CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
+ if (telephony == null) {
+ Rlog.e(TAG, "hasCarrierPrivileges: no Telephony service");
+ return false;
+ }
+ int status = telephony.getCarrierPrivilegeStatus(subId);
+ switch (status) {
+ case CARRIER_PRIVILEGE_STATUS_HAS_ACCESS:
+ return true;
+ case CARRIER_PRIVILEGE_STATUS_NO_ACCESS:
+ return false;
+ default:
+ Rlog.e(TAG, "hasCarrierPrivileges: " + status);
+ return false;
}
} catch (RemoteException ex) {
Rlog.e(TAG, "hasCarrierPrivileges RemoteException", ex);
diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java
index ca4a643..ae7346e 100644
--- a/telephony/java/android/telephony/euicc/EuiccManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccManager.java
@@ -1737,13 +1737,8 @@
private int getCardIdForDefaultEuicc() {
int cardId = TelephonyManager.UNINITIALIZED_CARD_ID;
- if (Flags.enforceTelephonyFeatureMappingForPublicApis()) {
- PackageManager pm = mContext.getPackageManager();
- if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) {
- TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
- cardId = tm.getCardIdForDefaultEuicc();
- }
- } else {
+ PackageManager pm = mContext.getPackageManager();
+ if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) {
TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
cardId = tm.getCardIdForDefaultEuicc();
}
diff --git a/tools/processors/view_inspector/OWNERS b/tools/processors/view_inspector/OWNERS
index 0473f54..38d21e1 100644
--- a/tools/processors/view_inspector/OWNERS
+++ b/tools/processors/view_inspector/OWNERS
@@ -1,3 +1,2 @@
alanv@google.com
-ashleyrose@google.com
-aurimas@google.com
\ No newline at end of file
+aurimas@google.com