Merge "Announce new state of modes when toggled in Modes Dialog" into main
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index df01aa8..1cfb9fe 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3450,7 +3450,7 @@
}
public static interface VirtualDeviceManager.ActivityListener {
- method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public default void onActivityLaunchBlocked(int, @NonNull android.content.ComponentName, int, @Nullable android.content.IntentSender);
+ method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public default void onActivityLaunchBlocked(int, @NonNull android.content.ComponentName, @NonNull android.os.UserHandle, @Nullable android.content.IntentSender);
method public void onDisplayEmpty(int);
method @Deprecated public void onTopActivityChanged(int, @NonNull android.content.ComponentName);
method public default void onTopActivityChanged(int, @NonNull android.content.ComponentName, int);
@@ -3530,7 +3530,7 @@
field @Deprecated public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; // 0x1
field @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public static final int POLICY_TYPE_ACTIVITY = 3; // 0x3
field public static final int POLICY_TYPE_AUDIO = 1; // 0x1
- field @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public static final int POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR = 6; // 0x6
+ field @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public static final int POLICY_TYPE_BLOCKED_ACTIVITY = 6; // 0x6
field @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final int POLICY_TYPE_CAMERA = 5; // 0x5
field @FlaggedApi("android.companion.virtual.flags.cross_device_clipboard") public static final int POLICY_TYPE_CLIPBOARD = 4; // 0x4
field public static final int POLICY_TYPE_RECENTS = 2; // 0x2
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index db979a5..e99ba84 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -1578,6 +1578,22 @@
public static final String EXTRA_DECLINE_COLOR = "android.declineColor";
/**
+ * {@link #extras} key: {@link Icon} of an image used as an overlay Icon on
+ * {@link Notification#mLargeIcon} for {@link EnRouteStyle} notifications.
+ * This extra is an {@code Icon}.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+ public static final String EXTRA_ENROUTE_OVERLAY_ICON = "android.enrouteOverlayIcon";
+
+ /**
+ * {@link #extras} key: text used as a sub-text for the largeIcon of
+ * {@link EnRouteStyle} notification. This extra is a {@code CharSequence}.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+ public static final String EXTRA_ENROUTE_LARGE_ICON_SUBTEXT = "android.enrouteLargeIconSubText";
+ /**
* {@link #extras} key: whether the notification should be colorized as
* supplied to {@link Builder#setColorized(boolean)}.
*/
@@ -3039,6 +3055,10 @@
visitIconUri(visitor, extras.getParcelable(EXTRA_VERIFICATION_ICON, Icon.class));
}
+ if (Flags.apiRichOngoing()) {
+ visitIconUri(visitor, extras.getParcelable(EXTRA_ENROUTE_OVERLAY_ICON, Icon.class));
+ }
+
if (mBubbleMetadata != null) {
visitIconUri(visitor, mBubbleMetadata.getIcon());
}
@@ -10979,6 +10999,144 @@
}
/**
+ * TODO(b/360827871): Make EnRouteStyle public.
+ * A style used to represent the progress of a real-world journey with a known destination.
+ * For example:
+ * <ul>
+ * <li>Delivery tracking</li>
+ * <li>Ride progress</li>
+ * <li>Flight tracking</li>
+ * </ul>
+ *
+ * The exact fields from {@link Notification} that are shown with this style may vary by
+ * the surface where this update appears, but the following fields are recommended:
+ * <ul>
+ * <li>{@link Notification.Builder#setContentTitle}</li>
+ * <li>{@link Notification.Builder#setContentText}</li>
+ * <li>{@link Notification.Builder#setSubText}</li>
+ * <li>{@link Notification.Builder#setLargeIcon}</li>
+ * <li>{@link Notification.Builder#setProgress}</li>
+ * <li>{@link Notification.Builder#setWhen} - This should be the future time of the next,
+ * final, or most important stop on this journey.</li>
+ * </ul>
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+ public static class EnRouteStyle extends Notification.Style {
+
+ @Nullable
+ private Icon mOverlayIcon = null;
+
+ @Nullable
+ private CharSequence mLargeIconSubText = null;
+
+ public EnRouteStyle() {
+ }
+
+ /**
+ * Returns the overlay icon to be displayed on {@link Notification#mLargeIcon}.
+ * @see EnRouteStyle#setOverlayIcon
+ */
+ @Nullable
+ public Icon getOverlayIcon() {
+ return mOverlayIcon;
+ }
+
+ /**
+ * Optional icon to be displayed on {@link Notification#mLargeIcon}.
+ *
+ * This image will be cropped to a circle and will obscure
+ * a semicircle of the right side of the large icon.
+ */
+ @NonNull
+ public EnRouteStyle setOverlayIcon(@Nullable Icon overlayIcon) {
+ mOverlayIcon = overlayIcon;
+ return this;
+ }
+
+ /**
+ * Returns the sub-text for {@link Notification#mLargeIcon}.
+ * @see EnRouteStyle#setLargeIconSubText
+ */
+ @Nullable
+ public CharSequence getLargeIconSubText() {
+ return mLargeIconSubText;
+ }
+
+ /**
+ * Optional text which generally related to
+ * the {@link Notification.Builder#setLargeIcon} or {@link #setOverlayIcon} or both.
+ */
+ @NonNull
+ public EnRouteStyle setLargeIconSubText(@Nullable CharSequence largeIconSubText) {
+ mLargeIconSubText = stripStyling(largeIconSubText);
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean areNotificationsVisiblyDifferent(Style other) {
+ if (other == null || getClass() != other.getClass()) {
+ return true;
+ }
+
+ final EnRouteStyle enRouteStyle = (EnRouteStyle) other;
+ return !Objects.equals(mOverlayIcon, enRouteStyle.mOverlayIcon)
+ || !Objects.equals(mLargeIconSubText, enRouteStyle.mLargeIconSubText);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void addExtras(Bundle extras) {
+ super.addExtras(extras);
+ extras.putParcelable(EXTRA_ENROUTE_OVERLAY_ICON, mOverlayIcon);
+ extras.putCharSequence(EXTRA_ENROUTE_LARGE_ICON_SUBTEXT, mLargeIconSubText);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected void restoreFromExtras(Bundle extras) {
+ super.restoreFromExtras(extras);
+ mOverlayIcon = extras.getParcelable(EXTRA_ENROUTE_OVERLAY_ICON, Icon.class);
+ mLargeIconSubText = extras.getCharSequence(EXTRA_ENROUTE_LARGE_ICON_SUBTEXT);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void purgeResources() {
+ super.purgeResources();
+ if (mOverlayIcon != null) {
+ mOverlayIcon.convertToAshmem();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void reduceImageSizes(Context context) {
+ super.reduceImageSizes(context);
+ if (mOverlayIcon != null) {
+ final Resources resources = context.getResources();
+ final boolean isLowRam = ActivityManager.isLowRamDeviceStatic();
+
+ int rightIconSize = resources.getDimensionPixelSize(isLowRam
+ ? R.dimen.notification_right_icon_size_low_ram
+ : R.dimen.notification_right_icon_size);
+ mOverlayIcon.scaleDownIfNecessary(rightIconSize, rightIconSize);
+ }
+ }
+ }
+
+ /**
* Notification style for custom views that are decorated by the system
*
* <p>Instead of providing a notification that is completely custom, a developer can set this
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index 326d7ce..789c99d 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -753,9 +753,14 @@
/**
* Sets whether or not notifications posted to this channel can interrupt the user in
- * {@link android.app.NotificationManager.Policy#INTERRUPTION_FILTER_PRIORITY} mode.
+ * {@link android.app.NotificationManager#INTERRUPTION_FILTER_PRIORITY} mode.
*
- * Only modifiable by the system and notification ranker.
+ * <p>Apps with Do Not Disturb policy access (see
+ * {@link NotificationManager#isNotificationPolicyAccessGranted()}) can set up their own
+ * channels this way, but only if the channel hasn't been updated by the user since its
+ * creation.
+ *
+ * <p>Otherwise, this value is only modifiable by the system and the notification ranker.
*/
public void setBypassDnd(boolean bypassDnd) {
this.mBypassDnd = bypassDnd;
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 3d1a785..83f9ff7 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -1440,10 +1440,36 @@
* Informs the notification manager that the state of an {@link AutomaticZenRule} has changed.
* Use this method to put the system into Do Not Disturb mode or request that it exits Do Not
* Disturb mode. The calling app must own the provided {@link android.app.AutomaticZenRule}.
- * <p>
- * This method can be used in conjunction with or as a replacement to
- * {@link android.service.notification.ConditionProviderService#notifyCondition(Condition)}.
- * </p>
+ *
+ * <p>This method can be used in conjunction with or as a replacement to
+ * {@link android.service.notification.ConditionProviderService#notifyCondition(Condition)}.
+ *
+ * <p>The condition change may be ignored if the user has activated or deactivated the rule
+ * manually -- the user can "override" the rule <em>this time</em>, with the rule resuming its
+ * normal operation for the next cycle. When this has happened, the supplied condition will be
+ * applied only once the automatic state is in agreement with the user-provided state. For
+ * example, assume that the {@link AutomaticZenRule} corresponds to a "Driving Mode" with
+ * automatic driving detection.
+ *
+ * <ol>
+ * <li>App detects driving and notifies the system that the rule should be active, calling
+ * this method with a {@link Condition} with {@link Condition#STATE_TRUE}).
+ * <li>User deactivates ("snoozes") the rule for some reason. This overrides the
+ * app-provided condition state.
+ * <li>App is still detecting driving, so again calls with {@link Condition#STATE_TRUE}.
+ * This is ignored by the system, as the user override prevails.
+ * <li>Some time later, the app detects that driving stopped, so the rule should be
+ * inactive, and calls with {@link Condition#STATE_FALSE}). This doesn't change the actual
+ * rule state (it was already inactive due to the user's override), but clears the override.
+ * <li>Some time later, the app detects that driving has started again, and notifies that
+ * the rule should be active (calling with {@link Condition#STATE_TRUE} again). The rule is
+ * activated.
+ * </ol>
+ *
+ * <p>Note that the behavior at step #3 is different if the app also specifies
+ * {@link Condition#SOURCE_USER_ACTION} as the {@link Condition#source} -- rule state updates
+ * coming from user actions are not ignored.
+ *
* @param id The id of the rule whose state should change
* @param condition The new state of this rule
*/
diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java
index bf21549..7801201 100644
--- a/core/java/android/app/appfunctions/AppFunctionManager.java
+++ b/core/java/android/app/appfunctions/AppFunctionManager.java
@@ -27,7 +27,6 @@
*
* <p>App function is a specific piece of functionality that an app offers to the system. These
* functionalities can be integrated into various system features.
- *
*/
@FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
@SystemService(Context.APP_FUNCTION_SERVICE)
diff --git a/core/java/android/app/appfunctions/IAppFunctionManager.aidl b/core/java/android/app/appfunctions/IAppFunctionManager.aidl
index 018bc75..14944f0 100644
--- a/core/java/android/app/appfunctions/IAppFunctionManager.aidl
+++ b/core/java/android/app/appfunctions/IAppFunctionManager.aidl
@@ -16,9 +16,22 @@
package android.app.appfunctions;
+import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
+import android.app.appfunctions.IExecuteAppFunctionCallback;
+
/**
* Interface between an app and the server implementation service (AppFunctionManagerService).
* @hide
*/
oneway interface IAppFunctionManager {
+ /**
+ * Executes an app function provided by {@link AppFunctionService} through the system.
+ *
+ * @param request the request to execute an app function.
+ * @param callback the callback to report the result.
+ */
+ void executeAppFunction(
+ in ExecuteAppFunctionAidlRequest request,
+ in IExecuteAppFunctionCallback callback
+ );
}
\ No newline at end of file
diff --git a/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl b/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl
index 564fb02..7c674f9 100644
--- a/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl
+++ b/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl
@@ -18,6 +18,7 @@
import android.content.ComponentName;
import android.content.IntentSender;
+import android.os.UserHandle;
/**
* Interface to listen for activity changes in a virtual device.
@@ -48,9 +49,9 @@
*
* @param displayId The display ID on which the activity tried to launch.
* @param componentName The component name of the blocked activity.
- * @param userId The user ID associated with the blocked activity.
+ * @param user The user associated with the blocked activity.
* @param intentSender The original sender of the intent.
*/
- void onActivityLaunchBlocked(int displayId, in ComponentName componentName, int userId,
+ void onActivityLaunchBlocked(int displayId, in ComponentName componentName, in UserHandle user,
in IntentSender intentSender);
}
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index 19eb497..9636cd4 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -19,7 +19,7 @@
import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
-import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_ACTIVITY;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
@@ -65,6 +65,7 @@
import android.os.Looper;
import android.os.RemoteException;
import android.os.ResultReceiver;
+import android.os.UserHandle;
import android.util.ArrayMap;
import android.view.WindowManager;
@@ -136,14 +137,14 @@
@Override
public void onActivityLaunchBlocked(int displayId, ComponentName componentName,
- @UserIdInt int userId, IntentSender intentSender) {
+ UserHandle user, IntentSender intentSender) {
final long token = Binder.clearCallingIdentity();
try {
synchronized (mActivityListenersLock) {
for (int i = 0; i < mActivityListeners.size(); i++) {
mActivityListeners.valueAt(i)
.onActivityLaunchBlocked(
- displayId, componentName, userId, intentSender);
+ displayId, componentName, user, intentSender);
}
}
} finally {
@@ -292,7 +293,7 @@
case POLICY_TYPE_RECENTS:
case POLICY_TYPE_CLIPBOARD:
case POLICY_TYPE_ACTIVITY:
- case POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR:
+ case POLICY_TYPE_BLOCKED_ACTIVITY:
break;
default:
throw new IllegalArgumentException("Device policy " + policyType
@@ -595,10 +596,10 @@
}
public void onActivityLaunchBlocked(int displayId, ComponentName componentName,
- @UserIdInt int userId, IntentSender intentSender) {
+ UserHandle user, IntentSender intentSender) {
mExecutor.execute(() ->
mActivityListener.onActivityLaunchBlocked(
- displayId, componentName, userId, intentSender));
+ displayId, componentName, user, intentSender));
}
}
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index d07fb25..c2300e0 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -67,6 +67,7 @@
import android.os.Binder;
import android.os.Looper;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Log;
import android.view.Display;
@@ -1263,7 +1264,7 @@
*
* @param displayId The display ID on which the activity tried to launch.
* @param componentName The component name of the blocked activity.
- * @param userId The user ID associated with the blocked activity.
+ * @param user The user associated with the blocked activity.
* @param intentSender The original sender of the intent. May be {@code null} if the sender
* expects an activity result to be reported. In that case
* {@link android.app.Activity#RESULT_CANCELED} was already reported back because the
@@ -1275,7 +1276,7 @@
*/
@FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
default void onActivityLaunchBlocked(int displayId, @NonNull ComponentName componentName,
- @UserIdInt int userId, @Nullable IntentSender intentSender) {}
+ @NonNull UserHandle user, @Nullable IntentSender intentSender) {}
}
/**
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index c1fc51d..03b72bd 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -160,7 +160,7 @@
*/
@IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_SENSORS, POLICY_TYPE_AUDIO,
POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CAMERA,
- POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR})
+ POLICY_TYPE_BLOCKED_ACTIVITY})
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
public @interface PolicyType {}
@@ -172,7 +172,7 @@
* @hide
*/
@IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY,
- POLICY_TYPE_CLIPBOARD, POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR})
+ POLICY_TYPE_CLIPBOARD, POLICY_TYPE_BLOCKED_ACTIVITY})
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
public @interface DynamicPolicyType {}
@@ -242,7 +242,7 @@
* @see VirtualDeviceManager.VirtualDevice#removeActivityPolicyExemption
*/
// TODO(b/333443509): Update the documentation of custom policy and link to the new policy
- // POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR
+ // POLICY_TYPE_BLOCKED_ACTIVITY
@FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
public static final int POLICY_TYPE_ACTIVITY = 3;
@@ -292,7 +292,7 @@
*/
// TODO(b/333443509): Link to POLICY_TYPE_ACTIVITY
@FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
- public static final int POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR = 6;
+ public static final int POLICY_TYPE_BLOCKED_ACTIVITY = 6;
private final int mLockState;
@NonNull private final ArraySet<UserHandle> mUsersWithMatchingAccounts;
@@ -1206,7 +1206,7 @@
}
if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
- mDevicePolicies.delete(POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR);
+ mDevicePolicies.delete(POLICY_TYPE_BLOCKED_ACTIVITY);
}
if ((mAudioPlaybackSessionId != AUDIO_SESSION_ID_GENERATE
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index fc6c2e8..57acc71 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -187,6 +187,13 @@
@Retention(RetentionPolicy.SOURCE)
public @interface ConfigOrigin {}
+ /**
+ * Prefix for the ids of implicit Zen rules. Implicit rules are those created automatically
+ * on behalf of apps that call {@link NotificationManager#setNotificationPolicy} or
+ * {@link NotificationManager#setInterruptionFilter}.
+ */
+ private static final String IMPLICIT_RULE_ID_PREFIX = "implicit_"; // + pkg_name
+
public static final int SOURCE_ANYONE = Policy.PRIORITY_SENDERS_ANY;
public static final int SOURCE_CONTACT = Policy.PRIORITY_SENDERS_CONTACTS;
public static final int SOURCE_STAR = Policy.PRIORITY_SENDERS_STARRED;
@@ -2492,6 +2499,16 @@
// ==== End built-in system conditions ====
+ /** Generate the rule id for the implicit rule for the specified package. */
+ public static String implicitRuleId(String forPackage) {
+ return IMPLICIT_RULE_ID_PREFIX + forPackage;
+ }
+
+ /** Returns whether the rule id corresponds to an implicit rule. */
+ public static boolean isImplicitRuleId(@NonNull String ruleId) {
+ return ruleId.startsWith(IMPLICIT_RULE_ID_PREFIX);
+ }
+
private static int[] tryParseHourAndMinute(String value) {
if (TextUtils.isEmpty(value)) return null;
final int i = value.indexOf('.');
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index 1c5049e..3cc0ad2 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -90,23 +90,24 @@
mDisplayManager = displayManager;
}
- /**
- * Register a listener to receive notifications about when the {@link MediaProjection} or
- * captured content changes state.
- *
- * <p>The callback must be registered before invoking
- * {@link #createVirtualDisplay(String, int, int, int, int, Surface, VirtualDisplay.Callback,
- * Handler)} to ensure that any notifications on the callback are not missed. The client must
- * implement {@link Callback#onStop()} and clean up any resources it is holding, e.g. the
- * {@link VirtualDisplay} and {@link Surface}.
- *
- * @param callback The callback to call.
- * @param handler The handler on which the callback should be invoked, or
- * null if the callback should be invoked on the calling thread's looper.
- * @throws NullPointerException If the given callback is null.
- * @see #unregisterCallback
- */
- public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
+ /**
+ * Register a listener to receive notifications about when the {@link MediaProjection} or captured
+ * content changes state.
+ *
+ * <p>The callback must be registered before invoking {@link #createVirtualDisplay(String, int,
+ * int, int, int, Surface, VirtualDisplay.Callback, Handler)} to ensure that any notifications on
+ * the callback are not missed. The client must implement {@link Callback#onStop()} and clean up
+ * any resources it is holding, e.g. the {@link VirtualDisplay} and {@link Surface}. This should
+ * also update any application UI indicating the MediaProjection status as MediaProjection has
+ * stopped.
+ *
+ * @param callback The callback to call.
+ * @param handler The handler on which the callback should be invoked, or null if the callback
+ * should be invoked on the calling thread's looper.
+ * @throws NullPointerException If the given callback is null.
+ * @see #unregisterCallback
+ */
+ public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
try {
final Callback c = Objects.requireNonNull(callback);
if (handler == null) {
@@ -158,74 +159,67 @@
return createVirtualDisplay(builder, callback, handler);
}
- /**
- * Creates a {@link android.hardware.display.VirtualDisplay} to capture the
- * contents of the screen.
- *
- * <p>To correctly clean up resources associated with a capture, the application must register a
- * {@link Callback} before invocation. The app must override {@link Callback#onStop()} to clean
- * up (by invoking{@link VirtualDisplay#release()}, {@link Surface#release()} and related
- * resources).
- *
- * @param name The name of the virtual display, must be non-empty.
- * @param width The width of the virtual display in pixels. Must be greater than 0.
- * @param height The height of the virtual display in pixels. Must be greater than 0.
- * @param dpi The density of the virtual display in dpi. Must be greater than 0.
- * @param surface The surface to which the content of the virtual display should be rendered,
- * or null if there is none initially.
- * @param flags A combination of virtual display flags. See {@link DisplayManager} for the
- * full list of flags. Note that
- * {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_PRESENTATION}
- * is always enabled. The following flags may be overridden, depending on how
- * the component with {android.Manifest.permission.MANAGE_MEDIA_PROJECTION}
- * handles the user's consent:
- * {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY},
- * {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR},
- * {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_PUBLIC}.
- * @param callback Callback invoked when the virtual display's state changes, or null.
- * @param handler The {@link android.os.Handler} on which the callback should be invoked, or
- * null if the callback should be invoked on the calling thread's main
- * {@link android.os.Looper}.
- * @throws IllegalStateException If the target SDK is {@link
- * android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up, and
- * if no {@link Callback} is registered.
- * @throws SecurityException In any of the following scenarios:
- * <ol>
- * <li>If attempting to create a new virtual display
- * associated with this MediaProjection instance after it has
- * been stopped by invoking {@link #stop()}.
- * <li>If attempting to create a new virtual display
- * associated with this MediaProjection instance after a
- * {@link MediaProjection.Callback#onStop()} callback has been
- * received due to the user or the system stopping the
- * MediaProjection session.
- * <li>If the target SDK is {@link
- * android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up,
- * and if this instance has already taken a recording through
- * {@code #createVirtualDisplay}, but {@link #stop()} wasn't
- * invoked to end the recording.
- * <li>If the target SDK is {@link
- * android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up,
- * and if {@link MediaProjectionManager#getMediaProjection}
- * was invoked more than once to get this
- * {@code MediaProjection} instance.
- * </ol>
- * In cases 2 & 3, no exception is thrown if the target SDK is
- * less than
- * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U}.
- * Instead, recording doesn't begin until the user re-grants
- * consent in the dialog.
- * @return The created {@link VirtualDisplay}, or {@code null} if no {@link VirtualDisplay}
- * could be created.
- * @see VirtualDisplay
- * @see VirtualDisplay.Callback
- */
- @SuppressWarnings("RequiresPermission")
- @Nullable
- public VirtualDisplay createVirtualDisplay(@NonNull String name,
- int width, int height, int dpi, @VirtualDisplayFlag int flags,
- @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback,
- @Nullable Handler handler) {
+ /**
+ * Creates a {@link android.hardware.display.VirtualDisplay} to capture the contents of the
+ * screen.
+ *
+ * <p>To correctly clean up resources associated with a capture, the application must register a
+ * {@link Callback} before invocation. The app must override {@link Callback#onStop()} to clean up
+ * resources (by invoking{@link VirtualDisplay#release()}, {@link Surface#release()} and related
+ * resources) and to update any available UI regarding the MediaProjection status.
+ *
+ * @param name The name of the virtual display, must be non-empty.
+ * @param width The width of the virtual display in pixels. Must be greater than 0.
+ * @param height The height of the virtual display in pixels. Must be greater than 0.
+ * @param dpi The density of the virtual display in dpi. Must be greater than 0.
+ * @param surface The surface to which the content of the virtual display should be rendered, or
+ * null if there is none initially.
+ * @param flags A combination of virtual display flags. See {@link DisplayManager} for the full
+ * list of flags. Note that {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_PRESENTATION} is always
+ * enabled. The following flags may be overridden, depending on how the component with
+ * {android.Manifest.permission.MANAGE_MEDIA_PROJECTION} handles the user's consent: {@link
+ * DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY}, {@link
+ * DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR}, {@link
+ * DisplayManager#VIRTUAL_DISPLAY_FLAG_PUBLIC}.
+ * @param callback Callback invoked when the virtual display's state changes, or null.
+ * @param handler The {@link android.os.Handler} on which the callback should be invoked, or null
+ * if the callback should be invoked on the calling thread's main {@link android.os.Looper}.
+ * @throws IllegalStateException If the target SDK is {@link
+ * android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up, and if no {@link Callback} is
+ * registered.
+ * @throws SecurityException In any of the following scenarios:
+ * <ol>
+ * <li>If attempting to create a new virtual display associated with this MediaProjection
+ * instance after it has been stopped by invoking {@link #stop()}.
+ * <li>If attempting to create a new virtual display associated with this MediaProjection
+ * instance after a {@link MediaProjection.Callback#onStop()} callback has been received
+ * due to the user or the system stopping the MediaProjection session.
+ * <li>If the target SDK is {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and
+ * up, and if this instance has already taken a recording through {@code
+ * #createVirtualDisplay}, but {@link #stop()} wasn't invoked to end the recording.
+ * <li>If the target SDK is {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and
+ * up, and if {@link MediaProjectionManager#getMediaProjection} was invoked more than
+ * once to get this {@code MediaProjection} instance.
+ * </ol>
+ * In cases 2 & 3, no exception is thrown if the target SDK is less than {@link
+ * android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U}. Instead, recording doesn't begin until
+ * the user re-grants consent in the dialog.
+ * @return The created {@link VirtualDisplay}, or {@code null} if no {@link VirtualDisplay} could
+ * be created.
+ * @see VirtualDisplay
+ * @see VirtualDisplay.Callback
+ */
+ @SuppressWarnings("RequiresPermission")
+ @Nullable
+ public VirtualDisplay createVirtualDisplay(
+ @NonNull String name,
+ int width,
+ int height,
+ int dpi,
+ @VirtualDisplayFlag int flags,
+ @Nullable Surface surface,
+ @Nullable VirtualDisplay.Callback callback,
+ @Nullable Handler handler) {
if (shouldMediaProjectionRequireCallback()) {
if (mCallbacks.isEmpty()) {
final IllegalStateException e = new IllegalStateException(
@@ -322,14 +316,20 @@
* Called when the MediaProjection session is no longer valid.
*
* <p>Once a MediaProjection has been stopped, it's up to the application to release any
- * resources it may be holding (e.g. releasing the {@link VirtualDisplay} and
- * {@link Surface}).
+ * resources it may be holding (e.g. releasing the {@link VirtualDisplay} and {@link
+ * Surface}). If the application is displaying any UI indicating the MediaProjection state
+ * it should be updated to indicate that MediaProjection is no longer active.
*
- * <p>After this callback any call to
- * {@link MediaProjection#createVirtualDisplay} will fail, even if no such
- * {@link VirtualDisplay} was ever created for this MediaProjection session.
+ * <p>MediaProjection stopping can be a result of the system stopping the ongoing
+ * MediaProjection due to various reasons, such as another MediaProjection session starting.
+ * MediaProjection may also stop due to the user explicitly stopping ongoing MediaProjection
+ * via any available system-level UI.
+ *
+ * <p>After this callback any call to {@link MediaProjection#createVirtualDisplay} will
+ * fail, even if no such {@link VirtualDisplay} was ever created for this MediaProjection
+ * session.
*/
- public void onStop() { }
+ public void onStop() {}
/**
* Invoked immediately after capture begins or when the size of the captured region changes,
diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java
index 7a7137a..03fd2c6 100644
--- a/media/java/android/media/projection/MediaProjectionManager.java
+++ b/media/java/android/media/projection/MediaProjectionManager.java
@@ -64,7 +64,9 @@
* holding, e.g. the {@link VirtualDisplay} and {@link Surface}. The MediaProjection may
* further no longer create any new {@link VirtualDisplay}s via {@link
* MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface,
- * VirtualDisplay.Callback, Handler)}.
+ * VirtualDisplay.Callback, Handler)}. Note that the `onStop()` callback can be a result of
+ * the system stopping MediaProjection due to various reasons or the user stopping the
+ * MediaProjection via UI affordances in system-level UI.
* <li>Start the screen capture session for media projection by calling {@link
* MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface,
* android.hardware.display.VirtualDisplay.Callback, Handler)}.
diff --git a/packages/ExternalStorageProvider/TEST_MAPPING b/packages/ExternalStorageProvider/TEST_MAPPING
new file mode 100644
index 0000000..dfa0c84
--- /dev/null
+++ b/packages/ExternalStorageProvider/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+ "postsubmit": [
+ {
+ "name": "ExternalStorageProviderTests",
+ "options": [
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ }
+ ]
+}
diff --git a/packages/ExternalStorageProvider/tests/Android.bp b/packages/ExternalStorageProvider/tests/Android.bp
index 86c62ef..097bb860 100644
--- a/packages/ExternalStorageProvider/tests/Android.bp
+++ b/packages/ExternalStorageProvider/tests/Android.bp
@@ -12,6 +12,10 @@
manifest: "AndroidManifest.xml",
+ test_suites: [
+ "general-tests",
+ ],
+
srcs: [
"src/**/*.java",
],
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
index 0d4ce5b..c13b261 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
@@ -117,6 +117,21 @@
.thenComparing(ZenMode::getType, PRIORITIZED_TYPE_COMPARATOR)
.thenComparing(ZenMode::getName);
+ public enum Kind {
+ /** A "normal" mode, created by apps or users via {@code addAutomaticZenRule()}. */
+ NORMAL,
+
+ /** The special, built-in "Do Not Disturb" mode. */
+ MANUAL_DND,
+
+ /**
+ * An implicit mode, automatically created and managed by the system on behalf of apps that
+ * call {@code setInterruptionFilter()} or {@code setNotificationPolicy()} (with some
+ * exceptions).
+ */
+ IMPLICIT,
+ }
+
public enum Status {
ENABLED,
ENABLED_AND_ACTIVE,
@@ -126,8 +141,8 @@
private final String mId;
private final AutomaticZenRule mRule;
+ private final Kind mKind;
private final Status mStatus;
- private final boolean mIsManualDnd;
/**
* Initializes a {@link ZenMode}, mainly based on the information from the
@@ -137,9 +152,11 @@
* active, or the reason it was disabled) are read from the {@link ZenModeConfig.ZenRule} --
* see {@link #computeStatus}.
*/
- public ZenMode(String id, @NonNull AutomaticZenRule rule,
+ ZenMode(String id, @NonNull AutomaticZenRule rule,
@NonNull ZenModeConfig.ZenRule zenRuleExtraData) {
- this(id, rule, computeStatus(zenRuleExtraData), false);
+ this(id, rule,
+ ZenModeConfig.isImplicitRuleId(id) ? Kind.IMPLICIT : Kind.NORMAL,
+ computeStatus(zenRuleExtraData));
}
private static Status computeStatus(@NonNull ZenModeConfig.ZenRule zenRuleExtraData) {
@@ -158,13 +175,16 @@
}
}
- public static ZenMode manualDndMode(AutomaticZenRule manualRule, boolean isActive) {
+ static ZenMode manualDndMode(AutomaticZenRule manualRule, boolean isActive) {
// Manual rule is owned by the system, so we set it here
AutomaticZenRule manualRuleWithPkg = new AutomaticZenRule.Builder(manualRule)
.setPackage(PACKAGE_ANDROID)
.build();
- return new ZenMode(MANUAL_DND_MODE_ID, manualRuleWithPkg,
- isActive ? Status.ENABLED_AND_ACTIVE : Status.ENABLED, true);
+ return new ZenMode(
+ MANUAL_DND_MODE_ID,
+ manualRuleWithPkg,
+ Kind.MANUAL_DND,
+ isActive ? Status.ENABLED_AND_ACTIVE : Status.ENABLED);
}
/**
@@ -183,19 +203,19 @@
.setIconResId(iconResId)
.setManualInvocationAllowed(true)
.build();
- return new ZenMode(TEMP_NEW_MODE_ID, rule, Status.ENABLED, false);
+ return new ZenMode(TEMP_NEW_MODE_ID, rule, Kind.NORMAL, Status.ENABLED);
}
- private ZenMode(String id, @NonNull AutomaticZenRule rule, Status status, boolean isManualDnd) {
+ private ZenMode(String id, @NonNull AutomaticZenRule rule, Kind kind, Status status) {
mId = id;
mRule = rule;
+ mKind = kind;
mStatus = status;
- mIsManualDnd = isManualDnd;
}
/** Creates a deep copy of this object. */
public ZenMode copy() {
- return new ZenMode(mId, new AutomaticZenRule.Builder(mRule).build(), mStatus, mIsManualDnd);
+ return new ZenMode(mId, new AutomaticZenRule.Builder(mRule).build(), mKind, mStatus);
}
@NonNull
@@ -264,10 +284,32 @@
return mRule.getType() + ":" + mRule.getPackageName() + ":" + mRule.getIconResId();
}
+ /**
+ * Returns the mode icon -- which can be either app-provided (via {@code addAutomaticZenRule}),
+ * user-chosen (via the icon picker in Settings), the app's launcher icon for implicit rules
+ * (in its monochrome variant, if available), or a default icon based on the mode type.
+ */
@NonNull
public ListenableFuture<Drawable> getIcon(@NonNull Context context,
@NonNull ZenIconLoader iconLoader) {
- if (mIsManualDnd) {
+ if (mKind == Kind.MANUAL_DND) {
+ return Futures.immediateFuture(requireNonNull(
+ context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
+ }
+
+ return iconLoader.getIcon(context, mRule);
+ }
+
+ /**
+ * Returns an alternative mode icon. The difference with {@link #getIcon} is that it's the
+ * basic DND icon not only for Manual DND, but also for <em>implicit rules</em>. As such, it's
+ * suitable for places where showing the launcher icon of an app could be confusing, such as
+ * the status bar or lockscreen.
+ */
+ @NonNull
+ public ListenableFuture<Drawable> getLockscreenIcon(@NonNull Context context,
+ @NonNull ZenIconLoader iconLoader) {
+ if (mKind == Kind.MANUAL_DND || mKind == Kind.IMPLICIT) {
return Futures.immediateFuture(requireNonNull(
context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
}
@@ -373,7 +415,7 @@
}
public boolean isManualDnd() {
- return mIsManualDnd;
+ return mKind == Kind.MANUAL_DND;
}
/**
@@ -404,18 +446,18 @@
return obj instanceof ZenMode other
&& mId.equals(other.mId)
&& mRule.equals(other.mRule)
- && mStatus.equals(other.mStatus)
- && mIsManualDnd == other.mIsManualDnd;
+ && mKind.equals(other.mKind)
+ && mStatus.equals(other.mStatus);
}
@Override
public int hashCode() {
- return Objects.hash(mId, mRule, mStatus, mIsManualDnd);
+ return Objects.hash(mId, mRule, mKind, mStatus);
}
@Override
public String toString() {
- return mId + " (" + mStatus + ") -> " + mRule;
+ return mId + " (" + mKind + ", " + mStatus + ") -> " + mRule;
}
@Override
@@ -427,8 +469,8 @@
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(mId);
dest.writeParcelable(mRule, 0);
+ dest.writeString(mKind.name());
dest.writeString(mStatus.name());
- dest.writeBoolean(mIsManualDnd);
}
public static final Creator<ZenMode> CREATOR = new Creator<ZenMode>() {
@@ -438,8 +480,8 @@
in.readString(),
checkNotNull(in.readParcelable(AutomaticZenRule.class.getClassLoader(),
AutomaticZenRule.class)),
- Status.valueOf(in.readString()),
- in.readBoolean());
+ Kind.valueOf(in.readString()),
+ Status.valueOf(in.readString()));
}
@Override
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
index bab4bc3b..f533e77 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
@@ -30,7 +30,14 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
import android.app.AutomaticZenRule;
+import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Parcel;
import android.service.notification.Condition;
@@ -40,9 +47,12 @@
import com.android.internal.R;
+import com.google.common.util.concurrent.ListenableFuture;
+
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList;
import java.util.List;
@@ -60,6 +70,13 @@
.setZenPolicy(ZEN_POLICY)
.build();
+ private static final String IMPLICIT_RULE_ID = ZenModeConfig.implicitRuleId("some.package");
+ private static final AutomaticZenRule IMPLICIT_ZEN_RULE =
+ new AutomaticZenRule.Builder("Implicit", Uri.parse("implicit/some.package"))
+ .setPackage("some.package")
+ .setType(TYPE_OTHER)
+ .build();
+
@Test
public void testBasicMethods() {
ZenMode zenMode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, true));
@@ -265,6 +282,79 @@
assertUnparceledIsEqualToOriginal("custom_manual",
ZenMode.newCustomManual("New mode", R.drawable.ic_zen_mode_type_immersive));
+
+ assertUnparceledIsEqualToOriginal("implicit",
+ new ZenMode(IMPLICIT_RULE_ID, IMPLICIT_ZEN_RULE,
+ zenConfigRuleFor(IMPLICIT_ZEN_RULE, false)));
+ }
+
+ @Test
+ public void getIcon_normalMode_loadsIconNormally() {
+ ZenIconLoader iconLoader = mock(ZenIconLoader.class);
+ ZenMode mode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, false));
+
+ ListenableFuture<Drawable> unused = mode.getIcon(RuntimeEnvironment.getApplication(),
+ iconLoader);
+
+ verify(iconLoader).getIcon(any(), eq(ZEN_RULE));
+ }
+
+ @Test
+ public void getIcon_manualDnd_returnsFixedIcon() {
+ ZenIconLoader iconLoader = mock(ZenIconLoader.class);
+
+ ListenableFuture<Drawable> future = TestModeBuilder.MANUAL_DND_INACTIVE.getIcon(
+ RuntimeEnvironment.getApplication(), iconLoader);
+
+ assertThat(future.isDone()).isTrue();
+ verify(iconLoader, never()).getIcon(any(), any());
+ }
+
+ @Test
+ public void getIcon_implicitMode_loadsIconNormally() {
+ ZenIconLoader iconLoader = mock(ZenIconLoader.class);
+ ZenMode mode = new ZenMode(IMPLICIT_RULE_ID, IMPLICIT_ZEN_RULE,
+ zenConfigRuleFor(IMPLICIT_ZEN_RULE, false));
+
+ ListenableFuture<Drawable> unused = mode.getIcon(RuntimeEnvironment.getApplication(),
+ iconLoader);
+
+ verify(iconLoader).getIcon(any(), eq(IMPLICIT_ZEN_RULE));
+ }
+
+ @Test
+ public void getLockscreenIcon_normalMode_loadsIconNormally() {
+ ZenIconLoader iconLoader = mock(ZenIconLoader.class);
+ ZenMode mode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, false));
+
+ ListenableFuture<Drawable> unused = mode.getLockscreenIcon(
+ RuntimeEnvironment.getApplication(), iconLoader);
+
+ verify(iconLoader).getIcon(any(), eq(ZEN_RULE));
+ }
+
+ @Test
+ public void getLockscreenIcon_manualDnd_returnsFixedIcon() {
+ ZenIconLoader iconLoader = mock(ZenIconLoader.class);
+
+ ListenableFuture<Drawable> future = TestModeBuilder.MANUAL_DND_INACTIVE.getLockscreenIcon(
+ RuntimeEnvironment.getApplication(), iconLoader);
+
+ assertThat(future.isDone()).isTrue();
+ verify(iconLoader, never()).getIcon(any(), any());
+ }
+
+ @Test
+ public void getLockscreenIcon_implicitMode_returnsFixedIcon() {
+ ZenIconLoader iconLoader = mock(ZenIconLoader.class);
+ ZenMode mode = new ZenMode(IMPLICIT_RULE_ID, IMPLICIT_ZEN_RULE,
+ zenConfigRuleFor(IMPLICIT_ZEN_RULE, false));
+
+ ListenableFuture<Drawable> future = mode.getLockscreenIcon(
+ RuntimeEnvironment.getApplication(), iconLoader);
+
+ assertThat(future.isDone()).isTrue();
+ verify(iconLoader, never()).getIcon(any(), any());
}
private static void assertUnparceledIsEqualToOriginal(String type, ZenMode original) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt
index bc142e6..6395448 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt
@@ -16,120 +16,108 @@
package com.android.systemui.gesture.domain
+import android.app.ActivityManager
import android.content.ComponentName
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.navigationbar.gestural.data.gestureRepository
import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
+import com.android.systemui.shared.system.activityManagerWrapper
+import com.android.systemui.shared.system.taskStackChangeListeners
+import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import kotlinx.coroutines.test.setMain
-import org.junit.After
-import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
-import org.mockito.kotlin.any
-import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
-import org.mockito.kotlin.never
-import org.mockito.kotlin.verify
+import org.mockito.kotlin.spy
import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
@SmallTest
class GestureInteractorTest : SysuiTestCase() {
@Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val kosmos = testKosmos()
- val dispatcher = StandardTestDispatcher()
+ val dispatcher = kosmos.testDispatcher
+ val repository = spy(kosmos.gestureRepository)
val testScope = TestScope(dispatcher)
- @Mock private lateinit var gestureRepository: GestureRepository
+ private val underTest by lazy { createInteractor() }
- private val underTest by lazy {
- GestureInteractor(gestureRepository, testScope.backgroundScope)
+ private fun createInteractor(): GestureInteractor {
+ return GestureInteractor(
+ repository,
+ dispatcher,
+ kosmos.backgroundCoroutineContext,
+ testScope,
+ kosmos.activityManagerWrapper,
+ kosmos.taskStackChangeListeners
+ )
}
- @Before
- fun setup() {
- Dispatchers.setMain(dispatcher)
- whenever(gestureRepository.gestureBlockedActivities).thenReturn(MutableStateFlow(setOf()))
- }
+ private fun setTopActivity(componentName: ComponentName) {
+ val task = mock<ActivityManager.RunningTaskInfo>()
+ task.topActivity = componentName
+ whenever(kosmos.activityManagerWrapper.runningTask).thenReturn(task)
- @After
- fun tearDown() {
- Dispatchers.resetMain()
+ kosmos.taskStackChangeListeners.listenerImpl.onTaskStackChanged()
}
@Test
fun addBlockedActivity_testCombination() =
testScope.runTest {
val globalComponent = mock<ComponentName>()
- whenever(gestureRepository.gestureBlockedActivities)
- .thenReturn(MutableStateFlow(setOf(globalComponent)))
+ repository.addGestureBlockedActivity(globalComponent)
+
val localComponent = mock<ComponentName>()
+
+ val blocked by collectLastValue(underTest.topActivityBlocked)
+
underTest.addGestureBlockedActivity(localComponent, GestureInteractor.Scope.Local)
- val lastSeen by collectLastValue(underTest.gestureBlockedActivities)
- testScope.runCurrent()
- verify(gestureRepository, never()).addGestureBlockedActivity(any())
- assertThat(lastSeen).hasSize(2)
- assertThat(lastSeen).containsExactly(globalComponent, localComponent)
+
+ assertThat(blocked).isFalse()
+
+ setTopActivity(localComponent)
+
+ assertThat(blocked).isTrue()
+ }
+
+ @Test
+ fun initialization_testEmit() =
+ testScope.runTest {
+ val globalComponent = mock<ComponentName>()
+ repository.addGestureBlockedActivity(globalComponent)
+ setTopActivity(globalComponent)
+
+ val interactor = createInteractor()
+
+ val blocked by collectLastValue(interactor.topActivityBlocked)
+ assertThat(blocked).isTrue()
}
@Test
fun addBlockedActivityLocally_onlyAffectsLocalInteractor() =
testScope.runTest {
- val component = mock<ComponentName>()
- underTest.addGestureBlockedActivity(component, GestureInteractor.Scope.Local)
- val lastSeen by collectLastValue(underTest.gestureBlockedActivities)
- testScope.runCurrent()
- verify(gestureRepository, never()).addGestureBlockedActivity(any())
- assertThat(lastSeen).contains(component)
- }
+ val interactor1 = createInteractor()
+ val interactor1Blocked by collectLastValue(interactor1.topActivityBlocked)
+ val interactor2 = createInteractor()
+ val interactor2Blocked by collectLastValue(interactor2.topActivityBlocked)
- @Test
- fun removeBlockedActivityLocally_onlyAffectsLocalInteractor() =
- testScope.runTest {
- val component = mock<ComponentName>()
- underTest.addGestureBlockedActivity(component, GestureInteractor.Scope.Local)
- val lastSeen by collectLastValue(underTest.gestureBlockedActivities)
- testScope.runCurrent()
- underTest.removeGestureBlockedActivity(component, GestureInteractor.Scope.Local)
- testScope.runCurrent()
- verify(gestureRepository, never()).removeGestureBlockedActivity(any())
- assertThat(lastSeen).isEmpty()
- }
+ val localComponent = mock<ComponentName>()
- @Test
- fun addBlockedActivity_invokesRepository() =
- testScope.runTest {
- val component = mock<ComponentName>()
- underTest.addGestureBlockedActivity(component, GestureInteractor.Scope.Global)
- runCurrent()
- val captor = argumentCaptor<ComponentName>()
- verify(gestureRepository).addGestureBlockedActivity(captor.capture())
- assertThat(captor.firstValue).isEqualTo(component)
- }
+ interactor1.addGestureBlockedActivity(localComponent, GestureInteractor.Scope.Local)
+ setTopActivity(localComponent)
- @Test
- fun removeBlockedActivity_invokesRepository() =
- testScope.runTest {
- val component = mock<ComponentName>()
- underTest.removeGestureBlockedActivity(component, GestureInteractor.Scope.Global)
- runCurrent()
- val captor = argumentCaptor<ComponentName>()
- verify(gestureRepository).removeGestureBlockedActivity(captor.capture())
- assertThat(captor.firstValue).isEqualTo(component)
+ assertThat(interactor1Blocked).isTrue()
+ assertThat(interactor2Blocked).isFalse()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 388272f..0f82e02 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -73,8 +73,8 @@
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.policy.GestureNavigationSettingsObserver;
-import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.contextualeducation.GestureType;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
@@ -102,6 +102,8 @@
import com.android.wm.shell.desktopmode.DesktopMode;
import com.android.wm.shell.pip.Pip;
+import kotlinx.coroutines.Job;
+
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.Date;
@@ -109,6 +111,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
@@ -158,7 +161,7 @@
private TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
@Override
public void onTaskStackChanged() {
- updateRunningActivityGesturesBlocked();
+ updateTopActivity();
}
@Override
public void onTaskCreated(int taskId, ComponentName componentName) {
@@ -222,6 +225,8 @@
private final Provider<LightBarController> mLightBarControllerProvider;
private final GestureInteractor mGestureInteractor;
+ private final ArraySet<ComponentName> mBlockedActivities = new ArraySet<>();
+ private Job mBlockedActivitiesJob = null;
private final JavaAdapter mJavaAdapter;
@@ -450,9 +455,6 @@
mJavaAdapter = javaAdapter;
mLastReportedConfig.setTo(mContext.getResources().getConfiguration());
- mJavaAdapter.alwaysCollectFlow(mGestureInteractor.getGestureBlockedActivities(),
- componentNames -> updateRunningActivityGesturesBlocked());
-
ComponentName recentsComponentName = ComponentName.unflattenFromString(
context.getString(com.android.internal.R.string.config_recentsComponentName));
if (recentsComponentName != null) {
@@ -568,12 +570,11 @@
}
}
- private void updateRunningActivityGesturesBlocked() {
+ private void updateTopActivity() {
if (edgebackGestureHandlerGetRunningTasksBackground()) {
- mBackgroundExecutor.execute(() -> mGestureBlockingActivityRunning.set(
- isGestureBlockingActivityRunning()));
+ mBackgroundExecutor.execute(() -> updateTopActivityPackageName());
} else {
- mGestureBlockingActivityRunning.set(isGestureBlockingActivityRunning());
+ updateTopActivityPackageName();
}
}
@@ -678,6 +679,11 @@
Log.e(TAG, "Failed to unregister window manager callbacks", e);
}
+ if (mBlockedActivitiesJob != null) {
+ mBlockedActivitiesJob.cancel(new CancellationException());
+ mBlockedActivitiesJob = null;
+ }
+ mBlockedActivities.clear();
} else {
mBackgroundExecutor.execute(mGestureNavigationSettingsObserver::register);
updateDisplaySize();
@@ -710,6 +716,12 @@
resetEdgeBackPlugin();
mPluginManager.addPluginListener(
this, NavigationEdgeBackPlugin.class, /*allowMultiple=*/ false);
+
+ // Begin listening to changes in blocked activities list
+ mBlockedActivitiesJob = mJavaAdapter.alwaysCollectFlow(
+ mGestureInteractor.getTopActivityBlocked(),
+ blocked -> mGestureBlockingActivityRunning.set(blocked));
+
}
// Update the ML model resources.
updateMLModelState();
@@ -1302,7 +1314,7 @@
}
}
- private boolean isGestureBlockingActivityRunning() {
+ private void updateTopActivityPackageName() {
ActivityManager.RunningTaskInfo runningTask =
ActivityManagerWrapper.getInstance().getRunningTask();
ComponentName topActivity = runningTask == null ? null : runningTask.topActivity;
@@ -1311,8 +1323,6 @@
} else {
mPackageName = "_UNKNOWN";
}
-
- return topActivity != null && mGestureInteractor.areGesturesBlocked(topActivity);
}
public void setBackAnimation(BackAnimation backAnimation) {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt
index 6dc5939..6182878 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt
@@ -17,17 +17,29 @@
package com.android.systemui.navigationbar.gestural.domain
import android.content.ComponentName
+import com.android.app.tracing.coroutines.flow.flowOn
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import com.android.systemui.shared.system.TaskStackChangeListener
+import com.android.systemui.shared.system.TaskStackChangeListeners
+import com.android.systemui.util.kotlin.combine
+import com.android.systemui.util.kotlin.emitOnStart
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
/**
* {@link GestureInteractor} helps interact with gesture-related logic, including accessing the
@@ -37,7 +49,11 @@
@Inject
constructor(
private val gestureRepository: GestureRepository,
- @Application private val scope: CoroutineScope
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ @Background private val backgroundCoroutineContext: CoroutineContext,
+ @Application private val scope: CoroutineScope,
+ private val activityManagerWrapper: ActivityManagerWrapper,
+ private val taskStackChangeListeners: TaskStackChangeListeners,
) {
enum class Scope {
Local,
@@ -45,16 +61,38 @@
}
private val _localGestureBlockedActivities = MutableStateFlow<Set<ComponentName>>(setOf())
- /** A {@link StateFlow} for listening to changes in Activities where gestures are blocked */
- val gestureBlockedActivities: StateFlow<Set<ComponentName>>
- get() =
- combine(
- gestureRepository.gestureBlockedActivities,
- _localGestureBlockedActivities.asStateFlow()
- ) { global, local ->
- global + local
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), setOf())
+
+ private val _topActivity =
+ conflatedCallbackFlow {
+ val taskListener =
+ object : TaskStackChangeListener {
+ override fun onTaskStackChanged() {
+ trySend(Unit)
+ }
+ }
+
+ taskStackChangeListeners.registerTaskStackListener(taskListener)
+ awaitClose { taskStackChangeListeners.unregisterTaskStackListener(taskListener) }
+ }
+ .flowOn(mainDispatcher)
+ .emitOnStart()
+ .mapLatest { getTopActivity() }
+ .distinctUntilChanged()
+
+ private suspend fun getTopActivity(): ComponentName? =
+ withContext(backgroundCoroutineContext) {
+ val runningTask = activityManagerWrapper.runningTask
+ runningTask?.topActivity
+ }
+
+ val topActivityBlocked =
+ combine(
+ _topActivity,
+ gestureRepository.gestureBlockedActivities,
+ _localGestureBlockedActivities.asStateFlow()
+ ) { activity, global, local ->
+ activity != null && (global + local).contains(activity)
+ }
/**
* Adds an {@link Activity} to be blocked based on component when the topmost, focused {@link
@@ -92,12 +130,4 @@
}
}
}
-
- /**
- * Checks whether the specified {@link Activity} {@link ComponentName} is being blocked from
- * gestures.
- */
- fun areGesturesBlocked(activity: ComponentName): Boolean {
- return gestureBlockedActivities.value.contains(activity)
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/DeviceBasedSatelliteTableLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/DeviceBasedSatelliteTableLog.kt
new file mode 100644
index 0000000..a40d510
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/DeviceBasedSatelliteTableLog.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.dagger
+
+import javax.inject.Qualifier
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class DeviceBasedSatelliteTableLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index a81bfa4..4850049 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -239,6 +239,13 @@
return factory.create("VerboseDeviceBasedSatelliteInputLog", 200)
}
+ @Provides
+ @SysUISingleton
+ @DeviceBasedSatelliteTableLog
+ fun provideDeviceBasedSatelliteTableLog(factory: TableLogBufferFactory): TableLogBuffer {
+ return factory.create("DeviceBasedSatelliteTableLog", 200)
+ }
+
const val FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON =
"FirstMobileSubShowingNetworkTypeIcon"
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index cc4d568..26553e6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -385,7 +385,15 @@
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean> =
- mobileConnectionsRepo.deviceServiceState.map { it?.isEmergencyOnly ?: false }
+ mobileConnectionsRepo.deviceServiceState
+ .map { it?.isEmergencyOnly ?: false }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ tableLogger,
+ columnPrefix = LOGGING_PREFIX,
+ columnName = "deviceEmergencyOnly",
+ initialValue = false,
+ )
/** Vends out new [MobileIconInteractor] for a particular subId */
override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
index 03f88c7..f1a444f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
@@ -21,7 +21,10 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteInputLog
+import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteTableLog
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
@@ -33,6 +36,7 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -47,6 +51,7 @@
wifiInteractor: WifiInteractor,
@Application scope: CoroutineScope,
@DeviceBasedSatelliteInputLog private val logBuffer: LogBuffer,
+ @DeviceBasedSatelliteTableLog private val tableLog: TableLogBuffer,
) {
/** Must be observed by any UI showing Satellite iconography */
val isSatelliteAllowed =
@@ -55,6 +60,13 @@
} else {
flowOf(false)
}
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ tableLog,
+ columnPrefix = "",
+ columnName = COL_ALLOWED,
+ initialValue = false,
+ )
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
/** See [SatelliteConnectionState] for relevant states */
@@ -65,6 +77,12 @@
flowOf(SatelliteConnectionState.Off)
}
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ tableLog,
+ columnPrefix = "",
+ initialValue = SatelliteConnectionState.Off,
+ )
.stateIn(scope, SharingStarted.WhileSubscribed(), SatelliteConnectionState.Off)
/** 0-4 description of the connection strength */
@@ -74,6 +92,13 @@
} else {
flowOf(0)
}
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ tableLog,
+ columnPrefix = "",
+ columnName = COL_LEVEL,
+ initialValue = 0,
+ )
.stateIn(scope, SharingStarted.WhileSubscribed(), 0)
val isSatelliteProvisioned = repo.isSatelliteProvisioned
@@ -82,19 +107,27 @@
wifiInteractor.wifiNetwork.map { it is WifiNetworkModel.Active }
private val allConnectionsOos =
- iconsInteractor.icons.aggregateOver(
- selector = { intr ->
- combine(intr.isInService, intr.isEmergencyOnly, intr.isNonTerrestrial) {
- isInService,
- isEmergencyOnly,
- isNtn ->
- !isInService && !isEmergencyOnly && !isNtn
- }
- },
- defaultValue = true, // no connections == everything is OOS
- ) { isOosAndNotEmergencyAndNotSatellite ->
- isOosAndNotEmergencyAndNotSatellite.all { it }
- }
+ iconsInteractor.icons
+ .aggregateOver(
+ selector = { intr ->
+ combine(intr.isInService, intr.isEmergencyOnly, intr.isNonTerrestrial) {
+ isInService,
+ isEmergencyOnly,
+ isNtn ->
+ !isInService && !isEmergencyOnly && !isNtn
+ }
+ },
+ defaultValue = true, // no connections == everything is OOS
+ ) { isOosAndNotEmergencyAndNotSatellite ->
+ isOosAndNotEmergencyAndNotSatellite.all { it }
+ }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ tableLog,
+ columnPrefix = "",
+ columnName = COL_ALL_OOS,
+ initialValue = true,
+ )
/** When all connections are considered OOS, satellite connectivity is potentially valid */
val areAllConnectionsOutOfService =
@@ -122,10 +155,24 @@
} else {
flowOf(false)
}
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ tableLog,
+ columnPrefix = "",
+ columnName = COL_FULL_OOS,
+ initialValue = true,
+ )
.stateIn(scope, SharingStarted.WhileSubscribed(), true)
companion object {
const val TAG = "DeviceBasedSatelliteInteractor"
+
+ const val COL_LEVEL = "level"
+ const val COL_ALL_OOS = "allConnsOOS"
+ const val COL_ALLOWED = "allowed"
+ // Going to try to optimize for not using too much width on the table here. This information
+ // can be ascertained by checking for the device emergency only in the mobile logs as well
+ const val COL_FULL_OOS = "allOosAndNoEmer"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt
index bfe2941..905ed730 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt
@@ -26,8 +26,10 @@
import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_OFF
import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE
import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN
+import com.android.systemui.log.table.Diffable
+import com.android.systemui.log.table.TableRowLogger
-enum class SatelliteConnectionState {
+enum class SatelliteConnectionState : Diffable<SatelliteConnectionState> {
// State is unknown or undefined
Unknown,
// Radio is off
@@ -37,7 +39,15 @@
// Radio is connected, aka satellite is available for use
Connected;
+ override fun logDiffs(prevVal: SatelliteConnectionState, row: TableRowLogger) {
+ if (prevVal != this) {
+ row.logChange(COL_CONNECTION_STATE, name)
+ }
+ }
+
companion object {
+ const val COL_CONNECTION_STATE = "connState"
+
// TODO(b/316635648): validate these states. We don't need the level of granularity that
// SatelliteManager gives us.
fun fromModemState(@SatelliteManager.SatelliteModemState modemState: Int) =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
index 48278d4..199b5b67 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -22,9 +22,12 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.res.R
import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteInputLog
+import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteTableLog
import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
@@ -71,22 +74,34 @@
@Application scope: CoroutineScope,
airplaneModeRepository: AirplaneModeRepository,
@DeviceBasedSatelliteInputLog logBuffer: LogBuffer,
+ @DeviceBasedSatelliteTableLog tableLog: TableLogBuffer,
) : DeviceBasedSatelliteViewModel {
private val shouldShowIcon: Flow<Boolean> =
- interactor.areAllConnectionsOutOfService.flatMapLatest { allOos ->
- if (!allOos) {
- flowOf(false)
- } else {
- combine(
- interactor.isSatelliteAllowed,
- interactor.isSatelliteProvisioned,
- interactor.isWifiActive,
- airplaneModeRepository.isAirplaneMode
- ) { isSatelliteAllowed, isSatelliteProvisioned, isWifiActive, isAirplaneMode ->
- isSatelliteAllowed && isSatelliteProvisioned && !isWifiActive && !isAirplaneMode
+ interactor.areAllConnectionsOutOfService
+ .flatMapLatest { allOos ->
+ if (!allOos) {
+ flowOf(false)
+ } else {
+ combine(
+ interactor.isSatelliteAllowed,
+ interactor.isSatelliteProvisioned,
+ interactor.isWifiActive,
+ airplaneModeRepository.isAirplaneMode
+ ) { isSatelliteAllowed, isSatelliteProvisioned, isWifiActive, isAirplaneMode ->
+ isSatelliteAllowed &&
+ isSatelliteProvisioned &&
+ !isWifiActive &&
+ !isAirplaneMode
+ }
}
}
- }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ tableLog,
+ columnPrefix = "vm",
+ columnName = COL_VISIBLE_CONDITION,
+ initialValue = false,
+ )
// This adds a 10 seconds delay before showing the icon
private val shouldActuallyShowIcon: StateFlow<Boolean> =
@@ -106,6 +121,13 @@
flowOf(false)
}
}
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ tableLog,
+ columnPrefix = "vm",
+ columnName = COL_VISIBLE,
+ initialValue = false,
+ )
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val icon: StateFlow<Icon?> =
@@ -163,5 +185,8 @@
companion object {
private const val TAG = "DeviceBasedSatelliteViewModel"
private val DELAY_DURATION = 10.seconds
+
+ const val COL_VISIBLE_CONDITION = "visCondition"
+ const val COL_VISIBLE = "visible"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 2468449..eb91518 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -695,11 +695,10 @@
addRow(AudioManager.STREAM_MUSIC,
R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true, true);
if (!AudioSystem.isSingleVolume(mContext)) {
-
addRow(AudioManager.STREAM_RING, R.drawable.ic_ring_volume,
R.drawable.ic_ring_volume_off, true, false);
-
-
+ addRow(AudioManager.STREAM_NOTIFICATION, R.drawable.ic_volume_ringer,
+ R.drawable.ic_volume_off, true, false);
addRow(STREAM_ALARM,
R.drawable.ic_alarm, R.drawable.ic_volume_alarm_mute, true, false);
addRow(AudioManager.STREAM_VOICE_CALL,
@@ -1994,7 +1993,7 @@
: R.drawable.ic_volume_media_bt;
}
} else if (isStreamMuted(ss)) {
- iconRes = ss.muted ? R.drawable.ic_volume_media_off : row.iconMuteRes;
+ iconRes = (ss.muted && isTv()) ? R.drawable.ic_volume_media_off : row.iconMuteRes;
} else {
iconRes = mShowLowMediaVolumeIcon && ss.level * 2 < (ss.levelMax + ss.levelMin)
? R.drawable.ic_volume_media_low : row.iconRes;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
index cd0390e..dbb77d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
@@ -70,6 +70,7 @@
wifiInteractor,
testScope.backgroundScope,
FakeLogBuffer.Factory.create(),
+ mock(),
)
}
@@ -113,6 +114,7 @@
wifiInteractor,
testScope.backgroundScope,
FakeLogBuffer.Factory.create(),
+ mock(),
)
val latest by collectLastValue(underTest.isSatelliteAllowed)
@@ -161,6 +163,7 @@
wifiInteractor,
testScope.backgroundScope,
FakeLogBuffer.Factory.create(),
+ mock(),
)
val latest by collectLastValue(underTest.connectionState)
@@ -217,6 +220,7 @@
wifiInteractor,
testScope.backgroundScope,
FakeLogBuffer.Factory.create(),
+ mock(),
)
val latest by collectLastValue(underTest.signalStrength)
@@ -535,6 +539,7 @@
wifiInteractor,
testScope.backgroundScope,
FakeLogBuffer.Factory.create(),
+ mock(),
)
val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
index 64b07fc..c3cc33f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -33,7 +33,6 @@
import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
-import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlin.time.Duration.Companion.seconds
@@ -44,6 +43,7 @@
import org.junit.Before
import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.mock
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -73,6 +73,7 @@
wifiInteractor,
testScope.backgroundScope,
FakeLogBuffer.Factory.create(),
+ mock(),
)
underTest =
@@ -82,6 +83,7 @@
testScope.backgroundScope,
airplaneModeRepository,
FakeLogBuffer.Factory.create(),
+ mock(),
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt
index 658aaa6..1d2439c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt
@@ -16,12 +16,23 @@
package com.android.systemui.keyguard.gesture.domain
-import com.android.systemui.keyguard.gesture.data.gestureRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.navigationbar.gestural.data.gestureRepository
import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
+import com.android.systemui.shared.system.activityManagerWrapper
+import com.android.systemui.shared.system.taskStackChangeListeners
val Kosmos.gestureInteractor: GestureInteractor by
Kosmos.Fixture {
- GestureInteractor(gestureRepository = gestureRepository, scope = applicationCoroutineScope)
+ GestureInteractor(
+ gestureRepository = gestureRepository,
+ mainDispatcher = testDispatcher,
+ backgroundCoroutineContext = backgroundCoroutineContext,
+ scope = applicationCoroutineScope,
+ activityManagerWrapper = activityManagerWrapper,
+ taskStackChangeListeners = taskStackChangeListeners
+ )
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/gestural/data/GestureRepositoryKosmos.kt
similarity index 94%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/gestural/data/GestureRepositoryKosmos.kt
index 9bd346e..55ce43a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/gestural/data/GestureRepositoryKosmos.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.keyguard.gesture.data
+package com.android.systemui.navigationbar.gestural.data
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java
index f30e770..954651d 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java
@@ -18,7 +18,6 @@
import static android.app.appfunctions.flags.Flags.enableAppFunctionManager;
-import android.app.appfunctions.IAppFunctionManager;
import android.content.Context;
import com.android.server.SystemService;
@@ -27,19 +26,17 @@
* Service that manages app functions.
*/
public class AppFunctionManagerService extends SystemService {
+ private final AppFunctionManagerServiceImpl mServiceImpl;
public AppFunctionManagerService(Context context) {
super(context);
+ mServiceImpl = new AppFunctionManagerServiceImpl(context);
}
@Override
public void onStart() {
if (enableAppFunctionManager()) {
- publishBinderService(Context.APP_FUNCTION_SERVICE, new AppFunctionManagerStub());
+ publishBinderService(Context.APP_FUNCTION_SERVICE, mServiceImpl);
}
}
-
- private static class AppFunctionManagerStub extends IAppFunctionManager.Stub {
-
- }
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
new file mode 100644
index 0000000..e2167a8
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.annotation.NonNull;
+import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
+import android.app.appfunctions.ExecuteAppFunctionResponse;
+import android.app.appfunctions.IAppFunctionManager;
+import android.app.appfunctions.IAppFunctionService;
+import android.app.appfunctions.IExecuteAppFunctionCallback;
+import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback;
+import android.app.appfunctions.ServiceCallHelper;
+import android.app.appfunctions.ServiceCallHelper.RunServiceCallCallback;
+import android.app.appfunctions.ServiceCallHelper.ServiceUsageCompleteListener;
+import android.app.appfunctions.ServiceCallHelperImpl;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Objects;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Implementation of the AppFunctionManagerService.
+ */
+public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
+ private static final String TAG = AppFunctionManagerServiceImpl.class.getSimpleName();
+ private final ServiceCallHelper<IAppFunctionService> mExternalServiceCallHelper;
+ private final CallerValidator mCallerValidator;
+ private final ServiceHelper mInternalServiceHelper;
+
+ public AppFunctionManagerServiceImpl(@NonNull Context context) {
+ this(new ServiceCallHelperImpl<>(
+ context,
+ IAppFunctionService.Stub::asInterface, new ThreadPoolExecutor(
+ /*corePoolSize=*/ Runtime.getRuntime().availableProcessors(),
+ /*maxConcurrency=*/ Runtime.getRuntime().availableProcessors(),
+ /*keepAliveTime=*/ 0L,
+ /*unit=*/ TimeUnit.SECONDS,
+ /*workQueue=*/ new LinkedBlockingQueue<>())),
+ new CallerValidatorImpl(context),
+ new ServiceHelperImpl(context));
+ }
+
+ @VisibleForTesting
+ AppFunctionManagerServiceImpl(ServiceCallHelper<IAppFunctionService> serviceCallHelper,
+ CallerValidator apiValidator,
+ ServiceHelper appFunctionInternalServiceHelper) {
+ mExternalServiceCallHelper = Objects.requireNonNull(serviceCallHelper);
+ mCallerValidator = Objects.requireNonNull(apiValidator);
+ mInternalServiceHelper =
+ Objects.requireNonNull(appFunctionInternalServiceHelper);
+ }
+
+ @Override
+ public void executeAppFunction(
+ @NonNull ExecuteAppFunctionAidlRequest requestInternal,
+ @NonNull IExecuteAppFunctionCallback executeAppFunctionCallback) {
+ Objects.requireNonNull(requestInternal);
+ Objects.requireNonNull(executeAppFunctionCallback);
+
+ final SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback =
+ new SafeOneTimeExecuteAppFunctionCallback(executeAppFunctionCallback);
+
+ String validatedCallingPackage = mCallerValidator
+ .validateCallingPackage(requestInternal.getCallingPackage());
+ UserHandle targetUser = mCallerValidator.verifyTargetUserHandle(
+ requestInternal.getUserHandle(), validatedCallingPackage);
+
+ // TODO(b/354956319): Add and honor the new enterprise policies.
+ if (mCallerValidator.isUserOrganizationManaged(targetUser)) {
+ safeExecuteAppFunctionCallback.onResult(new ExecuteAppFunctionResponse.Builder(
+ ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+ "Cannot run on a device with a device owner or from the managed profile."
+ ).build());
+ return;
+ }
+
+ String targetPackageName = requestInternal.getClientRequest().getTargetPackageName();
+ if (TextUtils.isEmpty(targetPackageName)) {
+ safeExecuteAppFunctionCallback.onResult(new ExecuteAppFunctionResponse.Builder(
+ ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT,
+ "Target package name cannot be empty."
+ ).build());
+ return;
+ }
+
+ if (!mCallerValidator.verifyCallerCanExecuteAppFunction(
+ validatedCallingPackage, targetPackageName)) {
+ throw new SecurityException("Caller does not have permission to execute the app "
+ + "function.");
+ }
+
+ Intent serviceIntent = mInternalServiceHelper.resolveAppFunctionService(
+ targetPackageName,
+ targetUser);
+ if (serviceIntent == null) {
+ safeExecuteAppFunctionCallback.onResult(new ExecuteAppFunctionResponse.Builder(
+ ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+ "Cannot find the target service."
+ ).build());
+ return;
+ }
+
+ // TODO(b/357551503): Offload call to async executor.
+ bindAppFunctionServiceUnchecked(requestInternal, serviceIntent, targetUser,
+ safeExecuteAppFunctionCallback,
+ /*bindFlags=*/ Context.BIND_AUTO_CREATE,
+ // TODO(b/357551503): Make timeout configurable.
+ /*timeoutInMillis=*/ 30_000L);
+ }
+
+ private void bindAppFunctionServiceUnchecked(
+ @NonNull ExecuteAppFunctionAidlRequest requestInternal,
+ @NonNull Intent serviceIntent, @NonNull UserHandle targetUser,
+ @NonNull SafeOneTimeExecuteAppFunctionCallback
+ safeExecuteAppFunctionCallback,
+ int bindFlags, long timeoutInMillis) {
+ boolean bindServiceResult = mExternalServiceCallHelper.runServiceCall(
+ serviceIntent,
+ bindFlags,
+ timeoutInMillis,
+ targetUser,
+ /*timeOutCallback=*/ new RunServiceCallCallback<IAppFunctionService>() {
+ @Override
+ public void onServiceConnected(@NonNull IAppFunctionService service,
+ @NonNull ServiceUsageCompleteListener
+ serviceUsageCompleteListener) {
+ try {
+ service.executeAppFunction(
+ requestInternal.getClientRequest(),
+ new IExecuteAppFunctionCallback.Stub() {
+ @Override
+ public void onResult(ExecuteAppFunctionResponse response) {
+ safeExecuteAppFunctionCallback.onResult(response);
+ serviceUsageCompleteListener.onCompleted();
+ }
+ }
+ );
+ } catch (Exception e) {
+ safeExecuteAppFunctionCallback.onResult(new ExecuteAppFunctionResponse
+ .Builder(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+ e.getMessage()).build());
+ serviceUsageCompleteListener.onCompleted();
+ }
+ }
+
+ @Override
+ public void onFailedToConnect() {
+ Slog.e(TAG, "Failed to connect to service");
+ safeExecuteAppFunctionCallback.onResult(new ExecuteAppFunctionResponse
+ .Builder(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+ "Failed to connect to AppFunctionService").build());
+ }
+
+ @Override
+ public void onTimedOut() {
+ Slog.e(TAG, "Timed out");
+ safeExecuteAppFunctionCallback.onResult(
+ new ExecuteAppFunctionResponse.Builder(
+ ExecuteAppFunctionResponse.RESULT_TIMED_OUT,
+ "Binding to AppFunctionService timed out."
+ ).build());
+ }
+ }
+ );
+
+ if (!bindServiceResult) {
+ Slog.e(TAG, "Failed to bind to the AppFunctionService");
+ safeExecuteAppFunctionCallback.onResult(new ExecuteAppFunctionResponse.Builder(
+ ExecuteAppFunctionResponse.RESULT_TIMED_OUT,
+ "Failed to bind the AppFunctionService."
+ ).build());
+ }
+ }
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java b/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java
new file mode 100644
index 0000000..9bd633f
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.os.UserHandle;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+
+/**
+ * Interface for validating that the caller has the correct privilege to call an AppFunctionManager
+ * API.
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public interface CallerValidator {
+ // TODO(b/357551503): Should we verify NOT instant app?
+ // TODO(b/357551503): Verify that user have been unlocked.
+
+ /**
+ * This method is used to validate that the calling package reported in the request is the
+ * same as the binder calling identity.
+ *
+ * @param claimedCallingPackage The package name of the caller.
+ * @return The package name of the caller.
+ * @throws SecurityException if the package name and uid don't match.
+ */
+ String validateCallingPackage(@NonNull String claimedCallingPackage);
+
+ /**
+ * Validates that the caller can invoke an AppFunctionManager API in the provided
+ * target user space.
+ *
+ * @param targetUserHandle The user which the caller is requesting to execute as.
+ * @param claimedCallingPackage The package name of the caller.
+ * @return The user handle that the call should run as. Will always be a concrete user.
+ * @throws IllegalArgumentException if the target user is a special user.
+ * @throws SecurityException if caller trying to interact across users without {@link
+ * Manifest.permission#INTERACT_ACROSS_USERS_FULL}
+ */
+ UserHandle verifyTargetUserHandle(@NonNull UserHandle targetUserHandle,
+ @NonNull String claimedCallingPackage);
+
+ /**
+ * Validates that the caller can execute the specified app function.
+ * <p>
+ * The caller can execute if the app function's package name is the same as the caller's package
+ * or the caller has either {@link Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or
+ * {@link Manifest.permission.EXECUTE_APP_FUNCTIONS} granted. In some cases, app functions
+ * can still opt-out of caller having {@link Manifest.permission.EXECUTE_APP_FUNCTIONS}.
+ *
+ * @param callerPackageName The calling package (as previously validated).
+ * @param targetPackageName The package that owns the app function to execute.
+ * @return Whether the caller can execute the specified app function.
+ */
+ boolean verifyCallerCanExecuteAppFunction(
+ @NonNull String callerPackageName, @NonNull String targetPackageName);
+
+ /**
+ * Checks if the user is organization managed.
+ *
+ * @param targetUser The user which the caller is requesting to execute as.
+ * @return Whether the user is organization managed.
+ */
+ boolean isUserOrganizationManaged(@NonNull UserHandle targetUser);
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java b/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java
new file mode 100644
index 0000000..7cd660d
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.Manifest;
+import android.annotation.BinderThread;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import java.util.Objects;
+
+/* Validates that caller has the correct privilege to call an AppFunctionManager Api. */
+class CallerValidatorImpl implements CallerValidator {
+ private final Context mContext;
+
+
+ CallerValidatorImpl(@NonNull Context context) {
+ mContext = Objects.requireNonNull(context);
+ }
+
+ @Override
+ @NonNull
+ @BinderThread
+ public String validateCallingPackage(@NonNull String claimedCallingPackage) {
+ int callingUid = Binder.getCallingUid();
+ final long callingIdentityToken = Binder.clearCallingIdentity();
+ try {
+ validateCallingPackageInternal(callingUid, claimedCallingPackage);
+ return claimedCallingPackage;
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentityToken);
+ }
+ }
+
+ @Override
+ @NonNull
+ @BinderThread
+ public UserHandle verifyTargetUserHandle(@NonNull UserHandle targetUserHandle,
+ @NonNull String claimedCallingPackage) {
+ int callingPid = Binder.getCallingPid();
+ int callingUid = Binder.getCallingUid();
+ final long callingIdentityToken = Binder.clearCallingIdentity();
+ try {
+ return handleIncomingUser(claimedCallingPackage, targetUserHandle,
+ callingPid, callingUid);
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentityToken);
+ }
+ }
+
+ @Override
+ @BinderThread
+ @RequiresPermission(anyOf = {Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED,
+ Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional = true)
+ // TODO(b/360864791): Add and honor apps that opt-out from EXECUTE_APP_FUNCTIONS caller.
+ public boolean verifyCallerCanExecuteAppFunction(
+ @NonNull String callerPackageName, @NonNull String targetPackageName) {
+ int pid = Binder.getCallingPid();
+ int uid = Binder.getCallingUid();
+ boolean hasExecutionPermission = mContext.checkPermission(
+ Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, pid, uid)
+ == PackageManager.PERMISSION_GRANTED;
+ boolean hasTrustedExecutionPermission = mContext.checkPermission(
+ Manifest.permission.EXECUTE_APP_FUNCTIONS, pid, uid)
+ == PackageManager.PERMISSION_GRANTED;
+ boolean isSamePackage = callerPackageName.equals(targetPackageName);
+ return hasExecutionPermission || hasTrustedExecutionPermission || isSamePackage;
+ }
+
+ @Override
+ @BinderThread
+ public boolean isUserOrganizationManaged(@NonNull UserHandle targetUser) {
+ final long callingIdentityToken = Binder.clearCallingIdentity();
+ try {
+ if (Objects.requireNonNull(mContext.getSystemService(DevicePolicyManager.class))
+ .isDeviceManaged()) {
+ return true;
+ }
+ return Objects.requireNonNull(mContext.getSystemService(UserManager.class))
+ .isManagedProfile(targetUser.getIdentifier());
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentityToken);
+ }
+ }
+
+ /**
+ * Helper for dealing with incoming user arguments to system service calls.
+ *
+ * <p>Takes care of checking permissions and if the target is special user, this method will
+ * simply throw.
+ *
+ * @param callingPackageName The package name of the caller.
+ * @param targetUserHandle The user which the caller is requesting to execute as.
+ * @param callingPid The actual pid of the caller as determined by Binder.
+ * @param callingUid The actual uid of the caller as determined by Binder.
+ * @return the user handle that the call should run as. Will always be a concrete user.
+ * @throws IllegalArgumentException if the target user is a special user.
+ * @throws SecurityException if caller trying to interact across user without {@link
+ * Manifest.permission#INTERACT_ACROSS_USERS_FULL}
+ */
+ @NonNull
+ private UserHandle handleIncomingUser(
+ @NonNull String callingPackageName,
+ @NonNull UserHandle targetUserHandle,
+ int callingPid,
+ int callingUid) {
+ UserHandle callingUserHandle = UserHandle.getUserHandleForUid(callingUid);
+ if (callingUserHandle.equals(targetUserHandle)) {
+ return targetUserHandle;
+ }
+
+ // Duplicates UserController#ensureNotSpecialUser
+ if (targetUserHandle.getIdentifier() < 0) {
+ throw new IllegalArgumentException(
+ "Call does not support special user " + targetUserHandle);
+ }
+
+ if (mContext.checkPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingPid, callingUid)
+ == PackageManager.PERMISSION_GRANTED) {
+ try {
+ mContext.createPackageContextAsUser(
+ callingPackageName, /* flags= */ 0, targetUserHandle);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new SecurityException(
+ "Package: "
+ + callingPackageName
+ + " haven't installed for user "
+ + targetUserHandle.getIdentifier());
+ }
+ return targetUserHandle;
+ }
+ throw new SecurityException(
+ "Permission denied while calling from uid "
+ + callingUid
+ + " with "
+ + targetUserHandle
+ + "; Requires permission: "
+ + Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+ }
+
+ /**
+ * Checks that the caller's supposed package name matches the uid making the call.
+ *
+ * @throws SecurityException if the package name and uid don't match.
+ */
+ private void validateCallingPackageInternal(
+ int actualCallingUid, @NonNull String claimedCallingPackage) {
+ UserHandle callingUserHandle = UserHandle.getUserHandleForUid(actualCallingUid);
+ Context actualCallingUserContext = mContext.createContextAsUser(
+ callingUserHandle, /* flags= */ 0);
+ int claimedCallingUid =
+ getPackageUid(actualCallingUserContext, claimedCallingPackage);
+ if (claimedCallingUid != actualCallingUid) {
+ throw new SecurityException(
+ "Specified calling package ["
+ + claimedCallingPackage
+ + "] does not match the calling uid "
+ + actualCallingUid);
+ }
+ }
+
+ /**
+ * Finds the UID of the {@code packageName} in the given {@code context}. Returns {@link
+ * Process#INVALID_UID} if unable to find the UID.
+ */
+ private int getPackageUid(@NonNull Context context, @NonNull String packageName) {
+ try {
+ return context.getPackageManager().getPackageUid(packageName, /* flags= */ 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ return Process.INVALID_UID;
+ }
+ }
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/ServiceHelper.java b/services/appfunctions/java/com/android/server/appfunctions/ServiceHelper.java
new file mode 100644
index 0000000..6cd87d3
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/ServiceHelper.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.annotation.NonNull;
+import android.content.Intent;
+import android.os.UserHandle;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Helper interface for AppFunctionService.
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public interface ServiceHelper {
+ /**
+ * Resolves the AppFunctionService for the target package.
+ *
+ * @param targetPackageName The package name of the target.
+ * @param targetUser The user which the caller is requesting to execute as.
+ * @return The intent to bind to the target service.
+ */
+ Intent resolveAppFunctionService(@NonNull String targetPackageName,
+ @NonNull UserHandle targetUser);
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/ServiceHelperImpl.java b/services/appfunctions/java/com/android/server/appfunctions/ServiceHelperImpl.java
new file mode 100644
index 0000000..e49fba5
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/ServiceHelperImpl.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.app.appfunctions.AppFunctionService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.UserHandle;
+
+import java.util.Objects;
+
+class ServiceHelperImpl implements ServiceHelper {
+ private final Context mContext;
+
+ // TODO(b/357551503): Keep track of unlocked users.
+
+ ServiceHelperImpl(@NonNull Context context) {
+ mContext = Objects.requireNonNull(context);
+ }
+
+ @Override
+ public Intent resolveAppFunctionService(@NonNull String targetPackageName,
+ @NonNull UserHandle targetUser) {
+ Intent serviceIntent = new Intent(AppFunctionService.SERVICE_INTERFACE);
+ serviceIntent.setPackage(targetPackageName);
+ ResolveInfo resolveInfo = mContext.createContextAsUser(targetUser, /* flags= */ 0)
+ .getPackageManager().resolveService(serviceIntent, 0);
+ if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+ return null;
+ }
+
+ ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ if (!Manifest.permission.BIND_APP_FUNCTION_SERVICE.equals(
+ serviceInfo.permission)) {
+ return null;
+ }
+ serviceIntent.setComponent(
+ new ComponentName(serviceInfo.packageName, serviceInfo.name));
+
+ return serviceIntent;
+ }
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 1be352e..0827f2a 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -24,7 +24,7 @@
import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
-import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_ACTIVITY;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
@@ -237,11 +237,11 @@
@Override
public void onActivityLaunchBlocked(int displayId,
- @NonNull ComponentName componentName, @UserIdInt int userId,
+ @NonNull ComponentName componentName, @NonNull UserHandle user,
@Nullable IntentSender intentSender) {
try {
mActivityListener.onActivityLaunchBlocked(
- displayId, componentName, userId, intentSender);
+ displayId, componentName, user, intentSender);
} catch (RemoteException e) {
Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e);
}
@@ -736,7 +736,7 @@
}
}
break;
- case POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR:
+ case POLICY_TYPE_BLOCKED_ACTIVITY:
if (android.companion.virtualdevice.flags.Flags.activityControlApi()) {
synchronized (mVirtualDeviceLock) {
mDevicePolicies.put(policyType, devicePolicy);
@@ -1371,8 +1371,7 @@
mActivityListenerAdapter.onActivityLaunchBlocked(
displayId,
activityInfo.getComponentName(),
- UserHandle.getUserHandleForUid(
- activityInfo.applicationInfo.uid).getIdentifier(),
+ UserHandle.getUserHandleForUid(activityInfo.applicationInfo.uid),
intentSender);
}
}
@@ -1388,7 +1387,7 @@
return true;
}
// Do not show the dialog if disabled by policy.
- return getDevicePolicy(POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR) == DEVICE_POLICY_DEFAULT;
+ return getDevicePolicy(POLICY_TYPE_BLOCKED_ACTIVITY) == DEVICE_POLICY_DEFAULT;
}
private void onSecureWindowShown(int displayId, int uid) {
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index c7c984b..ffb2bb6 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -5675,7 +5675,7 @@
// a "normal" rule, it must provide a CP/ConfigActivity too.
if (android.app.Flags.modesApi()) {
boolean isImplicitRuleUpdateFromSystem = updateId != null
- && ZenModeHelper.isImplicitRuleId(updateId)
+ && ZenModeConfig.isImplicitRuleId(updateId)
&& isCallerSystemOrSystemUi();
if (!isImplicitRuleUpdateFromSystem
&& rule.getOwner() == null
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 0f50260..ee3f48d 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -39,6 +39,7 @@
import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI;
import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_ACTIVATE;
import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE;
+import static android.service.notification.ZenModeConfig.implicitRuleId;
import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
import static com.android.internal.util.Preconditions.checkArgument;
@@ -155,8 +156,6 @@
static final int RULE_LIMIT_PER_PACKAGE = 100;
private static final Duration DELETED_RULE_KEPT_FOR = Duration.ofDays(30);
- private static final String IMPLICIT_RULE_ID_PREFIX = "implicit_"; // + pkg_name
-
private static final int MAX_ICON_RESOURCE_NAME_LENGTH = 1000;
/**
@@ -783,14 +782,6 @@
return rule;
}
- private static String implicitRuleId(String forPackage) {
- return IMPLICIT_RULE_ID_PREFIX + forPackage;
- }
-
- static boolean isImplicitRuleId(@NonNull String ruleId) {
- return ruleId.startsWith(IMPLICIT_RULE_ID_PREFIX);
- }
-
boolean removeAutomaticZenRule(String id, @ConfigOrigin int origin, String reason,
int callingUid) {
checkManageRuleOrigin("removeAutomaticZenRule", origin);
@@ -977,7 +968,16 @@
rule.setConditionOverride(OVERRIDE_DEACTIVATE);
}
}
+ } else if (origin == ORIGIN_USER_IN_APP && condition != null
+ && condition.source == SOURCE_USER_ACTION) {
+ // Remove override and just apply the condition. Since the app is reporting that the
+ // user asked for it, by definition it knows that, and will adjust its automatic
+ // behavior accordingly -> no need to override.
+ rule.condition = condition;
+ rule.resetConditionOverride();
} else {
+ // Update the condition, and check whether we can remove the override (if automatic
+ // and manual decisions agree).
rule.condition = condition;
rule.reconsiderConditionOverride();
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
index 6a99731..411a610 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
@@ -89,7 +89,7 @@
import javax.annotation.Nullable;
@RunWith(AndroidJUnit4.class)
-@EnableFlags(Flags.FLAG_VISIT_PERSON_URI)
+@EnableFlags({Flags.FLAG_VISIT_PERSON_URI, Flags.FLAG_API_RICH_ONGOING})
public class NotificationVisitUrisTest extends UiServiceTestCase {
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
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 ed8ebc8..dd2b845 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -82,6 +82,7 @@
import static com.android.server.notification.ZenModeEventLogger.ACTIVE_RULE_TYPE_MANUAL;
import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE;
+import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -6672,6 +6673,91 @@
assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE);
}
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void setAutomaticZenRuleState_withActivationOverride_userActionFromAppCanDeactivate() {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
+ .setPackage(mPkg)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding",
+ CUSTOM_PKG_UID);
+
+ // User manually turns on rule from SysUI / Settings...
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "manual-on-from-sysui", STATE_TRUE,
+ SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
+ assertThat(getZenRule(ruleId).isAutomaticActive()).isTrue();
+ assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE);
+
+ // ... and they can turn it off manually from inside the app.
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "manual-off-from-app", STATE_FALSE,
+ SOURCE_USER_ACTION), ORIGIN_USER_IN_APP, CUSTOM_PKG_UID);
+ assertThat(getZenRule(ruleId).isAutomaticActive()).isFalse();
+ assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void setAutomaticZenRuleState_withDeactivationOverride_userActionFromAppCanActivate() {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
+ .setPackage(mPkg)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding",
+ CUSTOM_PKG_UID);
+
+ // Rule is activated due to its schedule.
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "auto-on-from-app", STATE_TRUE,
+ SOURCE_SCHEDULE), ORIGIN_APP, CUSTOM_PKG_UID);
+ assertThat(getZenRule(ruleId).isAutomaticActive()).isTrue();
+ assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+
+ // User manually turns off rule from SysUI / Settings...
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "manual-off-from-sysui", STATE_FALSE,
+ SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
+ assertThat(getZenRule(ruleId).isAutomaticActive()).isFalse();
+ assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE);
+
+ // ... and they can turn it on manually from inside the app.
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "manual-on-from-app", STATE_TRUE,
+ SOURCE_USER_ACTION), ORIGIN_USER_IN_APP, CUSTOM_PKG_UID);
+ assertThat(getZenRule(ruleId).isAutomaticActive()).isTrue();
+ assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void setAutomaticZenRuleState_manualActionFromApp_isNotOverride() {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
+ .setPackage(mPkg)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding",
+ CUSTOM_PKG_UID);
+
+ // Rule is manually activated by the user in the app.
+ // This turns the rule on, but is NOT an override...
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "manual-on-from-app", STATE_TRUE,
+ SOURCE_USER_ACTION), ORIGIN_USER_IN_APP, CUSTOM_PKG_UID);
+ assertThat(getZenRule(ruleId).isAutomaticActive()).isTrue();
+ assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+
+ // ... so the app can turn it off when its schedule is over.
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "auto-off-from-app", STATE_FALSE,
+ SOURCE_SCHEDULE), ORIGIN_APP, CUSTOM_PKG_UID);
+ assertThat(getZenRule(ruleId).isAutomaticActive()).isFalse();
+ assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+ }
+
+ private ZenRule getZenRule(String ruleId) {
+ return checkNotNull(mZenModeHelper.mConfig.automaticRules.get(ruleId),
+ "Didn't find rule with id %s", ruleId);
+ }
+
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/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 8d1ba5b..c788f3b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -243,6 +243,18 @@
doReturn(embedded).when(mActivityStack.top()).isEmbedded();
}
+ void setTopActivityVisible(boolean isVisible) {
+ doReturn(isVisible).when(mActivityStack.top()).isVisible();
+ }
+
+ void setTopActivityVisibleRequested(boolean isVisibleRequested) {
+ doReturn(isVisibleRequested).when(mActivityStack.top()).isVisibleRequested();
+ }
+
+ void setTopActivityFillsParent(boolean fillsParent) {
+ doReturn(fillsParent).when(mActivityStack.top()).fillsParent();
+ }
+
void setTopActivityInMultiWindowMode(boolean multiWindowMode) {
doReturn(multiWindowMode).when(mActivityStack.top()).inMultiWindowMode();
if (multiWindowMode) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
index 7760051..05f6ed6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
@@ -26,6 +26,8 @@
import androidx.annotation.NonNull;
+import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType;
+
/**
* Robot implementation for {@link AppCompatConfiguration}.
*/
@@ -99,6 +101,32 @@
.getThinLetterboxHeightPx();
}
+ void setLetterboxActivityCornersRounded(boolean rounded) {
+ doReturn(rounded).when(mAppCompatConfiguration).isLetterboxActivityCornersRounded();
+ }
+
+ void setLetterboxEducationEnabled(boolean enabled) {
+ doReturn(enabled).when(mAppCompatConfiguration).getIsEducationEnabled();
+ }
+
+ void setLetterboxActivityCornersRadius(int cornerRadius) {
+ doReturn(cornerRadius).when(mAppCompatConfiguration).getLetterboxActivityCornersRadius();
+ }
+
+ void setLetterboxBackgroundType(@LetterboxBackgroundType int backgroundType) {
+ doReturn(backgroundType).when(mAppCompatConfiguration).getLetterboxBackgroundType();
+ }
+
+ void setLetterboxBackgroundWallpaperBlurRadiusPx(int blurRadiusPx) {
+ doReturn(blurRadiusPx).when(mAppCompatConfiguration)
+ .getLetterboxBackgroundWallpaperBlurRadiusPx();
+ }
+
+ void setLetterboxBackgroundWallpaperDarkScrimAlpha(float darkScrimAlpha) {
+ doReturn(darkScrimAlpha).when(mAppCompatConfiguration)
+ .getLetterboxBackgroundWallpaperDarkScrimAlpha();
+ }
+
void checkToNextLeftStop(boolean invoked) {
verify(mAppCompatConfiguration, times(invoked ? 1 : 0))
.movePositionForHorizontalReachabilityToNextLeftStop(anyBoolean());
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxOverrideTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxOverrideTest.java
new file mode 100644
index 0000000..af7f881
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxOverrideTest.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_OVERRIDE_UNSET;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
+
+import static junit.framework.Assert.assertEquals;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+
+/**
+ * Test class for {@link AppCompatLetterboxOverrides}.
+ *
+ * Build/Install/Run:
+ * atest WmTests:AppCompatLetterboxOverrideTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class AppCompatLetterboxOverrideTest extends WindowTestsBase {
+
+ @Test
+ public void testIsLetterboxEducationEnabled() {
+ runTestScenario((robot) -> {
+ robot.activity().createActivityWithComponent();
+
+ robot.conf().setLetterboxEducationEnabled(/* enabled */ true);
+ robot.checkLetterboxEducationEnabled(/* enabled */ true);
+
+ robot.conf().setLetterboxEducationEnabled(/* enabled */ false);
+ robot.checkLetterboxEducationEnabled(/* enabled */ false);
+ });
+ }
+
+ @Test
+ public void testShouldLetterboxHaveRoundedCorners() {
+ runTestScenario((robot) -> {
+ robot.activity().createActivityWithComponent();
+
+ robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+ robot.activity().setTopActivityFillsParent(/* fillsParent */ true);
+ robot.checkShouldLetterboxHaveRoundedCorners(/* expected */ true);
+
+ robot.conf().setLetterboxActivityCornersRounded(/* rounded */ false);
+ robot.checkShouldLetterboxHaveRoundedCorners(/* expected */ false);
+
+ robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+ robot.activity().setTopActivityFillsParent(/* fillsParent */ false);
+ robot.checkShouldLetterboxHaveRoundedCorners(/* expected */ false);
+ });
+ }
+
+ @Test
+ public void testHasWallpaperBackgroundForLetterbox() {
+ runTestScenario((robot) -> {
+ robot.activity().createActivityWithComponent();
+ robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ false);
+
+ robot.invokeCheckWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */ false);
+ robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ false);
+
+ robot.invokeCheckWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */ true);
+ robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ true);
+
+ robot.invokeCheckWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */ true);
+ robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ true);
+
+ robot.invokeCheckWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */ false);
+ robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ false);
+ });
+ }
+
+ @Test
+ public void testCheckWallpaperBackgroundForLetterbox() {
+ runTestScenario((robot) -> {
+ robot.activity().createActivityWithComponent();
+ robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ false);
+
+ robot.checkWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */
+ true, /* expected */ true);
+ robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ true);
+
+ robot.checkWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */
+ true, /* expected */ false);
+ robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ true);
+
+ robot.checkWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */
+ false, /* expected */ true);
+ robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ false);
+
+ robot.checkWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */
+ false, /* expected */ false);
+ robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ false);
+ });
+ }
+
+ @Test
+ public void testLetterboxActivityCornersRadius() {
+ runTestScenario((robot) -> {
+ robot.activity().createActivityWithComponent();
+
+ robot.conf().setLetterboxActivityCornersRadius(/* cornerRadius */ 0);
+ robot.checkLetterboxActivityCornersRadius(/* cornerRadius */ 0);
+
+ robot.conf().setLetterboxActivityCornersRadius(/* cornerRadius */ 37);
+ robot.checkLetterboxActivityCornersRadius(/* cornerRadius */ 37);
+
+ robot.conf().setLetterboxActivityCornersRadius(/* cornerRadius */ 5);
+ robot.checkLetterboxActivityCornersRadius(/* cornerRadius */ 5);
+ });
+ }
+
+ @Test
+ public void testLetterboxActivityCornersRaunded() {
+ runTestScenario((robot) -> {
+ robot.activity().createActivityWithComponent();
+
+ robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+ robot.checkLetterboxActivityCornersRounded(/* expected */ true);
+
+ robot.conf().setLetterboxActivityCornersRounded(/* rounded */ false);
+ robot.checkLetterboxActivityCornersRounded(/* expected */ false);
+ });
+ }
+
+ @Test
+ public void testLetterboxBackgroundType() {
+ runTestScenario((robot) -> {
+ robot.activity().createActivityWithComponent();
+
+ robot.conf().setLetterboxBackgroundType(LETTERBOX_BACKGROUND_OVERRIDE_UNSET);
+ robot.checkLetterboxBackgroundType(LETTERBOX_BACKGROUND_OVERRIDE_UNSET);
+
+ robot.conf().setLetterboxBackgroundType(LETTERBOX_BACKGROUND_SOLID_COLOR);
+ robot.checkLetterboxBackgroundType(LETTERBOX_BACKGROUND_SOLID_COLOR);
+
+ robot.conf().setLetterboxBackgroundType(LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND);
+ robot.checkLetterboxBackgroundType(LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND);
+
+ robot.conf().setLetterboxBackgroundType(
+ LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING);
+ robot.checkLetterboxBackgroundType(LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING);
+
+ robot.conf().setLetterboxBackgroundType(LETTERBOX_BACKGROUND_WALLPAPER);
+ robot.checkLetterboxBackgroundType(LETTERBOX_BACKGROUND_WALLPAPER);
+ });
+ }
+
+ @Test
+ public void testLetterboxBackgroundWallpaperBlurRadiusPx() {
+ runTestScenario((robot) -> {
+ robot.activity().createActivityWithComponent();
+
+ robot.conf().setLetterboxBackgroundWallpaperBlurRadiusPx(-1);
+ robot.checkLetterboxWallpaperBlurRadiusPx(0);
+
+ robot.conf().setLetterboxBackgroundWallpaperBlurRadiusPx(0);
+ robot.checkLetterboxWallpaperBlurRadiusPx(0);
+
+ robot.conf().setLetterboxBackgroundWallpaperBlurRadiusPx(10);
+ robot.checkLetterboxWallpaperBlurRadiusPx(10);
+ });
+ }
+
+ @Test
+ public void testLetterboxBackgroundWallpaperDarkScrimAlpha() {
+ runTestScenario((robot) -> {
+ robot.activity().createActivityWithComponent();
+
+ robot.conf().setLetterboxBackgroundWallpaperDarkScrimAlpha(-1f);
+ robot.checkLetterboxWallpaperDarkScrimAlpha(0);
+
+ robot.conf().setLetterboxBackgroundWallpaperDarkScrimAlpha(1.1f);
+ robot.checkLetterboxWallpaperDarkScrimAlpha(0);
+
+ robot.conf().setLetterboxBackgroundWallpaperDarkScrimAlpha(0.001f);
+ robot.checkLetterboxWallpaperDarkScrimAlpha(0.001f);
+
+ robot.conf().setLetterboxBackgroundWallpaperDarkScrimAlpha(0.999f);
+ robot.checkLetterboxWallpaperDarkScrimAlpha(0.999f);
+ });
+ }
+
+ /**
+ * Runs a test scenario providing a Robot.
+ */
+ void runTestScenario(@NonNull Consumer<LetterboxOverridesRobotTest> consumer) {
+ final LetterboxOverridesRobotTest robot =
+ new LetterboxOverridesRobotTest(mWm, mAtm, mSupervisor);
+ consumer.accept(robot);
+ }
+
+ private static class LetterboxOverridesRobotTest extends AppCompatRobotBase {
+ LetterboxOverridesRobotTest(@NonNull WindowManagerService wm,
+ @NonNull ActivityTaskManagerService atm,
+ @NonNull ActivityTaskSupervisor supervisor) {
+ super(wm, atm, supervisor);
+ }
+
+ void invokeCheckWallpaperBackgroundForLetterbox(boolean wallpaperShouldBeShown) {
+ getLetterboxOverrides().checkWallpaperBackgroundForLetterbox(wallpaperShouldBeShown);
+ }
+
+ void checkLetterboxEducationEnabled(boolean enabled) {
+ assertEquals(enabled, getLetterboxOverrides().isLetterboxEducationEnabled());
+ }
+
+ void checkShouldLetterboxHaveRoundedCorners(boolean expected) {
+ assertEquals(expected,
+ getLetterboxOverrides().shouldLetterboxHaveRoundedCorners());
+ }
+
+ void checkHasWallpaperBackgroundForLetterbox(boolean expected) {
+ assertEquals(expected,
+ getLetterboxOverrides().hasWallpaperBackgroundForLetterbox());
+ }
+
+ void checkWallpaperBackgroundForLetterbox(boolean wallpaperShouldBeShown,
+ boolean expected) {
+ assertEquals(expected,
+ getLetterboxOverrides().checkWallpaperBackgroundForLetterbox(
+ wallpaperShouldBeShown));
+ }
+
+ void checkLetterboxActivityCornersRadius(int expected) {
+ assertEquals(expected, getLetterboxOverrides().getLetterboxActivityCornersRadius());
+ }
+
+ void checkLetterboxActivityCornersRounded(boolean expected) {
+ assertEquals(expected, getLetterboxOverrides().isLetterboxActivityCornersRounded());
+ }
+
+ void checkLetterboxBackgroundType(@LetterboxBackgroundType int expected) {
+ assertEquals(expected, getLetterboxOverrides().getLetterboxBackgroundType());
+ }
+
+ void checkLetterboxWallpaperBlurRadiusPx(int expected) {
+ assertEquals(expected, getLetterboxOverrides().getLetterboxWallpaperBlurRadiusPx());
+ }
+
+ void checkLetterboxWallpaperDarkScrimAlpha(float expected) {
+ assertEquals(expected, getLetterboxOverrides().getLetterboxWallpaperDarkScrimAlpha(),
+ FLOAT_TOLLERANCE);
+ }
+
+ @NonNull
+ private AppCompatLetterboxOverrides getLetterboxOverrides() {
+ return activity().top().mAppCompatController.getAppCompatLetterboxOverrides();
+ }
+
+ }
+
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java
new file mode 100644
index 0000000..e046f7c
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.RoundedCorner;
+import android.view.RoundedCorners;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.R;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+
+/**
+ * Test class for {@link AppCompatLetterboxPolicy}.
+ *
+ * Build/Install/Run:
+ * atest WmTests:AppCompatLetterboxPolicyTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class AppCompatLetterboxPolicyTest extends WindowTestsBase {
+
+ @Test
+ public void testGetCropBoundsIfNeeded_handleCropForTransparentActivityBasedOnOpaqueBounds() {
+ runTestScenario((robot) -> {
+ robot.configureWindowStateWithTaskBar(/* hasTaskBarInsetsRoundedCorners */ true);
+ robot.activity().createActivityWithComponent();
+ robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+ robot.activity().setTopActivityVisible(/* isVisible */ true);
+ robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+ robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+ robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+ robot.setTopActivityTransparentPolicyRunning(/* running */ true);
+ robot.activity().configureTopActivityBounds(new Rect(0, 0, 500, 300));
+
+ robot.resizeMainWindow(/* newWidth */ 499, /* newHeight */ 299);
+ robot.checkWindowStateHasCropBounds(/* expected */ false);
+
+ robot.resizeMainWindow(/* newWidth */ 500, /* newHeight */ 300);
+ robot.checkWindowStateHasCropBounds(/* expected */ true);
+ });
+ }
+
+ @Test
+ public void testGetCropBoundsIfNeeded_noCrop() {
+ runTestScenario((robot) -> {
+ robot.configureWindowStateWithTaskBar(/* hasTaskBarInsetsRoundedCorners */ false);
+ robot.activity().createActivityWithComponent();
+ robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+ robot.activity().setTopActivityVisible(/* isVisible */ true);
+ robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+ robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+ robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+
+ // Do not apply crop if taskbar is collapsed
+ robot.collapseTaskBar();
+ robot.checkTaskBarIsExpanded(/* expected */ false);
+
+ robot.activity().configureTopActivityBounds(new Rect(50, 25, 150, 75));
+ robot.checkWindowStateHasCropBounds(/* expected */ true);
+ // Expected the same size of the activity.
+ robot.validateWindowStateCropBounds(0, 0, 100, 50);
+ });
+ }
+
+ @Test
+ public void testGetCropBoundsIfNeeded_appliesCrop() {
+ runTestScenario((robot) -> {
+ robot.configureWindowStateWithTaskBar(/* hasTaskBarInsetsRoundedCorners */ true);
+ robot.activity().createActivityWithComponent();
+ robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+ robot.activity().setTopActivityVisible(/* isVisible */ true);
+ robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+ robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+ robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+
+ // Apply crop if taskbar is expanded.
+ robot.expandTaskBar();
+ robot.checkTaskBarIsExpanded(/* expected */ true);
+
+ robot.activity().configureTopActivityBounds(new Rect(50, 0, 150, 100));
+ robot.checkWindowStateHasCropBounds(/* expected */ true);
+ // The task bar expanded height is removed from the crop height.
+ robot.validateWindowStateCropBounds(0, 0, 100, 80);
+ });
+ }
+
+ @Test
+ public void testGetCropBoundsIfNeeded_appliesCropWithSizeCompatScaling() {
+ runTestScenario((robot) -> {
+ robot.configureWindowStateWithTaskBar(/* hasTaskBarInsetsRoundedCorners */ true);
+ robot.activity().createActivityWithComponent();
+ robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+ robot.activity().setTopActivityVisible(/* isVisible */ true);
+ robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+ robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+ robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+
+ // Apply crop if taskbar is expanded.
+ robot.expandTaskBar();
+ robot.checkTaskBarIsExpanded(/* expected */ true);
+ robot.activity().setTopActivityInSizeCompatMode(/* isScm */ true);
+ robot.setInvCompatState(/* scale */ 2.0f);
+
+ robot.activity().configureTopActivityBounds(new Rect(50, 0, 150, 100));
+
+ robot.checkWindowStateHasCropBounds(/* expected */ true);
+ // The width and height, considering task bar, are scaled by 2.
+ robot.validateWindowStateCropBounds(0, 0, 200, 160);
+ });
+ }
+
+ @Test
+ public void testGetRoundedCornersRadius_withRoundedCornersFromInsets() {
+ runTestScenario((robot) -> {
+ robot.conf().setLetterboxActivityCornersRadius(-1);
+ robot.configureWindowState();
+ robot.activity().createActivityWithComponent();
+ robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+ robot.activity().setTopActivityVisible(/* isVisible */ true);
+ robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+ robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+ robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+
+ robot.setInvCompatState(/* scale */ 0.5f);
+ robot.configureInsetsRoundedCorners(new RoundedCorners(
+ /*topLeft=*/ null,
+ /*topRight=*/ null,
+ /*bottomRight=*/ new RoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT,
+ /* configurationRadius */ 15, /*centerX=*/ 1, /*centerY=*/ 1),
+ /*bottomLeft=*/ new RoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT,
+ 30 /*2 is to test selection of the min radius*/,
+ /*centerX=*/ 1, /*centerY=*/ 1)
+ ));
+ robot.checkWindowStateRoundedCornersRadius(/* expected */ 7);
+ });
+ }
+
+
+ @Test
+ public void testGetRoundedCornersRadius_withLetterboxActivityCornersRadius() {
+ runTestScenario((robot) -> {
+ robot.conf().setLetterboxActivityCornersRadius(15);
+ robot.configureWindowState();
+ robot.activity().createActivityWithComponent();
+ robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+ robot.activity().setTopActivityVisible(/* isVisible */ true);
+ robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+ robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+ robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+ robot.setInvCompatState(/* scale */ 0.5f);
+
+ robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ true);
+ robot.checkWindowStateRoundedCornersRadius(/* expected */ 7);
+
+ robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+ robot.checkWindowStateRoundedCornersRadius(/* expected */ 7);
+
+ robot.activity().setTopActivityVisibleRequested(/* isVisibleRequested */ false);
+ robot.activity().setTopActivityVisible(/* isVisible */ false);
+ robot.checkWindowStateRoundedCornersRadius(/* expected */ 0);
+
+ robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ true);
+ robot.checkWindowStateRoundedCornersRadius(/* expected */ 7);
+ });
+ }
+
+ @Test
+ public void testGetRoundedCornersRadius_noScalingApplied() {
+ runTestScenario((robot) -> {
+ robot.conf().setLetterboxActivityCornersRadius(15);
+ robot.configureWindowState();
+ robot.activity().createActivityWithComponent();
+ robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+ robot.activity().setTopActivityVisible(/* isVisible */ true);
+ robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+ robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+ robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+
+ robot.setInvCompatState(/* scale */ -1f);
+ robot.checkWindowStateRoundedCornersRadius(/* expected */ 15);
+
+ robot.setInvCompatState(/* scale */ 0f);
+ robot.checkWindowStateRoundedCornersRadius(/* expected */ 15);
+
+ robot.setInvCompatState(/* scale */ 1f);
+ robot.checkWindowStateRoundedCornersRadius(/* expected */ 15);
+ });
+ }
+
+ /**
+ * Runs a test scenario providing a Robot.
+ */
+ void runTestScenario(@NonNull Consumer<LetterboxPolicyRobotTest> consumer) {
+ final LetterboxPolicyRobotTest robot = new LetterboxPolicyRobotTest(mWm, mAtm, mSupervisor);
+ consumer.accept(robot);
+ }
+
+ private static class LetterboxPolicyRobotTest extends AppCompatRobotBase {
+
+ static final int TASKBAR_COLLAPSED_HEIGHT = 10;
+ static final int TASKBAR_EXPANDED_HEIGHT = 20;
+ private static final int SCREEN_WIDTH = 200;
+ private static final int SCREEN_HEIGHT = 100;
+ static final Rect TASKBAR_COLLAPSED_BOUNDS = new Rect(0,
+ SCREEN_HEIGHT - TASKBAR_COLLAPSED_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT);
+ static final Rect TASKBAR_EXPANDED_BOUNDS = new Rect(0,
+ SCREEN_HEIGHT - TASKBAR_EXPANDED_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT);
+
+ @NonNull
+ private final WindowState mWindowState;
+ @Nullable
+ private InsetsSource mTaskbar;
+ @Nullable
+ private InsetsState mInsetsState;
+
+ LetterboxPolicyRobotTest(@NonNull WindowManagerService wm,
+ @NonNull ActivityTaskManagerService atm,
+ @NonNull ActivityTaskSupervisor supervisor) {
+ super(wm, atm, supervisor);
+ mWindowState = mock(WindowState.class);
+ }
+
+ @Override
+ void onPostActivityCreation(@NonNull ActivityRecord activity) {
+ super.onPostActivityCreation(activity);
+ spyOn(getAspectRatioPolicy());
+ spyOn(getTransparentPolicy());
+ }
+
+ void configureWindowStateWithTaskBar(boolean hasInsetsRoundedCorners) {
+ configureWindowState(/* withTaskBar */ true, hasInsetsRoundedCorners);
+ }
+
+ void configureWindowState() {
+ configureWindowState(/* withTaskBar */ false, /* hasInsetsRoundedCorners */ false);
+ }
+
+ void configureInsetsRoundedCorners(@NonNull RoundedCorners roundedCorners) {
+ mInsetsState.setRoundedCorners(roundedCorners);
+ }
+
+ private void configureWindowState(boolean withTaskBar, boolean hasInsetsRoundedCorners) {
+ mInsetsState = new InsetsState();
+ if (withTaskBar) {
+ mTaskbar = new InsetsSource(/*id=*/ 0,
+ WindowInsets.Type.navigationBars());
+ if (hasInsetsRoundedCorners) {
+ mTaskbar.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
+ }
+ mTaskbar.setVisible(true);
+ mInsetsState.addSource(mTaskbar);
+ }
+ mWindowState.mInvGlobalScale = 1f;
+ final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
+ doReturn(mInsetsState).when(mWindowState).getInsetsState();
+ doReturn(attrs).when(mWindowState).getAttrs();
+ doReturn(true).when(mWindowState).isDrawn();
+ doReturn(true).when(mWindowState).isOnScreen();
+ doReturn(false).when(mWindowState).isLetterboxedForDisplayCutout();
+ doReturn(true).when(mWindowState).areAppWindowBoundsLetterboxed();
+ }
+
+ void setInvCompatState(float scale) {
+ mWindowState.mInvGlobalScale = scale;
+ }
+
+ void setTopActivityInLetterboxAnimation(boolean inLetterboxAnimation) {
+ doReturn(inLetterboxAnimation).when(activity().top()).isInLetterboxAnimation();
+ }
+
+ void setTopActivityTransparentPolicyRunning(boolean running) {
+ doReturn(running).when(getTransparentPolicy()).isRunning();
+ }
+
+ void setIsLetterboxedForFixedOrientationAndAspectRatio(boolean isLetterboxed) {
+ doReturn(isLetterboxed).when(getAspectRatioPolicy())
+ .isLetterboxedForFixedOrientationAndAspectRatio();
+ }
+
+ void resizeMainWindow(int newWidth, int newHeight) {
+ mWindowState.mRequestedWidth = newWidth;
+ mWindowState.mRequestedHeight = newHeight;
+ }
+
+ void collapseTaskBar() {
+ mTaskbar.setFrame(TASKBAR_COLLAPSED_BOUNDS);
+ }
+
+ void expandTaskBar() {
+ mTaskbar.setFrame(TASKBAR_EXPANDED_BOUNDS);
+ }
+
+ void checkWindowStateHasCropBounds(boolean expected) {
+ final Rect cropBounds = getAppCompatLetterboxPolicy().getCropBoundsIfNeeded(
+ mWindowState);
+ if (expected) {
+ assertNotNull(cropBounds);
+ } else {
+ assertNull(cropBounds);
+ }
+ }
+
+ void checkTaskBarIsExpanded(boolean expected) {
+ final InsetsSource expandedTaskBar = AppCompatUtils.getExpandedTaskbarOrNull(
+ mWindowState);
+ if (expected) {
+ assertNotNull(expandedTaskBar);
+ } else {
+ assertNull(expandedTaskBar);
+ }
+ }
+
+ void checkWindowStateRoundedCornersRadius(int expected) {
+ assertEquals(expected, getAppCompatLetterboxPolicy()
+ .getRoundedCornersRadius(mWindowState));
+ }
+
+ void validateWindowStateCropBounds(int left, int top, int right, int bottom) {
+ final Rect cropBounds = getAppCompatLetterboxPolicy().getCropBoundsIfNeeded(
+ mWindowState);
+ assertEquals(left, cropBounds.left);
+ assertEquals(top, cropBounds.top);
+ assertEquals(right, cropBounds.right);
+ assertEquals(bottom, cropBounds.bottom);
+ }
+
+ @NonNull
+ private AppCompatAspectRatioPolicy getAspectRatioPolicy() {
+ return activity().top().mAppCompatController.getAppCompatAspectRatioPolicy();
+ }
+
+ @NonNull
+ private TransparentPolicy getTransparentPolicy() {
+ return activity().top().mAppCompatController.getTransparentPolicy();
+ }
+
+ @NonNull
+ private AppCompatLetterboxPolicy getAppCompatLetterboxPolicy() {
+ return activity().top().mAppCompatController.getAppCompatLetterboxPolicy();
+ }
+
+ }
+
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatResourcesRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatResourcesRobot.java
new file mode 100644
index 0000000..05e9a0f
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatResourcesRobot.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.mockito.Mockito.doReturn;
+
+import android.content.res.Resources;
+
+import androidx.annotation.DimenRes;
+import androidx.annotation.NonNull;
+
+/**
+ * Robot for managing {@link Resources} in unit tests.
+ */
+public class AppCompatResourcesRobot {
+
+ @NonNull
+ private final Resources mResources;
+
+ AppCompatResourcesRobot(@NonNull Resources resources) {
+ mResources = resources;
+ spyOn(mResources);
+ }
+
+ void configureGetDimensionPixelSize(@DimenRes int resId, int value) {
+ doReturn(value).when(mResources).getDimensionPixelSize(resId);
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java
index 4e58e1d..5f2a63a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java
@@ -37,6 +37,8 @@
private final AppCompatConfigurationRobot mConfigurationRobot;
@NonNull
private final AppCompatComponentPropRobot mOptPropRobot;
+ @NonNull
+ private final AppCompatResourcesRobot mResourcesRobot;
AppCompatRobotBase(@NonNull WindowManagerService wm,
@NonNull ActivityTaskManagerService atm,
@@ -48,6 +50,7 @@
mConfigurationRobot =
new AppCompatConfigurationRobot(wm.mAppCompatConfiguration);
mOptPropRobot = new AppCompatComponentPropRobot(wm);
+ mResourcesRobot = new AppCompatResourcesRobot(wm.mContext.getResources());
}
AppCompatRobotBase(@NonNull WindowManagerService wm,
@@ -60,7 +63,7 @@
* Specific Robots can override this method to add operation to run on a newly created
* {@link ActivityRecord}. Common case is to invoke spyOn().
*
- * @param activity THe newly created {@link ActivityRecord}.
+ * @param activity The newly created {@link ActivityRecord}.
*/
@CallSuper
void onPostActivityCreation(@NonNull ActivityRecord activity) {
@@ -81,7 +84,6 @@
return mConfigurationRobot;
}
- @NonNull
void applyOnConf(@NonNull Consumer<AppCompatConfigurationRobot> consumer) {
consumer.accept(mConfigurationRobot);
}
@@ -91,7 +93,6 @@
return mActivityRobot;
}
- @NonNull
void applyOnActivity(@NonNull Consumer<AppCompatActivityRobot> consumer) {
consumer.accept(mActivityRobot);
}
@@ -101,8 +102,16 @@
return mOptPropRobot;
}
- @NonNull
void applyOnProp(@NonNull Consumer<AppCompatComponentPropRobot> consumer) {
consumer.accept(mOptPropRobot);
}
+
+ @NonNull
+ AppCompatResourcesRobot resources() {
+ return mResourcesRobot;
+ }
+
+ void applyOnResources(@NonNull Consumer<AppCompatResourcesRobot> consumer) {
+ consumer.accept(mResourcesRobot);
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
deleted file mode 100644
index 8947522..0000000
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * Copyright (C) 2022 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 static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
-
-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.spyOn;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.annotation.Nullable;
-import android.compat.testing.PlatformCompatChangeRule;
-import android.content.ComponentName;
-import android.content.res.Resources;
-import android.graphics.Rect;
-import android.platform.test.annotations.Presubmit;
-import android.view.InsetsSource;
-import android.view.InsetsState;
-import android.view.RoundedCorner;
-import android.view.RoundedCorners;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.R;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-
-/**
- * Test class for {@link LetterboxUiController}.
- *
- * Build/Install/Run:
- * atest WmTests:LetterboxUiControllerTest
- */
-@SmallTest
-@Presubmit
-@RunWith(WindowTestRunner.class)
-public class LetterboxUiControllerTest extends WindowTestsBase {
- private static final int TASKBAR_COLLAPSED_HEIGHT = 10;
- private static final int TASKBAR_EXPANDED_HEIGHT = 20;
- private static final int SCREEN_WIDTH = 200;
- private static final int SCREEN_HEIGHT = 100;
- private static final Rect TASKBAR_COLLAPSED_BOUNDS = new Rect(0,
- SCREEN_HEIGHT - TASKBAR_COLLAPSED_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT);
- private static final Rect TASKBAR_EXPANDED_BOUNDS = new Rect(0,
- SCREEN_HEIGHT - TASKBAR_EXPANDED_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT);
-
- @Rule
- public TestRule compatChangeRule = new PlatformCompatChangeRule();
-
- private ActivityRecord mActivity;
- private Task mTask;
- private DisplayContent mDisplayContent;
- private AppCompatConfiguration mAppCompatConfiguration;
- private final Rect mLetterboxedPortraitTaskBounds = new Rect();
-
- @Before
- public void setUp() throws Exception {
- mActivity = setUpActivityWithComponent();
-
- mAppCompatConfiguration = mWm.mAppCompatConfiguration;
- spyOn(mAppCompatConfiguration);
- }
-
- @Test
- public void testGetCropBoundsIfNeeded_handleCropForTransparentActivityBasedOnOpaqueBounds() {
- final InsetsSource taskbar = new InsetsSource(/*id=*/ 0,
- WindowInsets.Type.navigationBars());
- taskbar.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
- final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
- final Rect opaqueBounds = new Rect(0, 0, 500, 300);
- doReturn(opaqueBounds).when(mActivity).getBounds();
- // Activity is translucent
- spyOn(mActivity.mAppCompatController.getTransparentPolicy());
- when(mActivity.mAppCompatController.getTransparentPolicy()
- .isRunning()).thenReturn(true);
-
- // Makes requested sizes different
- mainWindow.mRequestedWidth = opaqueBounds.width() - 1;
- mainWindow.mRequestedHeight = opaqueBounds.height() - 1;
- final AppCompatLetterboxPolicy letterboxPolicy =
- mActivity.mAppCompatController.getAppCompatLetterboxPolicy();
- assertNull(letterboxPolicy.getCropBoundsIfNeeded(mainWindow));
-
- // Makes requested sizes equals
- mainWindow.mRequestedWidth = opaqueBounds.width();
- mainWindow.mRequestedHeight = opaqueBounds.height();
- assertNotNull(letterboxPolicy.getCropBoundsIfNeeded(mainWindow));
- }
-
- @Test
- public void testGetCropBoundsIfNeeded_noCrop() {
- final InsetsSource taskbar = new InsetsSource(/*id=*/ 0,
- WindowInsets.Type.navigationBars());
- final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
-
- // Do not apply crop if taskbar is collapsed
- taskbar.setFrame(TASKBAR_COLLAPSED_BOUNDS);
- assertNull(AppCompatUtils.getExpandedTaskbarOrNull(mainWindow));
-
- mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4,
- SCREEN_WIDTH - SCREEN_WIDTH / 4, SCREEN_HEIGHT - SCREEN_HEIGHT / 4);
-
- final AppCompatLetterboxPolicy letterboxPolicy =
- mActivity.mAppCompatController.getAppCompatLetterboxPolicy();
-
- final Rect noCrop = letterboxPolicy.getCropBoundsIfNeeded(mainWindow);
- assertNotEquals(null, noCrop);
- assertEquals(0, noCrop.left);
- assertEquals(0, noCrop.top);
- assertEquals(mLetterboxedPortraitTaskBounds.width(), noCrop.right);
- assertEquals(mLetterboxedPortraitTaskBounds.height(), noCrop.bottom);
- }
-
- @Test
- public void testGetCropBoundsIfNeeded_appliesCrop() {
- final InsetsSource taskbar = new InsetsSource(/*id=*/ 0,
- WindowInsets.Type.navigationBars());
- taskbar.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
- final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
-
- // Apply crop if taskbar is expanded
- taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS);
- assertNotNull(AppCompatUtils.getExpandedTaskbarOrNull(mainWindow));
-
- mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, 0, SCREEN_WIDTH - SCREEN_WIDTH / 4,
- SCREEN_HEIGHT);
-
- final AppCompatLetterboxPolicy letterboxPolicy =
- mActivity.mAppCompatController.getAppCompatLetterboxPolicy();
- final Rect crop = letterboxPolicy.getCropBoundsIfNeeded(mainWindow);
- assertNotEquals(null, crop);
- assertEquals(0, crop.left);
- assertEquals(0, crop.top);
- assertEquals(mLetterboxedPortraitTaskBounds.width(), crop.right);
- assertEquals(mLetterboxedPortraitTaskBounds.height() - TASKBAR_EXPANDED_HEIGHT,
- crop.bottom);
- }
-
- @Test
- public void testGetCropBoundsIfNeeded_appliesCropWithSizeCompatScaling() {
- final InsetsSource taskbar = new InsetsSource(/*id=*/ 0,
- WindowInsets.Type.navigationBars());
- taskbar.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
- final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
- final float scaling = 2.0f;
-
- // Apply crop if taskbar is expanded
- taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS);
- assertNotNull(AppCompatUtils.getExpandedTaskbarOrNull(mainWindow));
- // With SizeCompat scaling
- doReturn(true).when(mActivity).inSizeCompatMode();
- mainWindow.mInvGlobalScale = scaling;
-
- mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, 0, SCREEN_WIDTH - SCREEN_WIDTH / 4,
- SCREEN_HEIGHT);
-
- final int appWidth = mLetterboxedPortraitTaskBounds.width();
- final int appHeight = mLetterboxedPortraitTaskBounds.height();
-
- final AppCompatLetterboxPolicy letterboxPolicy =
- mActivity.mAppCompatController.getAppCompatLetterboxPolicy();
- final Rect crop = letterboxPolicy.getCropBoundsIfNeeded(mainWindow);
- assertNotEquals(null, crop);
- assertEquals(0, crop.left);
- assertEquals(0, crop.top);
- assertEquals((int) (appWidth * scaling), crop.right);
- assertEquals((int) ((appHeight - TASKBAR_EXPANDED_HEIGHT) * scaling), crop.bottom);
- }
-
- @Test
- public void testGetRoundedCornersRadius_withRoundedCornersFromInsets() {
- final float invGlobalScale = 0.5f;
- final int expectedRadius = 7;
- final int configurationRadius = 15;
-
- final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null);
- mainWindow.mInvGlobalScale = invGlobalScale;
- final InsetsState insets = mainWindow.getInsetsState();
-
- RoundedCorners roundedCorners = new RoundedCorners(
- /*topLeft=*/ null,
- /*topRight=*/ null,
- /*bottomRight=*/ new RoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT,
- configurationRadius, /*centerX=*/ 1, /*centerY=*/ 1),
- /*bottomLeft=*/ new RoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT,
- configurationRadius * 2 /*2 is to test selection of the min radius*/,
- /*centerX=*/ 1, /*centerY=*/ 1)
- );
- insets.setRoundedCorners(roundedCorners);
- mAppCompatConfiguration.setLetterboxActivityCornersRadius(-1);
-
- assertEquals(expectedRadius, mActivity.mAppCompatController.getAppCompatLetterboxPolicy()
- .getRoundedCornersRadius(mainWindow));
- }
-
- @Test
- public void testGetRoundedCornersRadius_withLetterboxActivityCornersRadius() {
- final float invGlobalScale = 0.5f;
- final int expectedRadius = 7;
- final int configurationRadius = 15;
-
- final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null);
- mainWindow.mInvGlobalScale = invGlobalScale;
- mAppCompatConfiguration.setLetterboxActivityCornersRadius(configurationRadius);
-
- final AppCompatLetterboxPolicy letterboxPolicy =
- mActivity.mAppCompatController.getAppCompatLetterboxPolicy();
-
- doReturn(true).when(mActivity).isInLetterboxAnimation();
- assertEquals(expectedRadius, letterboxPolicy.getRoundedCornersRadius(mainWindow));
-
- doReturn(false).when(mActivity).isInLetterboxAnimation();
- assertEquals(expectedRadius, letterboxPolicy.getRoundedCornersRadius(mainWindow));
-
- doReturn(false).when(mActivity).isVisibleRequested();
- doReturn(false).when(mActivity).isVisible();
- assertEquals(0, letterboxPolicy.getRoundedCornersRadius(mainWindow));
-
- doReturn(true).when(mActivity).isInLetterboxAnimation();
- assertEquals(expectedRadius, letterboxPolicy.getRoundedCornersRadius(mainWindow));
- }
-
- @Test
- public void testGetRoundedCornersRadius_noScalingApplied() {
- final int configurationRadius = 15;
-
- final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null);
- mAppCompatConfiguration.setLetterboxActivityCornersRadius(configurationRadius);
-
- final AppCompatLetterboxPolicy letterboxPolicy =
- mActivity.mAppCompatController.getAppCompatLetterboxPolicy();
-
- mainWindow.mInvGlobalScale = -1f;
- assertEquals(configurationRadius, letterboxPolicy.getRoundedCornersRadius(mainWindow));
-
- mainWindow.mInvGlobalScale = 0f;
- assertEquals(configurationRadius, letterboxPolicy.getRoundedCornersRadius(mainWindow));
-
- mainWindow.mInvGlobalScale = 1f;
- assertEquals(configurationRadius, letterboxPolicy.getRoundedCornersRadius(mainWindow));
- }
-
- private WindowState mockForGetCropBoundsAndRoundedCorners(@Nullable InsetsSource taskbar) {
- final WindowState mainWindow = mock(WindowState.class);
- final InsetsState insets = new InsetsState();
- final Resources resources = mWm.mContext.getResources();
- final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
-
- final AppCompatAspectRatioPolicy aspectRatioPolicy = mActivity.mAppCompatController
- .getAppCompatAspectRatioPolicy();
-
- mainWindow.mInvGlobalScale = 1f;
- spyOn(resources);
- spyOn(mActivity);
- spyOn(aspectRatioPolicy);
-
- if (taskbar != null) {
- taskbar.setVisible(true);
- insets.addSource(taskbar);
- }
- doReturn(mLetterboxedPortraitTaskBounds).when(mActivity).getBounds();
- doReturn(false).when(mActivity).isInLetterboxAnimation();
- doReturn(true).when(mActivity).isVisible();
- doReturn(true).when(aspectRatioPolicy)
- .isLetterboxedForFixedOrientationAndAspectRatio();
- doReturn(insets).when(mainWindow).getInsetsState();
- doReturn(attrs).when(mainWindow).getAttrs();
- doReturn(true).when(mainWindow).isDrawn();
- doReturn(true).when(mainWindow).isOnScreen();
- doReturn(false).when(mainWindow).isLetterboxedForDisplayCutout();
- doReturn(true).when(mainWindow).areAppWindowBoundsLetterboxed();
- doReturn(true).when(mAppCompatConfiguration).isLetterboxActivityCornersRounded();
- doReturn(TASKBAR_EXPANDED_HEIGHT).when(resources).getDimensionPixelSize(
- R.dimen.taskbar_frame_height);
-
- return mainWindow;
- }
-
- @Test
- public void testIsLetterboxEducationEnabled() {
- mActivity.mAppCompatController.getAppCompatLetterboxOverrides()
- .isLetterboxEducationEnabled();
- verify(mAppCompatConfiguration).getIsEducationEnabled();
- }
-
- private ActivityRecord setUpActivityWithComponent() {
- mDisplayContent = new TestDisplayContent
- .Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
- mTask = new TaskBuilder(mSupervisor).setDisplay(mDisplayContent).build();
- final ActivityRecord activity = new ActivityBuilder(mAtm)
- .setOnTop(true)
- .setTask(mTask)
- // Set the component to be that of the test class in order to enable compat changes
- .setComponent(ComponentName.createRelative(mContext,
- com.android.server.wm.LetterboxUiControllerTest.class.getName()))
- .build();
- spyOn(activity.mAppCompatController.getAppCompatCameraOverrides());
- return activity;
- }
-}