diff options
140 files changed, 4712 insertions, 996 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 0f3b1c366fb0..033da2df9bf6 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -4944,10 +4944,14 @@ public class AlarmManagerService extends SystemService { @Override public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (action == null) { + return; + } final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); synchronized (mLock) { String pkgList[] = null; - switch (intent.getAction()) { + switch (action) { case Intent.ACTION_QUERY_PACKAGE_RESTART: pkgList = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES); for (String packageName : pkgList) { diff --git a/core/api/current.txt b/core/api/current.txt index 8eb881139b34..836d4e99ce45 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -6854,6 +6854,47 @@ package android.app { method public android.app.Notification.MessagingStyle.Message setData(String, android.net.Uri); } + @FlaggedApi("android.app.api_rich_ongoing") public static class Notification.ProgressStyle extends android.app.Notification.Style { + ctor public Notification.ProgressStyle(); + method @NonNull public android.app.Notification.ProgressStyle addProgressSegment(@NonNull android.app.Notification.ProgressStyle.Segment); + method @NonNull public android.app.Notification.ProgressStyle addProgressStep(@NonNull android.app.Notification.ProgressStyle.Step); + method public int getProgress(); + method @Nullable public android.graphics.drawable.Icon getProgressEndIcon(); + method public int getProgressMax(); + method @NonNull public java.util.List<android.app.Notification.ProgressStyle.Segment> getProgressSegments(); + method @Nullable public android.graphics.drawable.Icon getProgressStartIcon(); + method @NonNull public java.util.List<android.app.Notification.ProgressStyle.Step> getProgressSteps(); + method @Nullable public android.graphics.drawable.Icon getProgressTrackerIcon(); + method public boolean isProgressIndeterminate(); + method public boolean isStyledByProgress(); + method @NonNull public android.app.Notification.ProgressStyle setProgress(int); + method @NonNull public android.app.Notification.ProgressStyle setProgressEndIcon(@Nullable android.graphics.drawable.Icon); + method @NonNull public android.app.Notification.ProgressStyle setProgressIndeterminate(boolean); + method @NonNull public android.app.Notification.ProgressStyle setProgressSegments(@NonNull java.util.List<android.app.Notification.ProgressStyle.Segment>); + method @NonNull public android.app.Notification.ProgressStyle setProgressStartIcon(@Nullable android.graphics.drawable.Icon); + method @NonNull public android.app.Notification.ProgressStyle setProgressSteps(@NonNull java.util.List<android.app.Notification.ProgressStyle.Step>); + method @NonNull public android.app.Notification.ProgressStyle setProgressTrackerIcon(@Nullable android.graphics.drawable.Icon); + method @NonNull public android.app.Notification.ProgressStyle setStyledByProgress(boolean); + } + + public static final class Notification.ProgressStyle.Segment { + ctor public Notification.ProgressStyle.Segment(int); + method @ColorInt public int getColor(); + method public int getLength(); + method public int getStableId(); + method @NonNull public android.app.Notification.ProgressStyle.Segment setColor(@ColorInt int); + method @NonNull public android.app.Notification.ProgressStyle.Segment setStableId(int); + } + + public static final class Notification.ProgressStyle.Step { + ctor public Notification.ProgressStyle.Step(int); + method @ColorInt public int getColor(); + method public int getPosition(); + method public int getStableId(); + method @NonNull public android.app.Notification.ProgressStyle.Step setColor(@ColorInt int); + method @NonNull public android.app.Notification.ProgressStyle.Step setStableId(int); + } + public abstract static class Notification.Style { ctor @Deprecated public Notification.Style(); method public android.app.Notification build(); @@ -8732,13 +8773,15 @@ package android.app.admin { package android.app.appfunctions { @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager { - method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); + method @Deprecated @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); + method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); } @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); + method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); + method @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 6511c214ee52..0a10920154b8 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1614,15 +1614,15 @@ package android.hardware.camera2 { public final class CameraManager { method @NonNull public android.hardware.camera2.CameraCharacteristics getCameraCharacteristics(@NonNull String, boolean) throws android.hardware.camera2.CameraAccessException; method public String[] getCameraIdListNoLazy() throws android.hardware.camera2.CameraAccessException; - method @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static int getRotationOverrideInternal(@Nullable android.content.Context, @Nullable android.content.pm.PackageManager, @Nullable String); + method @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static int getRotationOverrideInternal(@Nullable android.content.Context, @Nullable android.content.pm.PackageManager, @Nullable String); method @RequiresPermission(android.Manifest.permission.CAMERA) public void openCamera(@NonNull String, boolean, @Nullable android.os.Handler, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException; method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException; method public static boolean shouldOverrideToPortrait(@Nullable android.content.pm.PackageManager, @Nullable String); field public static final String LANDSCAPE_TO_PORTRAIT_PROP = "camera.enable_landscape_to_portrait"; field public static final long OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT = 250678880L; // 0xef10e60L - field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_NONE = 0; // 0x0 - field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT = 1; // 0x1 - field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_ROTATION_ONLY = 2; // 0x2 + field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_NONE = 0; // 0x0 + field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT = 1; // 0x1 + field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_ROTATION_ONLY = 2; // 0x2 } public abstract static class CameraManager.AvailabilityCallback { diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 392a1f113c23..a39f216d033e 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -121,7 +121,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -783,10 +782,32 @@ public class Notification implements Parcelable @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) public static final int FLAG_PROMOTED_ONGOING = 0x00040000; - private static final List<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Arrays.asList( - BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class, - DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class, - MessagingStyle.class, CallStyle.class); + private static final Set<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Set.of( + BigTextStyle.class, + BigPictureStyle.class, + InboxStyle.class, + MediaStyle.class, + DecoratedCustomViewStyle.class, + DecoratedMediaCustomViewStyle.class, + MessagingStyle.class, + CallStyle.class + ); + + private static boolean isPlatformStyle(Style style) { + if (style == null) { + return false; + } + + if (PLATFORM_STYLE_CLASSES.contains(style.getClass())) { + return true; + } + + if (Flags.apiRichOngoing()) { + return style.getClass() == ProgressStyle.class; + } + + return false; + } /** @hide */ @IntDef(flag = true, prefix = {"FLAG_"}, value = { @@ -1620,6 +1641,66 @@ public class Notification implements Parcelable public static final String EXTRA_COLORIZED = "android.colorized"; /** + * {@link #extras} key: an arraylist of {@link android.app.Notification.ProgressStyle.Segment} + * bundles provided by a + * {@link android.app.Notification.ProgressStyle} notification as supplied to + * {@link ProgressStyle#setProgressSegments} + * or {@link ProgressStyle#addProgressSegment(ProgressStyle.Segment)}. + * This extra is a parcelable array list of bundles. + * @hide + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_PROGRESS_SEGMENTS = "android.progressSegments"; + + /** + * {@link #extras} key: an arraylist of {@link android.app.Notification.ProgressStyle.Step} + * bundles provided by a + * {@link android.app.Notification.ProgressStyle} notification as supplied to + * {@link ProgressStyle#setProgressSteps} + * or {@link ProgressStyle#addProgressStep(ProgressStyle.Step)}. + * This extra is a parcelable array list of bundles. + * @hide + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_PROGRESS_STEPS = "android.progressSteps"; + + /** + * {@link #extras} key: whether the progress bar should be styled by its progress as + * supplied to {@link ProgressStyle#setStyledByProgress}. + * This extra is a boolean. + * @hide + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_STYLED_BY_PROGRESS = "android.styledByProgress"; + + /** + * {@link #extras} key: this is an {@link Icon} of an image to be + * shown as progress bar progress tracker icon in {@link ProgressStyle}, supplied to + *{@link ProgressStyle#setProgressTrackerIcon(Icon)}. + * @hide + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_PROGRESS_TRACKER_ICON = "android.progressTrackerIcon"; + + /** + * {@link #extras} key: this is an {@link Icon} of an image to be + * shown at the beginning of the progress bar in {@link ProgressStyle}, supplied to + *{@link ProgressStyle#setProgressStartIcon(Icon)}. + * @hide + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_PROGRESS_START_ICON = "android.progressStartIcon"; + + /** + * {@link #extras} key: this is an {@link Icon} of an image to be + * shown at the end of the progress bar in {@link ProgressStyle}, supplied to + *{@link ProgressStyle#setProgressEndIcon(Icon)}. + * @hide + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_PROGRESS_END_ICON = "android.progressEndIcon"; + + /** * @hide */ public static final String EXTRA_BUILDER_APPLICATION_INFO = "android.appInfo"; @@ -3072,6 +3153,9 @@ public class Notification implements Parcelable if (Flags.apiRichOngoing()) { visitIconUri(visitor, extras.getParcelable(EXTRA_ENROUTE_OVERLAY_ICON, Icon.class)); + visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_TRACKER_ICON, Icon.class)); + visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_START_ICON, Icon.class)); + visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_END_ICON, Icon.class)); } if (mBubbleMetadata != null) { @@ -6630,7 +6714,7 @@ public class Notification implements Parcelable // Custom views which come from a platform style class are safe, and thus do not need to // be wrapped. Any subclass of those styles has the opportunity to make arbitrary // changes to the RemoteViews, and thus can't be trusted as a fully vetted view. - if (fromStyle && PLATFORM_STYLE_CLASSES.contains(mStyle.getClass())) { + if (fromStyle && isPlatformStyle(mStyle)) { return false; } return mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S; @@ -7971,6 +8055,12 @@ public class Notification implements Parcelable return innerClass; } } + + if (Flags.apiRichOngoing()) { + if (templateClass.equals(ProgressStyle.class.getName())) { + return ProgressStyle.class; + } + } return null; } @@ -11220,6 +11310,724 @@ public class Notification implements Parcelable } } + + /** + * A Notification Style used to to define a notification whose expanded state includes + * a highly customizable progress bar with segments, steps, a custom tracker icon, + * and custom icons at the start and end of the progress bar. + * + * This style is suggested for use cases where the app is showing a tracker to the + * user of a thing they are interested in: the location of a car on its way + * to pick them up, food being delivered, or their own progress in a navigation + * journey. + * + * To use this style with your Notification, feed it to + * {@link Notification.Builder#setStyle(android.app.Notification.Style)} like so: + * <pre class="prettyprint"> + * new Notification.Builder(context) + * .setSmallIcon(R.drawable.ic_notification) + * .setColor(Color.GREEN) + * .setColorized(true) + * .setContentTitle("Arrive 10:08 AM"). + * .setContentText("Dominique Ansel Bakery Soho") + * .addAction(new Notification.Action("Exit navigation",...)) + * .setStyle(new Notification.ProgressStyle() + * .setStyledByProgress(false) + * .setProgress(456) + * .setProgressTrackerIcon(Icon.createWithResource(R.drawable.ic_driving_tracker)) + * .addProgressSegment(new Segment(41).setColor(Color.BLACK)) + * .addProgressSegment(new Segment(552).setColor(Color.YELLOW)) + * .addProgressSegment(new Segment(253).setColor(Color.YELLOW)) + * .addProgressSegment(new Segment(94).setColor(Color.BLUE)) + * .addProgressStep(new Step(60).setColor(Color.RED)) + * .addProgressStep(new Step(560).setColor(Color.YELLOW)) + * ) + * </pre> + * + * + * + * NOTE: The progress bar layout will be mirrored for RTL layout. + * NOTE: The extras set by {@link Notification.Builder#setProgress} will be overridden by + * the values set on this style object when the notification is built. + * + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static class ProgressStyle extends Notification.Style { + private static final String KEY_ELEMENT_STABLE_ID = "stableId"; + private static final String KEY_ELEMENT_COLOR = "colorInt"; + private static final String KEY_SEGMENT_LENGTH = "length"; + private static final String KEY_STEP_POSITION = "position"; + + private static final int MAX_PROGRESS_SEGMENT_LIMIT = 15; + private static final int MAX_PROGRESS_STEP_LIMIT = 5; + private static final int DEFAULT_PROGRESS_MAX = 100; + + private List<Segment> mProgressSegments = new ArrayList<>(); + private List<Step> mProgressSteps = new ArrayList<>(); + + private int mProgress = 0; + + private boolean mIndeterminate; + + private boolean mIsStyledByProgress = true; + + @Nullable + private Icon mTrackerIcon; + @Nullable + private Icon mStartIcon; + @Nullable + private Icon mEndIcon; + + /** + * @hide + */ + @Override + public boolean areNotificationsVisiblyDifferent(Style other) { + if (other == null || getClass() != other.getClass()) { + return true; + } + + final ProgressStyle progressStyle = (ProgressStyle) other; + + /** + * @see #setProgressIndeterminate + */ + if (!Objects.equals(mIndeterminate, progressStyle.mIndeterminate)) { + return true; + } + boolean nonIndeterminateCheckResult = false; + if (!mIndeterminate) { + nonIndeterminateCheckResult = !Objects.equals(mProgress, progressStyle.mProgress) + || !Objects.equals(mIsStyledByProgress, progressStyle.mIsStyledByProgress) + || !Objects.equals(mProgressSegments, progressStyle.mProgressSegments) + || !Objects.equals(mProgressSteps, progressStyle.mProgressSteps) + || !Objects.equals(mTrackerIcon, progressStyle.mTrackerIcon); + } + + return !Objects.equals(mStartIcon, progressStyle.mStartIcon) + || !Objects.equals(mEndIcon, progressStyle.mEndIcon) + || nonIndeterminateCheckResult; + } + + /** + * Gets the segments that define the background layer of the progress bar. + * + * If no segments are provided, the progress bar will be rendered with a single segment + * with length 100 and default color. + * + * @see #setProgressSegments + * @see #addProgressSegment + * @see Segment + */ + public @NonNull List<Segment> getProgressSegments() { + return mProgressSegments; + } + + /** + * Sets or replaces the segments of the progress bar. + * + * Segments allow for creating progress bars with multiple colors or sections + * to represent different stages or categories of progress. + * For example, Traffic conditions along a navigation journey. + * @see Segment + */ + public @NonNull ProgressStyle setProgressSegments(@NonNull List<Segment> progressSegments) { + mProgressSegments = new ArrayList<>(progressSegments.size()); + return this; + } + + /** + * Appends a segment to the end of the progress bar. + * + * Segments allow for creating progress bars with multiple colors or sections + * to represent different stages or categories of progress. + * For example, Traffic conditions along a navigation journey. + * @see Segment + */ + public @NonNull ProgressStyle addProgressSegment(@NonNull Segment segment) { + if (mProgressSegments == null) { + mProgressSegments = new ArrayList<>(); + } + mProgressSegments.add(segment); + + return this; + } + + /** + * Gets the steps that are displayed on the progress bar. + *. + * @see #setProgressSteps + * @see #addProgressStep + * @see Step + */ + public @NonNull List<Step> getProgressSteps() { + return mProgressSteps; + } + + /** + * Replaces all the progress steps. + * + * Steps are designated points within a progressbar to visualize + * distinct stages or milestones. + * For example, you might use steps to mark stops in a multi-stop + * navigation journey, where each step represents a destination. + * @see Step + */ + public @NonNull ProgressStyle setProgressSteps(@NonNull List<Step> steps) { + mProgressSteps = new ArrayList<>(steps); + return this; + } + + /** + * Adds another step. + * + * Steps are designated points within a progressbar to visualize + * distinct stages or milestones. + * For example, you might use steps to mark stops in a multi-stop + * navigation journey, where each step represents a destination. + * + * Steps can be added in any order, as their + * position within the progress bar is determined by their individual + * {@link Step#getPosition()}. + * @see Step + */ + public @NonNull ProgressStyle addProgressStep(@NonNull Step step) { + if (mProgressSteps == null) { + mProgressSteps = new ArrayList<>(); + } + mProgressSteps.add(step); + + return this; + } + + /** + * Gets the progress value of the progress bar. + * @see #setProgress + */ + public int getProgress() { + return mProgress; + } + + /** + * Specifies the progress (in the same units as {@link Segment#getLength()}) + * of the tracker along the length of the bar. + * + * The max progress value is the sum of all Segment lengths. + * The default value is 0. + */ + public @NonNull ProgressStyle setProgress(int progress) { + mProgress = progress; + return this; + } + + /** + * Gets the sum of the lengths of all Segments in the style, which + * defines the maximum progress. Defaults to 100 when segments are omitted. + */ + public int getProgressMax() { + final List<Segment> progressSegment = mProgressSegments; + if (progressSegment == null || progressSegment.isEmpty()) { + return DEFAULT_PROGRESS_MAX; + } else { + int progressMax = 0; + int validSegmentCount = 0; + for (int i = 0; i < progressSegment.size() + && validSegmentCount < MAX_PROGRESS_SEGMENT_LIMIT; i++) { + int segmentLength = progressSegment.get(i).getLength(); + if (segmentLength > 0) { + try { + progressMax = Math.addExact(progressMax, segmentLength); + validSegmentCount++; + } catch (ArithmeticException e) { + Log.e(TAG, + "Notification.ProgressStyle segment total overflowed.", e); + return DEFAULT_PROGRESS_MAX; + } + } + } + + if (validSegmentCount == 0) { + return DEFAULT_PROGRESS_MAX; + } + + return progressMax; + } + + } + + /** + * Get indeterminate value of the progress bar. + * @see #setProgressIndeterminate + */ + public boolean isProgressIndeterminate() { + return mIndeterminate; + } + + /** + * Used to indicate an initialization state without a known progress amount. + * When specified, the following fields are ignored: + * @see #setProgress + * @see #setProgressSegments + * @see #setProgressSteps + * @see #setProgressTrackerIcon + * @see #setStyledByProgress + * + * If the app provides exactly one Segment, that segment's color will be + * used to style the indeterminate bar. + */ + public @NonNull ProgressStyle setProgressIndeterminate(boolean indeterminate) { + mIndeterminate = indeterminate; + return this; + } + + /** + * Gets whether the progress bar's style is based on its progress. + * @see #setStyledByProgress + */ + public boolean isStyledByProgress() { + return mIsStyledByProgress; + } + + /** + * Indicates whether the segments and steps will be styled differently + * based on whether they are behind or ahead of the current progress. + * When true, segments appearing ahead of the current progress will be given a + * slightly different appearance to indicate that it is part of the progress bar + * that is not "filled". + * When false, all segments will be given the filled appearance, and it will be + * the app's responsibility to use #setProgressTrackerIcon or segment colors + * to make the current progress clear to the user. + * the default value is true. + */ + public @NonNull ProgressStyle setStyledByProgress(boolean enabled) { + mIsStyledByProgress = enabled; + return this; + } + + + /** + * Gets the progress tracker icon for the progress bar. + * @see #setProgressTrackerIcon + */ + public @Nullable Icon getProgressTrackerIcon() { + return mTrackerIcon; + } + + /** + * An optional icon that can appear as an overlay on the bar at the point of + * current progress. + * Aspect ratio may be anywhere from 2:1 to 1:2; content outside that + * aspect ratio range will be cropped. + * This icon will be mirrored in RTL. + */ + public @NonNull ProgressStyle setProgressTrackerIcon(@Nullable Icon trackerIcon) { + mTrackerIcon = trackerIcon; + return this; + } + + /** + * Gets the progress bar start icon. + * @see #setProgressStartIcon + */ + public @Nullable Icon getProgressStartIcon() { + return mStartIcon; + } + + /** + * An optional square icon that appears at the start of the progress bar. + * This icon will be cropped to its central square. + * This icon will NOT be mirrored in RTL layouts. + */ + public @NonNull ProgressStyle setProgressStartIcon(@Nullable Icon startIcon) { + mStartIcon = startIcon; + return this; + } + + /** + * Gets the progress bar end icon. + * @see #setProgressEndIcon(Icon) + */ + public @Nullable Icon getProgressEndIcon() { + return mEndIcon; + } + + /** + * An optional square icon that appears at the end of the progress bar. + * This icon will be cropped to its central square. + * This icon will NOT be mirrored in RTL layouts. + */ + public @NonNull ProgressStyle setProgressEndIcon(@Nullable Icon endIcon) { + mEndIcon = endIcon; + return this; + } + + /** + * @hide + */ + @Override + public void purgeResources() { + super.purgeResources(); + if (mTrackerIcon != null) { + mTrackerIcon.convertToAshmem(); + } + if (mStartIcon != null) { + mStartIcon.convertToAshmem(); + } + if (mEndIcon != null) { + mEndIcon.convertToAshmem(); + } + } + + /** + * @hide + */ + @Override + public void reduceImageSizes(Context context) { + super.reduceImageSizes(context); + + final Resources resources = context.getResources(); + + int progressIconSize = + resources.getDimensionPixelSize(R.dimen.notification_progress_icon_size); + if (mStartIcon != null) { + mStartIcon.scaleDownIfNecessary(progressIconSize, progressIconSize); + } + if (mEndIcon != null) { + mEndIcon.scaleDownIfNecessary(progressIconSize, progressIconSize); + } + if (mTrackerIcon != null) { + int progressTrackerWidth = resources.getDimensionPixelSize( + R.dimen.notification_progress_tracker_width); + int progressTrackerHeight = resources.getDimensionPixelSize( + R.dimen.notification_progress_tracker_height); + mTrackerIcon.scaleDownIfNecessary(progressTrackerWidth, progressTrackerHeight); + } + } + + /** + * @hide + */ + @Override + public void addExtras(Bundle extras) { + super.addExtras(extras); + extras.putParcelableArrayList(EXTRA_PROGRESS_SEGMENTS, + getProgressSegmentsAsBundleList(mProgressSegments)); + extras.putParcelableArrayList(EXTRA_PROGRESS_STEPS, + getProgressStepsAsBundleList(mProgressSteps)); + + extras.putInt(EXTRA_PROGRESS, mProgress); + extras.putBoolean(EXTRA_PROGRESS_INDETERMINATE, mIndeterminate); + extras.putInt(EXTRA_PROGRESS_MAX, getProgressMax()); + extras.putBoolean(EXTRA_STYLED_BY_PROGRESS, mIsStyledByProgress); + + if (mTrackerIcon != null) { + extras.putParcelable(EXTRA_PROGRESS_TRACKER_ICON, mTrackerIcon); + } else { + extras.remove(EXTRA_PROGRESS_TRACKER_ICON); + } + + if (mStartIcon != null) { + extras.putParcelable(EXTRA_PROGRESS_START_ICON, mStartIcon); + } else { + extras.remove(EXTRA_PROGRESS_START_ICON); + } + + if (mEndIcon != null) { + extras.putParcelable(EXTRA_PROGRESS_END_ICON, mEndIcon); + } else { + extras.remove(EXTRA_PROGRESS_END_ICON); + } + } + + /** + * @hide + */ + @Override + protected void restoreFromExtras(Bundle extras) { + super.restoreFromExtras(extras); + mProgressSegments = getProgressSegmentsFromBundleList( + extras.getParcelableArrayList(EXTRA_PROGRESS_SEGMENTS, Bundle.class)); + mProgress = extras.getInt(EXTRA_PROGRESS, 0); + mIndeterminate = extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE, false); + mIsStyledByProgress = extras.getBoolean(EXTRA_STYLED_BY_PROGRESS, true); + mTrackerIcon = extras.getParcelable(EXTRA_PROGRESS_TRACKER_ICON, Icon.class); + mStartIcon = extras.getParcelable(EXTRA_PROGRESS_START_ICON, Icon.class); + mEndIcon = extras.getParcelable(EXTRA_PROGRESS_END_ICON, Icon.class); + mProgressSteps = getProgressStepsFromBundleList( + extras.getParcelableArrayList(EXTRA_PROGRESS_STEPS, Bundle.class)); + } + + /** + * @hide + */ + @Override + public boolean displayCustomViewInline() { + // This is a lie; True is returned for progress notifications to make sure + // that the custom view is not used instead of the template, but it will not + // actually be included. + return true; + } + + private static @NonNull ArrayList<Bundle> getProgressSegmentsAsBundleList( + @Nullable List<Segment> progressSegments) { + final ArrayList<Bundle> segments = new ArrayList<>(); + if (progressSegments != null && !progressSegments.isEmpty()) { + for (int i = 0; i < progressSegments.size(); i++) { + final Segment segment = progressSegments.get(i); + if (segment.getLength() <= 0) { + continue; + } + + final Bundle bundle = new Bundle(); + bundle.putInt(KEY_SEGMENT_LENGTH, segment.getLength()); + bundle.putInt(KEY_ELEMENT_STABLE_ID, segment.getStableId()); + bundle.putInt(KEY_ELEMENT_COLOR, segment.getColor()); + + segments.add(bundle); + } + } + + return segments; + } + + private static @NonNull List<Segment> getProgressSegmentsFromBundleList( + @Nullable List<Bundle> segmentBundleList) { + final ArrayList<Segment> segments = new ArrayList<>(); + if (segmentBundleList != null && !segmentBundleList.isEmpty()) { + for (int i = 0; i < segmentBundleList.size(); i++) { + final Bundle segmentBundle = segmentBundleList.get(i); + final int length = segmentBundle.getInt(KEY_SEGMENT_LENGTH); + if (length <= 0) { + continue; + } + + final int stableId = segmentBundle.getInt(KEY_ELEMENT_STABLE_ID); + final int color = segmentBundle.getInt(KEY_ELEMENT_COLOR, + Notification.COLOR_DEFAULT); + final Segment segment = new Segment(length) + .setStableId(stableId).setColor(color); + + segments.add(segment); + } + } + + return segments; + } + + private static @NonNull ArrayList<Bundle> getProgressStepsAsBundleList( + @Nullable List<Step> progressSteps) { + final ArrayList<Bundle> steps = new ArrayList<>(); + if (progressSteps != null && !progressSteps.isEmpty()) { + for (int i = 0; i < progressSteps.size(); i++) { + final Step step = progressSteps.get(i); + if (step.getPosition() < 0) { + continue; + } + + final Bundle bundle = new Bundle(); + bundle.putInt(KEY_STEP_POSITION, step.getPosition()); + bundle.putInt(KEY_ELEMENT_STABLE_ID, step.getStableId()); + bundle.putInt(KEY_ELEMENT_COLOR, step.getColor()); + + steps.add(bundle); + } + } + + return steps; + } + + private static @NonNull List<Step> getProgressStepsFromBundleList( + @Nullable List<Bundle> stepBundleList) { + final ArrayList<Step> steps = new ArrayList<>(); + + if (stepBundleList != null && !stepBundleList.isEmpty()) { + for (int i = 0; i < stepBundleList.size(); i++) { + final Bundle segmentBundle = stepBundleList.get(i); + final int position = segmentBundle.getInt(KEY_STEP_POSITION); + if (position < 0) { + continue; + } + final int stableId = segmentBundle.getInt(KEY_ELEMENT_STABLE_ID); + final int color = segmentBundle.getInt(KEY_ELEMENT_COLOR, + Notification.COLOR_DEFAULT); + final Step step = new Step(position).setStableId(stableId).setColor(color); + steps.add(step); + } + } + + return steps; + } + + /** + * A segment of the progress bar, which defines its length and color. + * Segments allow for creating progress bars with multiple colors or sections + * to represent different stages or categories of progress. + * For example, Traffic conditions along a navigation journey. + */ + public static final class Segment { + private int mLength; + private int mStableId = 0; + @ColorInt + private int mColor = Notification.COLOR_DEFAULT; + + /** + * Create a segment with a non-zero length. + * @param length + * See {@link #getLength} + */ + public Segment(int length) { + mLength = length; + } + + /** + * The length of this Segment within the progress bar. + * This value has no units, it is just relative to the length of other segments, + * and the value provided to {@link ProgressStyle#setProgress}. + */ + public int getLength() { + return mLength; + } + + /** + * Gets the stable id of this Segment. + * + * @see #setStableId + */ + public int getStableId() { + return mStableId; + } + + /** + * Optional ID used to uniquely identify the element across updates. + */ + public @NonNull Segment setStableId(int stableId) { + mStableId = stableId; + return this; + } + + /** + * Returns the color of this Segment. + * + * @see #setColor + */ + @ColorInt + public int getColor() { + return mColor; + } + + /** + * Optional color of this Segment + */ + public @NonNull Segment setColor(@ColorInt int color) { + mColor = color; + return this; + } + + /** + * Needed for {@link Notification.Style#areNotificationsVisiblyDifferent} + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Segment segment = (Segment) o; + return mLength == segment.mLength && mStableId == segment.mStableId + && mColor == segment.mColor; + } + + @Override + public int hashCode() { + return Objects.hash(mLength, mStableId, mColor); + } + } + + /** + * A step within the progress bar, defining its position and color. + * Steps are designated points within a progressbar to visualize + * distinct stages or milestones. + * For example, you might use steps to mark stops in a multi-stop + * navigation journey, where each step represents a destination. + */ + public static final class Step { + + private int mPosition; + private int mStableId; + @ColorInt + private int mColor = Notification.COLOR_DEFAULT; + + /** + * Create a step element. + * The position of this step on the progress bar + * relative to {@link ProgressStyle#getProgressMax} + * @param position + * See {@link #getPosition} + */ + public Step(int position) { + mPosition = position; + } + + /** + * Gets the position of this Step. + * The position of this step on the progress bar + * relative to {@link ProgressStyle#getProgressMax}. + */ + public int getPosition() { + return mPosition; + } + + + /** + * Optional ID used to uniqurely identify the element across updates. + */ + public int getStableId() { + return mStableId; + } + + /** + * Optional ID used to uniqurely identify the element across updates. + */ + public @NonNull Step setStableId(int stableId) { + mStableId = stableId; + return this; + } + + /** + * Returns the color of this Segment. + * + * @see #setColor + */ + @ColorInt + public int getColor() { + return mColor; + } + + /** + * Optional color of this Segment + */ + public @NonNull Step setColor(@ColorInt int color) { + mColor = color; + return this; + } + + /** + * Needed for {@link Notification.Style#areNotificationsVisiblyDifferent} + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Step step = (Step) o; + return mPosition == step.mPosition && mStableId == step.mStableId + && mColor == step.mColor; + } + + @Override + public int hashCode() { + return Objects.hash(mPosition, mStableId, mColor); + } + } + } + /** * Notification style for custom views that are decorated by the system * diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java index 4682f3d30e1e..216ba5d994ec 100644 --- a/core/java/android/app/appfunctions/AppFunctionManager.java +++ b/core/java/android/app/appfunctions/AppFunctionManager.java @@ -27,6 +27,8 @@ import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.annotation.UserHandleAware; import android.content.Context; +import android.os.CancellationSignal; +import android.os.ICancellationSignal; import android.os.RemoteException; import java.util.Objects; @@ -73,7 +75,43 @@ public final class AppFunctionManager { * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code * android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code * ExecuteAppFunctionResponse.RESULT_DENIED}. + * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor, + * CancellationSignal, Consumer)} instead. This method will be removed once usage references + * are updated. */ + @RequiresPermission( + anyOf = { + Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, + Manifest.permission.EXECUTE_APP_FUNCTIONS + }, + conditional = true) + @UserHandleAware + @Deprecated + public void executeAppFunction( + @NonNull ExecuteAppFunctionRequest request, + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + executeAppFunction(request, executor, new CancellationSignal(), callback); + } + + /** + * Executes the app function. + * + * <p>Note: Applications can execute functions they define. To execute functions defined in + * another component, apps would need to have {@code + * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code + * android.permission.EXECUTE_APP_FUNCTIONS}. + * + * @param request the request to execute the app function + * @param executor the executor to run the callback + * @param cancellationSignal the cancellation signal to cancel the execution. + * @param callback the callback to receive the function execution result. if the calling app + * does not own the app function or does not have {@code + * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code + * android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code + * ExecuteAppFunctionResponse.RESULT_DENIED}. + */ + // TODO(b/357551503): Document the behavior when the cancellation signal is issued. // TODO(b/360864791): Document that apps can opt-out from being executed by callers with // EXECUTE_APP_FUNCTIONS and how a caller knows whether a function is opted out. // TODO(b/357551503): Update documentation when get / set APIs are implemented that this will @@ -88,6 +126,7 @@ public final class AppFunctionManager { public void executeAppFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull @CallbackExecutor Executor executor, + @NonNull CancellationSignal cancellationSignal, @NonNull Consumer<ExecuteAppFunctionResponse> callback) { Objects.requireNonNull(request); Objects.requireNonNull(executor); @@ -96,25 +135,31 @@ public final class AppFunctionManager { ExecuteAppFunctionAidlRequest aidlRequest = new ExecuteAppFunctionAidlRequest( request, mContext.getUser(), mContext.getPackageName()); + try { - mService.executeAppFunction( - aidlRequest, - new IExecuteAppFunctionCallback.Stub() { - @Override - public void onResult(ExecuteAppFunctionResponse result) { - try { - executor.execute(() -> callback.accept(result)); - } catch (RuntimeException e) { - // Ideally shouldn't happen since errors are wrapped into the - // response, but we catch it here for additional safety. - callback.accept( - ExecuteAppFunctionResponse.newFailure( - getResultCode(e), - e.getMessage(), - /* extras= */ null)); - } - } - }); + ICancellationSignal cancellationTransport = + mService.executeAppFunction( + aidlRequest, + new IExecuteAppFunctionCallback.Stub() { + @Override + public void onResult(ExecuteAppFunctionResponse result) { + try { + executor.execute(() -> callback.accept(result)); + } catch (RuntimeException e) { + // Ideally shouldn't happen since errors are wrapped into + // the + // response, but we catch it here for additional safety. + callback.accept( + ExecuteAppFunctionResponse.newFailure( + getResultCode(e), + e.getMessage(), + /* extras= */ null)); + } + } + }); + if (cancellationTransport != null) { + cancellationSignal.setRemote(cancellationTransport); + } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java index 0d981ea5a679..8e417737515e 100644 --- a/core/java/android/app/appfunctions/AppFunctionService.java +++ b/core/java/android/app/appfunctions/AppFunctionService.java @@ -29,7 +29,12 @@ import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.Binder; +import android.os.Bundle; import android.os.IBinder; +import android.os.ICancellationSignal; +import android.os.CancellationSignal; +import android.os.RemoteCallback; +import android.os.RemoteException; import java.util.function.Consumer; @@ -74,6 +79,7 @@ public abstract class AppFunctionService extends Service { */ void perform( @NonNull ExecuteAppFunctionRequest request, + @NonNull CancellationSignal cancellationSignal, @NonNull Consumer<ExecuteAppFunctionResponse> callback); } @@ -85,6 +91,7 @@ public abstract class AppFunctionService extends Service { @Override public void executeAppFunction( @NonNull ExecuteAppFunctionRequest request, + @NonNull ICancellationCallback cancellationCallback, @NonNull IExecuteAppFunctionCallback callback) { if (context.checkCallingPermission(BIND_APP_FUNCTION_SERVICE) == PERMISSION_DENIED) { @@ -93,7 +100,10 @@ public abstract class AppFunctionService extends Service { SafeOneTimeExecuteAppFunctionCallback safeCallback = new SafeOneTimeExecuteAppFunctionCallback(callback); try { - onExecuteFunction.perform(request, safeCallback::onResult); + onExecuteFunction.perform( + request, + buildCancellationSignal(cancellationCallback), + safeCallback::onResult); } catch (Exception ex) { // Apps should handle exceptions. But if they don't, report the error on // behalf of them. @@ -105,6 +115,21 @@ public abstract class AppFunctionService extends Service { }; } + private static CancellationSignal buildCancellationSignal( + @NonNull ICancellationCallback cancellationCallback) { + final ICancellationSignal cancellationSignalTransport = + CancellationSignal.createTransport(); + CancellationSignal cancellationSignal = + CancellationSignal.fromTransport(cancellationSignalTransport); + try { + cancellationCallback.sendCancellationTransport(cancellationSignalTransport); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + return cancellationSignal ; + } + private final Binder mBinder = createBinder( AppFunctionService.this, AppFunctionService.this::onExecuteFunction); @@ -115,6 +140,7 @@ public abstract class AppFunctionService extends Service { return mBinder; } + /** * Called by the system to execute a specific app function. * @@ -134,9 +160,45 @@ public abstract class AppFunctionService extends Service { * * @param request The function execution request. * @param callback A callback to report back the result. + * + * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal, + * Consumer)} instead. This method will be removed once usage references are updated. */ @MainThread + @Deprecated public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull Consumer<ExecuteAppFunctionResponse> callback); + + /** + * Called by the system to execute a specific app function. + * + * <p>This method is triggered when the system requests your AppFunctionService to handle a + * particular function you have registered and made available. + * + * <p>To ensure proper routing of function requests, assign a unique identifier to each + * function. This identifier doesn't need to be globally unique, but it must be unique within + * your app. For example, a function to order food could be identified as "orderFood". In most + * cases this identifier should come from the ID automatically generated by the AppFunctions + * SDK. You can determine the specific function to invoke by calling {@link + * ExecuteAppFunctionRequest#getFunctionIdentifier()}. + * + * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker + * thread and dispatch the result with the given callback. You should always report back the + * result using the callback, no matter if the execution was successful or not. + * + * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel + * the execution of function if requested by the system. + * + * @param request The function execution request. + * @param cancellationSignal A signal to cancel the execution. + * @param callback A callback to report back the result. + */ + @MainThread + public void onExecuteFunction( + @NonNull ExecuteAppFunctionRequest request, + @NonNull CancellationSignal cancellationSignal, + @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + onExecuteFunction(request, callback); + } } diff --git a/core/java/android/app/appfunctions/IAppFunctionManager.aidl b/core/java/android/app/appfunctions/IAppFunctionManager.aidl index 28827bb3052c..c63217ffe850 100644 --- a/core/java/android/app/appfunctions/IAppFunctionManager.aidl +++ b/core/java/android/app/appfunctions/IAppFunctionManager.aidl @@ -18,6 +18,7 @@ package android.app.appfunctions; import android.app.appfunctions.ExecuteAppFunctionAidlRequest; import android.app.appfunctions.IExecuteAppFunctionCallback; +import android.os.ICancellationSignal; /** * Defines the interface for apps to interact with the app function execution service @@ -32,8 +33,8 @@ interface IAppFunctionManager { * @param callback the callback to report the result. */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf = {android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED,android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional = true)") - void executeAppFunction( + ICancellationSignal executeAppFunction( in ExecuteAppFunctionAidlRequest request, in IExecuteAppFunctionCallback callback ); -}
\ No newline at end of file +} diff --git a/core/java/android/app/appfunctions/IAppFunctionService.aidl b/core/java/android/app/appfunctions/IAppFunctionService.aidl index cc5a20cfa194..291f33ccb1b8 100644 --- a/core/java/android/app/appfunctions/IAppFunctionService.aidl +++ b/core/java/android/app/appfunctions/IAppFunctionService.aidl @@ -16,7 +16,7 @@ package android.app.appfunctions; -import android.os.Bundle; +import android.app.appfunctions.ICancellationCallback; import android.app.appfunctions.IExecuteAppFunctionCallback; import android.app.appfunctions.ExecuteAppFunctionRequest; @@ -34,10 +34,12 @@ oneway interface IAppFunctionService { * Called by the system to execute a specific app function. * * @param request the function execution request. + * @param cancellationCallback a callback to send back the cancellation transport. * @param callback a callback to report back the result. */ void executeAppFunction( in ExecuteAppFunctionRequest request, + in ICancellationCallback cancellationCallback, in IExecuteAppFunctionCallback callback ); } diff --git a/core/java/android/app/appfunctions/ICancellationCallback.aidl b/core/java/android/app/appfunctions/ICancellationCallback.aidl new file mode 100644 index 000000000000..03235aca017a --- /dev/null +++ b/core/java/android/app/appfunctions/ICancellationCallback.aidl @@ -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 android.app.appfunctions; + +import android.os.ICancellationSignal; + +/** {@hide} */ +oneway interface ICancellationCallback { + void sendCancellationTransport(in ICancellationSignal cancellationTransport); +}
\ No newline at end of file diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index 21627920f598..1b21bdf7ba45 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -181,7 +181,7 @@ public final class CameraManager { * @hide */ @TestApi - @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public static final int ROTATION_OVERRIDE_NONE = ICameraService.ROTATION_OVERRIDE_NONE; /** @@ -191,7 +191,7 @@ public final class CameraManager { * @hide */ @TestApi - @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT = ICameraService.ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT; @@ -201,7 +201,7 @@ public final class CameraManager { * @hide */ @TestApi - @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public static final int ROTATION_OVERRIDE_ROTATION_ONLY = ICameraService.ROTATION_OVERRIDE_ROTATION_ONLY; @@ -1562,7 +1562,7 @@ public final class CameraManager { */ public static int getRotationOverride(@Nullable Context context, @Nullable PackageManager packageManager, @Nullable String packageName) { - if (com.android.window.flags.Flags.cameraCompatForFreeform()) { + if (com.android.window.flags.Flags.enableCameraCompatForDesktopWindowing()) { return getRotationOverrideInternal(context, packageManager, packageName); } else { return shouldOverrideToPortrait(packageManager, packageName) @@ -1574,7 +1574,7 @@ public final class CameraManager { /** * @hide */ - @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @TestApi public static int getRotationOverrideInternal(@Nullable Context context, @Nullable PackageManager packageManager, @Nullable String packageName) { diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index b8a8be159d12..d82af55e2771 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -10750,6 +10750,16 @@ public final class Settings { "lock_screen_show_only_unseen_notifications"; /** + * Indicates whether to minimalize the number of notifications to show on the lockscreen. + * <p> + * Type: int (0 for false, 1 for true) + * + * @hide + */ + public static final String LOCK_SCREEN_NOTIFICATION_MINIMALISM = + "lock_screen_notification_minimalism"; + + /** * Indicates whether snooze options should be shown on notifications * <p> * Type: int (0 for false, 1 for true) diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig index 45e9def2c15f..5457bbee8ad3 100644 --- a/core/java/android/security/responsible_apis_flags.aconfig +++ b/core/java/android/security/responsible_apis_flags.aconfig @@ -24,6 +24,17 @@ flag { } flag { + name: "asm_reintroduce_grace_period" + namespace: "responsible_apis" + description: "Allow launches within the grace period for ASM apps" + bug: "367702727" + metadata { + purpose: PURPOSE_BUGFIX + } +} + + +flag { name: "content_uri_permission_apis" is_exported: true namespace: "responsible_apis" diff --git a/core/java/android/view/WindowLayout.java b/core/java/android/view/WindowLayout.java index dda399357d8c..d5ccca992b4f 100644 --- a/core/java/android/view/WindowLayout.java +++ b/core/java/android/view/WindowLayout.java @@ -157,10 +157,10 @@ public class WindowLayout { // which prevents overlap with the DisplayCutout. if (!attachedInParent && !floatingInScreenWindow) { mTempRect.set(outParentFrame); - outParentFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars); + intersectOrClamp(outParentFrame, displayCutoutSafeExceptMaybeBars); frames.isParentFrameClippedByDisplayCutout = !mTempRect.equals(outParentFrame); } - outDisplayFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars); + intersectOrClamp(outDisplayFrame, displayCutoutSafeExceptMaybeBars); } final boolean noLimits = (attrs.flags & FLAG_LAYOUT_NO_LIMITS) != 0; @@ -283,6 +283,19 @@ public class WindowLayout { + " requestedInvisibleTypes=" + WindowInsets.Type.toString(~requestedVisibleTypes)); } + /** + * If both rectangles intersect, set inOutRect to that intersection. Otherwise, clamp inOutRect + * to the side (or the corner) that the other rectangle is away from. + * Unlike {@link Rect#intersectUnchecked(Rect)}, this method guarantees that the new rectangle + * is valid and contained in inOutRect if rectangles involved are valid. + */ + private static void intersectOrClamp(Rect inOutRect, Rect other) { + inOutRect.left = Math.min(Math.max(inOutRect.left, other.left), inOutRect.right); + inOutRect.top = Math.min(Math.max(inOutRect.top, other.top), inOutRect.bottom); + inOutRect.right = Math.max(Math.min(inOutRect.right, other.right), inOutRect.left); + inOutRect.bottom = Math.max(Math.min(inOutRect.bottom, other.bottom), inOutRect.top); + } + public static void extendFrameByCutout(Rect displayCutoutSafe, Rect displayFrame, Rect inOutFrame, Rect tempRect) { if (displayCutoutSafe.contains(inOutFrame)) { diff --git a/core/proto/android/server/vibrator/vibratormanagerservice.proto b/core/proto/android/server/vibrator/vibratormanagerservice.proto index e7f0560612cc..258832e3e7ff 100644 --- a/core/proto/android/server/vibrator/vibratormanagerservice.proto +++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto @@ -157,10 +157,8 @@ message VibratorManagerServiceDumpProto { option (.android.msg_privacy).dest = DEST_AUTOMATIC; repeated int32 vibrator_ids = 1; optional VibrationProto current_vibration = 2; - optional bool is_vibrating = 3; optional int32 is_vibrator_controller_registered = 27; optional VibrationProto current_external_vibration = 4; - optional bool vibrator_under_external_control = 5; optional bool low_power_mode = 6; optional bool vibrate_on = 24; reserved 25; // prev keyboard_vibration_on @@ -183,4 +181,6 @@ message VibratorManagerServiceDumpProto { repeated VibrationProto previous_vibrations = 16; repeated VibrationParamProto previous_vibration_params = 28; reserved 17; // prev previous_external_vibrations + reserved 3; // prev is_vibrating, check current_vibration instead + reserved 5; // prev vibrator_under_external_control, check current_external_vibration instead }
\ No newline at end of file diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 07efad89010a..92c390656da5 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4457,6 +4457,11 @@ <!-- Bytes that the PinnerService will pin for WebView --> <integer name="config_pinnerWebviewPinBytes">0</integer> + <!-- Maximum memory that PinnerService will pin for apps expressed + as a percentage of total device memory [0,100]. + Example: 10, means 10% of total memory will be the maximum pinned memory --> + <integer name="config_pinnerMaxPinnedMemoryPercentage">10</integer> + <!-- Number of days preloaded file cache should be preserved on a device before it can be deleted --> <integer name="config_keepPreloadsMinDays">7</integer> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index f397ef2b151c..6683dc044c9a 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -808,6 +808,12 @@ This is bigger than displayed because listeners can use it for other displays e.g. wearables. --> <dimen name="notification_person_icon_max_size">144dp</dimen> + <!-- The size of the progress bar icon --> + <dimen name="notification_progress_icon_size">20dp</dimen> + <!-- The size of the progress tracker width --> + <dimen name="notification_progress_tracker_width">40dp</dimen> + <!-- The size of the progress tracker height --> + <dimen name="notification_progress_tracker_height">20dp</dimen> <!-- The maximum size of the small notification icon on low memory devices. --> <dimen name="notification_small_icon_size_low_ram">@dimen/notification_small_icon_size</dimen> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 06b36b8f74af..5f40a6c7eba4 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3467,6 +3467,7 @@ <java-symbol type="integer" name="config_pinnerHomePinBytes" /> <java-symbol type="bool" name="config_pinnerAssistantApp" /> <java-symbol type="integer" name="config_pinnerWebviewPinBytes" /> + <java-symbol type="integer" name="config_pinnerMaxPinnedMemoryPercentage" /> <java-symbol type="string" name="config_doubleTouchGestureEnableFile" /> @@ -3854,6 +3855,9 @@ <java-symbol type="dimen" name="notification_custom_view_max_image_height"/> <java-symbol type="dimen" name="notification_custom_view_max_image_width"/> <java-symbol type="dimen" name="notification_person_icon_max_size" /> + <java-symbol type="dimen" name="notification_progress_icon_size" /> + <java-symbol type="dimen" name="notification_progress_tracker_width" /> + <java-symbol type="dimen" name="notification_progress_tracker_height" /> <java-symbol type="dimen" name="notification_small_icon_size_low_ram"/> <java-symbol type="dimen" name="notification_big_picture_max_height_low_ram"/> diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index 0f73df92ca93..edcea241e620 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -97,7 +97,6 @@ import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TextAppearanceSpan; import android.util.Pair; -import android.util.Slog; import android.widget.RemoteViews; import androidx.test.InstrumentationRegistry; @@ -118,6 +117,7 @@ import org.junit.Test; import org.junit.rules.TestRule; import org.junit.runner.RunWith; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -2114,6 +2114,300 @@ public class NotificationTest { assertThat(n.getWhen()).isEqualTo(9); } + @Test + public void getNotificationStyleClass_forPlatformClassName_returnsPlatformClass() { + final List<Class<? extends Notification.Style>> platformStyleClasses = List.of( + Notification.BigTextStyle.class, Notification.BigPictureStyle.class, + Notification.MessagingStyle.class, Notification.CallStyle.class, + Notification.InboxStyle.class, Notification.MediaStyle.class, + Notification.DecoratedCustomViewStyle.class, + Notification.DecoratedMediaCustomViewStyle.class + ); + + for (Class<? extends Notification.Style> platformStyleClass : platformStyleClasses) { + assertThat(Notification.getNotificationStyleClass(platformStyleClass.getName())) + .isEqualTo(platformStyleClass); + } + } + + @Test + public void getNotificationStyleClass_forNotPlatformClassName_returnsNull() { + assertThat(Notification.getNotificationStyleClass(NotAPlatformStyle.class.getName())) + .isNull(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_richOngoingEnabled_platformClass() { + assertThat( + Notification.getNotificationStyleClass(Notification.ProgressStyle.class.getName())) + .isEqualTo(Notification.ProgressStyle.class); + } + + @Test + @DisableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_richOngoingDisabled_notPlatformClass() { + assertThat( + Notification.getNotificationStyleClass(Notification.ProgressStyle.class.getName())) + .isNull(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onSegmentChange_visiblyDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .addProgressSegment(new Notification.ProgressStyle.Segment(100)) + .addProgressSegment(new Notification.ProgressStyle.Segment(50) + .setColor(Color.RED))); + + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .addProgressSegment(new Notification.ProgressStyle.Segment(100)) + .addProgressSegment(new Notification.ProgressStyle.Segment(50) + .setColor(Color.BLUE))); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void indeterminateProgressStyle_onSegmentChange_visiblyNotDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true) + .addProgressSegment(new Notification.ProgressStyle.Segment(100)) + .addProgressSegment(new Notification.ProgressStyle.Segment(50) + .setColor(Color.RED))); + + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true) + .addProgressSegment(new Notification.ProgressStyle.Segment(100)) + .addProgressSegment(new Notification.ProgressStyle.Segment(50) + .setColor(Color.BLUE))); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onStartIconChange_visiblyDifferent() { + final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96)); + + final Icon icon2 = Icon.createWithBitmap( + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressStartIcon(icon1)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressStartIcon(icon2)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onEndIconChange_visiblyDifferent() { + final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96)); + + final Icon icon2 = Icon.createWithBitmap( + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressEndIcon(icon1)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressEndIcon(icon2)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onProgressChange_visiblyDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgress(20)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgress(21)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void indeterminateProgressStyle_onProgressChange_visiblyNotDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .setProgressIndeterminate(true).setProgress(20)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .setProgressIndeterminate(true).setProgress(21)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onIsStyledByProgressChange_visiblyDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setStyledByProgress(true)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setStyledByProgress(false)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void indeterminateProgressStyle_onIsStyledByProgressChange_visiblyNotDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .setProgressIndeterminate(true).setStyledByProgress(true)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .setProgressIndeterminate(true).setStyledByProgress(false)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onProgressStepChange_visiblyDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .addProgressStep(new Notification.ProgressStyle.Step(10))); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .addProgressStep(new Notification.ProgressStyle.Step(12))); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void indeterminateProgressStyle_onProgressStepChange_visiblyNotDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true) + .addProgressStep(new Notification.ProgressStyle.Step(10))); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true) + .addProgressStep(new Notification.ProgressStyle.Step(12))); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onTrackerIconChange_visiblyDifferent() { + final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96)); + + final Icon icon2 = Icon.createWithBitmap( + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressTrackerIcon(icon1)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressTrackerIcon(icon2)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void indeterminateProgressStyle_onTrackerIconChange_visiblyNotDifferent() { + final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96)); + + final Icon icon2 = Icon.createWithBitmap( + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true) + .setProgressTrackerIcon(icon1)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .setProgressIndeterminate(true).setProgressTrackerIcon(icon2)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onIndeterminateChange_visiblyDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(false)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_getProgressMax_default100() { + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + assertThat(progressStyle.getProgressMax()).isEqualTo(100); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_getProgressMax_nooSegments_returnsDefault() { + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + progressStyle.setProgressSegments(Collections.emptyList()); + assertThat(progressStyle.getProgressMax()).isEqualTo(100); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_getProgressMax_returnsSumOfSegmentLength() { + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + progressStyle + .addProgressSegment(new Notification.ProgressStyle.Segment(10)) + .addProgressSegment(new Notification.ProgressStyle.Segment(20)); + + assertThat(progressStyle.getProgressMax()).isEqualTo(30); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_getProgressMax_onSegmentOverflow_returnsDefault() { + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + progressStyle + .addProgressSegment(new Notification.ProgressStyle.Segment(Integer.MAX_VALUE)) + .addProgressSegment(new Notification.ProgressStyle.Segment(10)); + + assertThat(progressStyle.getProgressMax()).isEqualTo(100); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_indeterminate_defaultValueFalse() { + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + + assertThat(progressStyle1.isProgressIndeterminate()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_styledByProgress_defaultValueTrue() { + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + + assertThat(progressStyle1.isStyledByProgress()).isTrue(); + } private void assertValid(Notification.Colors c) { // Assert that all colors are populated assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID); @@ -2214,4 +2508,11 @@ public class NotificationTest { new Intent(action).setPackage(mContext.getPackageName()), PendingIntent.FLAG_MUTABLE); } + + private static class NotAPlatformStyle extends Notification.Style { + @Override + public boolean areNotificationsVisiblyDifferent(Notification.Style other) { + return false; + } + } } diff --git a/core/tests/coretests/src/android/view/WindowLayoutTests.java b/core/tests/coretests/src/android/view/WindowLayoutTests.java index 5cac98daee80..d4693e6e7130 100644 --- a/core/tests/coretests/src/android/view/WindowLayoutTests.java +++ b/core/tests/coretests/src/android/view/WindowLayoutTests.java @@ -413,4 +413,19 @@ public class WindowLayoutTests { assertInsetByTopBottom(0, 0, mFrames.parentFrame); assertInsetByTopBottom(0, 0, mFrames.frame); } + + @Test + public void windowBoundsOutsideDisplayCutoutSafe() { + addDisplayCutout(); + mAttrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; + mWindowBounds.set(0, -1000, DISPLAY_WIDTH, 0); + computeFrames(); + + assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0, + mFrames.displayFrame); + assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0, + mFrames.parentFrame); + assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0, + mFrames.frame); + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java index 37f0067de453..089613853555 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java @@ -16,12 +16,11 @@ package androidx.window.common; -import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; +import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN; import static androidx.window.common.layout.CommonFoldingFeature.parseListFromString; -import android.annotation.NonNull; import android.content.Context; import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateManager; @@ -31,16 +30,23 @@ import android.text.TextUtils; import android.util.Log; import android.util.SparseIntArray; +import androidx.annotation.BinderThread; +import androidx.annotation.GuardedBy; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import androidx.window.common.layout.CommonFoldingFeature; import androidx.window.common.layout.DisplayFoldFeatureCommon; import com.android.internal.R; +import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.Executor; import java.util.function.Consumer; /** @@ -55,13 +61,6 @@ public final class DeviceStateManagerFoldingFeatureProducer private static final boolean DEBUG = false; /** - * Emulated device state - * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to - * {@link CommonFoldingFeature.State} map. - */ - private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray(); - - /** * Device state received via * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)}. * The identifier returned through {@link DeviceState#getIdentifier()} may not correspond 1:1 @@ -71,23 +70,40 @@ public final class DeviceStateManagerFoldingFeatureProducer * "rear display". Concurrent mode for example is activated via public API and can be active in * both the "open" and "half folded" device states. */ - private DeviceState mCurrentDeviceState = new DeviceState( - new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER, - "INVALID").build()); + // TODO: b/337820752 - Add @GuardedBy("mCurrentDeviceStateLock") after flag cleanup. + private DeviceState mCurrentDeviceState = INVALID_DEVICE_STATE; - private List<DeviceState> mSupportedStates; + /** + * Lock to synchronize access to {@link #mCurrentDeviceState}. + * + * <p>This lock is used to ensure thread-safety when accessing and modifying the + * {@link #mCurrentDeviceState} field. It is acquired by both the binder thread (if + * {@link Flags#wlinfoOncreate()} is enabled) and the main thread (if + * {@link Flags#wlinfoOncreate()} is disabled) to prevent race conditions and + * ensure data consistency. + */ + private final Object mCurrentDeviceStateLock = new Object(); @NonNull private final RawFoldingFeatureProducer mRawFoldSupplier; - private final boolean mIsHalfOpenedSupported; - - private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() { + @NonNull + private final DeviceStateMapper mDeviceStateMapper; + + @VisibleForTesting + final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() { + // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the getData() + // implementation. See https://errorprone.info/bugpattern/GuardedBy for limitations. + @SuppressWarnings("GuardedBy") + @BinderThread // When Flags.wlinfoOncreate() is enabled. + @MainThread // When Flags.wlinfoOncreate() is disabled. @Override public void onDeviceStateChanged(@NonNull DeviceState state) { - mCurrentDeviceState = state; - mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer - .this::notifyFoldingFeatureChange); + synchronized (mCurrentDeviceStateLock) { + mCurrentDeviceState = state; + mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer.this + ::notifyFoldingFeatureChangeLocked); + } } }; @@ -95,41 +111,14 @@ public final class DeviceStateManagerFoldingFeatureProducer @NonNull RawFoldingFeatureProducer rawFoldSupplier, @NonNull DeviceStateManager deviceStateManager) { mRawFoldSupplier = rawFoldSupplier; - String[] deviceStatePosturePairs = context.getResources() - .getStringArray(R.array.config_device_state_postures); - mSupportedStates = deviceStateManager.getSupportedDeviceStates(); - boolean isHalfOpenedSupported = false; - for (String deviceStatePosturePair : deviceStatePosturePairs) { - String[] deviceStatePostureMapping = deviceStatePosturePair.split(":"); - if (deviceStatePostureMapping.length != 2) { - if (DEBUG) { - Log.e(TAG, "Malformed device state posture pair: " - + deviceStatePosturePair); - } - continue; - } + mDeviceStateMapper = + new DeviceStateMapper(context, deviceStateManager.getSupportedDeviceStates()); - int deviceState; - int posture; - try { - deviceState = Integer.parseInt(deviceStatePostureMapping[0]); - posture = Integer.parseInt(deviceStatePostureMapping[1]); - } catch (NumberFormatException e) { - if (DEBUG) { - Log.e(TAG, "Failed to parse device state or posture: " - + deviceStatePosturePair, - e); - } - continue; - } - isHalfOpenedSupported = isHalfOpenedSupported - || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED; - mDeviceStateToPostureMap.put(deviceState, posture); - } - mIsHalfOpenedSupported = isHalfOpenedSupported; - if (mDeviceStateToPostureMap.size() > 0) { + if (!mDeviceStateMapper.isDeviceStateToPostureMapEmpty()) { + final Executor executor = + Flags.wlinfoOncreate() ? Runnable::run : context.getMainExecutor(); Objects.requireNonNull(deviceStateManager) - .registerCallback(context.getMainExecutor(), mDeviceStateCallback); + .registerCallback(executor, mDeviceStateCallback); } } @@ -137,50 +126,51 @@ public final class DeviceStateManagerFoldingFeatureProducer * Add a callback to mCallbacks if there is no device state. This callback will be run * once a device state is set. Otherwise,run the callback immediately. */ - private void runCallbackWhenValidState(@NonNull Consumer<List<CommonFoldingFeature>> callback, - String displayFeaturesString) { - if (isCurrentStateValid()) { - callback.accept(calculateFoldingFeature(displayFeaturesString)); + private void runCallbackWhenValidState(@NonNull DeviceState state, + @NonNull Consumer<List<CommonFoldingFeature>> callback, + @NonNull String displayFeaturesString) { + if (mDeviceStateMapper.isDeviceStateValid(state)) { + callback.accept(calculateFoldingFeature(state, displayFeaturesString)); } else { // This callback will be added to mCallbacks and removed once it runs once. - AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback = + final AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback = new AcceptOnceConsumer<>(this, callback); addDataChangedCallback(singleRunCallback); } } - /** - * Checks to find {@link DeviceStateManagerFoldingFeatureProducer#mCurrentDeviceState} in the - * {@link DeviceStateManagerFoldingFeatureProducer#mDeviceStateToPostureMap} which was - * initialized in the constructor of {@link DeviceStateManagerFoldingFeatureProducer}. - * Returns a boolean value of whether the device state is valid. - */ - private boolean isCurrentStateValid() { - // If the device state is not found in the map, indexOfKey returns a negative number. - return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState.getIdentifier()) >= 0; - } - + // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the implementation of + // addDataChangedCallback(). See https://errorprone.info/bugpattern/GuardedBy for limitations. + @SuppressWarnings("GuardedBy") @Override protected void onListenersChanged() { super.onListenersChanged(); - if (hasListeners()) { - mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange); - } else { - mCurrentDeviceState = new DeviceState( - new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER, - "INVALID").build()); - mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange); + synchronized (mCurrentDeviceStateLock) { + if (hasListeners()) { + mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChangeLocked); + } else { + mCurrentDeviceState = INVALID_DEVICE_STATE; + mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChangeLocked); + } + } + } + + @NonNull + private DeviceState getCurrentDeviceState() { + synchronized (mCurrentDeviceStateLock) { + return mCurrentDeviceState; } } @NonNull @Override public Optional<List<CommonFoldingFeature>> getCurrentData() { - Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData(); - if (!isCurrentStateValid()) { + final Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData(); + final DeviceState state = getCurrentDeviceState(); + if (!mDeviceStateMapper.isDeviceStateValid(state) || displayFeaturesString.isEmpty()) { return Optional.empty(); } else { - return displayFeaturesString.map(this::calculateFoldingFeature); + return Optional.of(calculateFoldingFeature(state, displayFeaturesString.get())); } } @@ -191,7 +181,7 @@ public final class DeviceStateManagerFoldingFeatureProducer */ @NonNull public List<CommonFoldingFeature> getFoldsWithUnknownState() { - Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData(); + final Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData(); if (optionalFoldingFeatureString.isPresent()) { return CommonFoldingFeature.parseListFromString( @@ -201,7 +191,6 @@ public final class DeviceStateManagerFoldingFeatureProducer return Collections.emptyList(); } - /** * Returns the list of supported {@link DisplayFoldFeatureCommon} calculated from the * {@link DeviceStateManagerFoldingFeatureProducer}. @@ -218,16 +207,16 @@ public final class DeviceStateManagerFoldingFeatureProducer return foldFeatures; } - /** * Returns {@code true} if the device supports half-opened mode, {@code false} otherwise. */ public boolean isHalfOpenedSupported() { - return mIsHalfOpenedSupported; + return mDeviceStateMapper.mIsHalfOpenedSupported; } /** * Adds the data to the storeFeaturesConsumer when the data is ready. + * * @param storeFeaturesConsumer a consumer to collect the data when it is first available. */ @Override @@ -236,38 +225,123 @@ public final class DeviceStateManagerFoldingFeatureProducer if (TextUtils.isEmpty(displayFeaturesString)) { storeFeaturesConsumer.accept(new ArrayList<>()); } else { - runCallbackWhenValidState(storeFeaturesConsumer, displayFeaturesString); + final DeviceState state = getCurrentDeviceState(); + runCallbackWhenValidState(state, storeFeaturesConsumer, displayFeaturesString); } }); } - private void notifyFoldingFeatureChange(String displayFeaturesString) { - if (!isCurrentStateValid()) { + @GuardedBy("mCurrentDeviceStateLock") + private void notifyFoldingFeatureChangeLocked(String displayFeaturesString) { + final DeviceState state = mCurrentDeviceState; + if (!mDeviceStateMapper.isDeviceStateValid(state)) { return; } if (TextUtils.isEmpty(displayFeaturesString)) { notifyDataChanged(new ArrayList<>()); } else { - notifyDataChanged(calculateFoldingFeature(displayFeaturesString)); + notifyDataChanged(calculateFoldingFeature(state, displayFeaturesString)); } } - private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) { - return parseListFromString(displayFeaturesString, currentHingeState()); + @NonNull + private List<CommonFoldingFeature> calculateFoldingFeature(@NonNull DeviceState deviceState, + @NonNull String displayFeaturesString) { + @CommonFoldingFeature.State + final int hingeState = mDeviceStateMapper.getHingeState(deviceState); + return parseListFromString(displayFeaturesString, hingeState); } - @CommonFoldingFeature.State - private int currentHingeState() { - @CommonFoldingFeature.State - int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState.getIdentifier(), - COMMON_STATE_UNKNOWN); + /** + * Internal class to map device states to corresponding postures. + * + * <p>This class encapsulates the logic for mapping device states to postures. The mapping is + * immutable after initialization to ensure thread safety. + */ + private static class DeviceStateMapper { + /** + * Emulated device state + * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to + * {@link CommonFoldingFeature.State} map. + * + * <p>This map must be immutable after initialization to ensure thread safety, as it may be + * accessed from multiple threads. Modifications should only occur during object + * construction. + */ + private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray(); + + /** + * The list of device states that are supported. + * + * <p>This list must be immutable after initialization to ensure thread safety. + */ + @NonNull + private final List<DeviceState> mSupportedStates; + + final boolean mIsHalfOpenedSupported; + + DeviceStateMapper(@NonNull Context context, @NonNull List<DeviceState> supportedStates) { + mSupportedStates = supportedStates; + + final String[] deviceStatePosturePairs = context.getResources() + .getStringArray(R.array.config_device_state_postures); + boolean isHalfOpenedSupported = false; + for (String deviceStatePosturePair : deviceStatePosturePairs) { + final String[] deviceStatePostureMapping = deviceStatePosturePair.split(":"); + if (deviceStatePostureMapping.length != 2) { + if (DEBUG) { + Log.e(TAG, "Malformed device state posture pair: " + + deviceStatePosturePair); + } + continue; + } - if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) { - posture = mDeviceStateToPostureMap.get( - DeviceStateUtil.calculateBaseStateIdentifier(mCurrentDeviceState, - mSupportedStates), COMMON_STATE_UNKNOWN); + final int deviceState; + final int posture; + try { + deviceState = Integer.parseInt(deviceStatePostureMapping[0]); + posture = Integer.parseInt(deviceStatePostureMapping[1]); + } catch (NumberFormatException e) { + if (DEBUG) { + Log.e(TAG, "Failed to parse device state or posture: " + + deviceStatePosturePair, + e); + } + continue; + } + isHalfOpenedSupported = isHalfOpenedSupported + || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED; + mDeviceStateToPostureMap.put(deviceState, posture); + } + mIsHalfOpenedSupported = isHalfOpenedSupported; + } + + boolean isDeviceStateToPostureMapEmpty() { + return mDeviceStateToPostureMap.size() == 0; + } + + /** + * Validates if the provided deviceState exists in the {@link #mDeviceStateToPostureMap} + * which was initialized in the constructor of {@link DeviceStateMapper}. + * Returns a boolean value of whether the device state is valid. + */ + boolean isDeviceStateValid(@NonNull DeviceState deviceState) { + // If the device state is not found in the map, indexOfKey returns a negative number. + return mDeviceStateToPostureMap.indexOfKey(deviceState.getIdentifier()) >= 0; } - return posture; + @CommonFoldingFeature.State + int getHingeState(@NonNull DeviceState deviceState) { + @CommonFoldingFeature.State + final int posture = + mDeviceStateToPostureMap.get(deviceState.getIdentifier(), COMMON_STATE_UNKNOWN); + if (posture != CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) { + return posture; + } + + final int baseStateIdentifier = + DeviceStateUtil.calculateBaseStateIdentifier(deviceState, mSupportedStates); + return mDeviceStateToPostureMap.get(baseStateIdentifier, COMMON_STATE_UNKNOWN); + } } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 74cce68f270b..dcc2d93060c9 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -377,8 +377,16 @@ class TaskContainer { @Nullable TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { - return getContainer(container -> container.hasAppearedActivity(activityToken) - || container.hasPendingAppearedActivity(activityToken)); + // When the new activity is launched to the topmost TF because the source activity + // was in that TF, and the source activity is finished before resolving the new activity, + // we will try to see if the new activity match a rule with the split activities below. + // If matched, it can be reparented. + final TaskFragmentContainer taskFragmentContainer + = getContainer(container -> container.hasPendingAppearedActivity(activityToken)); + if (taskFragmentContainer != null) { + return taskFragmentContainer; + } + return getContainer(container -> container.hasAppearedActivity(activityToken)); } @Nullable diff --git a/libs/WindowManager/Jetpack/tests/unittest/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp index bd430c0e610b..09185ee203b8 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/Android.bp +++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp @@ -29,6 +29,7 @@ android_test { srcs: [ "**/*.java", + "**/*.kt", ], static_libs: [ @@ -41,6 +42,7 @@ android_test { "androidx.test.ext.junit", "flag-junit", "mockito-target-extended-minus-junit4", + "mockito-kotlin-nodeps", "truth", "testables", "platform-test-annotations", diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt new file mode 100644 index 000000000000..90887a747a6f --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt @@ -0,0 +1,341 @@ +/* + * 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 androidx.window.common + +import android.content.Context +import android.content.res.Resources +import android.hardware.devicestate.DeviceState +import android.hardware.devicestate.DeviceStateManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.window.common.layout.CommonFoldingFeature +import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_FLAT +import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_HALF_OPENED +import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_NO_FOLDING_FEATURES +import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN +import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE +import androidx.window.common.layout.DisplayFoldFeatureCommon +import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED +import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN +import com.android.internal.R +import com.android.window.flags.Flags +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import java.util.concurrent.Executor +import java.util.function.Consumer +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify + +/** + * Test class for [DeviceStateManagerFoldingFeatureProducer]. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:DeviceStateManagerFoldingFeatureProducerTest + */ +@RunWith(AndroidJUnit4::class) +class DeviceStateManagerFoldingFeatureProducerTest { + @get:Rule + val setFlagsRule: SetFlagsRule = SetFlagsRule() + + private val mMockDeviceStateManager = mock<DeviceStateManager>() + private val mMockResources = mock<Resources> { + on { getStringArray(R.array.config_device_state_postures) } doReturn DEVICE_STATE_POSTURES + } + private val mMockContext = mock<Context> { + on { resources } doReturn mMockResources + } + private val mRawFoldSupplier = mock<RawFoldingFeatureProducer> { + on { currentData } doReturn Optional.of(DISPLAY_FEATURES) + on { getData(any<Consumer<String>>()) } doAnswer { invocation -> + val callback = invocation.getArgument(0) as Consumer<String> + callback.accept(DISPLAY_FEATURES) + } + } + + @Test + @DisableFlags(Flags.FLAG_WLINFO_ONCREATE) + fun testRegisterCallback_whenWlinfoOncreateIsDisabled_usesMainExecutor() { + DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + verify(mMockDeviceStateManager).registerCallback(eq(mMockContext.mainExecutor), any()) + } + + @Test + @EnableFlags(Flags.FLAG_WLINFO_ONCREATE) + fun testRegisterCallback_whenWlinfoOncreateIsEnabled_usesRunnableRun() { + val executorCaptor = ArgumentCaptor.forClass(Executor::class.java) + val runnable = mock<Runnable>() + + DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + verify(mMockDeviceStateManager).registerCallback(executorCaptor.capture(), any()) + executorCaptor.value.execute(runnable) + verify(runnable).run() + } + + @Test + fun testGetCurrentData_validCurrentState_returnsFoldingFeatureWithState() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED) + + val currentData = ffp.getCurrentData() + + assertThat(currentData).isPresent() + assertThat(currentData.get()).containsExactlyElementsIn(HALF_OPENED_FOLDING_FEATURES) + } + + @Test + fun testGetCurrentData_invalidCurrentState_returnsEmptyOptionalFoldingFeature() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + val currentData = ffp.getCurrentData() + + assertThat(currentData).isEmpty() + } + + @Test + fun testGetFoldsWithUnknownState_validFoldingFeature_returnsFoldingFeaturesWithUnknownState() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + val result = ffp.getFoldsWithUnknownState() + + assertThat(result).containsExactlyElementsIn(UNKNOWN_STATE_FOLDING_FEATURES) + } + + @Test + fun testGetFoldsWithUnknownState_emptyFoldingFeature_returnsEmptyList() { + mRawFoldSupplier.stub { + on { currentData } doReturn Optional.empty() + } + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + val result = ffp.getFoldsWithUnknownState() + + assertThat(result).isEmpty() + } + + @Test + fun testGetDisplayFeatures_validFoldingFeature_returnsDisplayFoldFeatures() { + mRawFoldSupplier.stub { + on { currentData } doReturn Optional.of(DISPLAY_FEATURES_HALF_OPENED_HINGE) + } + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + val result = ffp.displayFeatures + + assertThat(result).containsExactly( + DisplayFoldFeatureCommon( + DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN, + setOf(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED), + ), + ) + } + + @Test + fun testIsHalfOpenedSupported_withHalfOpenedPostures_returnsTrue() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + assertThat(ffp.isHalfOpenedSupported).isTrue() + } + + @Test + fun testIsHalfOpenedSupported_withEmptyPostures_returnsFalse() { + mMockResources.stub { + on { getStringArray(R.array.config_device_state_postures) } doReturn emptyArray() + } + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + assertThat(ffp.isHalfOpenedSupported).isFalse() + } + + @Test + fun testGetData_emptyDisplayFeaturesString_callsConsumerWithEmptyList() { + mRawFoldSupplier.stub { + on { getData(any<Consumer<String>>()) } doAnswer { invocation -> + val callback = invocation.getArgument(0) as Consumer<String> + callback.accept("") + } + } + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>() + + ffp.getData(storeFeaturesConsumer) + + verify(storeFeaturesConsumer).accept(emptyList()) + } + + @Test + fun testGetData_validState_callsConsumerWithFoldingFeatures() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED) + val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>() + + ffp.getData(storeFeaturesConsumer) + + verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES) + } + + @Test + fun testGetData_invalidState_addsAcceptOnceConsumerToDataChangedCallback() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>() + + ffp.getData(storeFeaturesConsumer) + + verify(storeFeaturesConsumer, never()).accept(any()) + ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED) + ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_OPENED) + verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES) + } + + @Test + fun testDeviceStateMapper_malformedDeviceStatePosturePair_skipsPair() { + val malformedDeviceStatePostures = arrayOf( + // Missing the posture. + "0", + // Empty string. + "", + // Too many elements. + "0:1:2", + ) + mMockResources.stub { + on { getStringArray(R.array.config_device_state_postures) } doReturn + malformedDeviceStatePostures + } + + DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + verify(mMockDeviceStateManager, never()).registerCallback(any(), any()) + } + + @Test + fun testDeviceStateMapper_invalidNumberFormat_skipsPair() { + val invalidNumberFormatDeviceStatePostures = arrayOf("a:1", "0:b", "a:b", ":1") + mMockResources.stub { + on { getStringArray(R.array.config_device_state_postures) } doReturn + invalidNumberFormatDeviceStatePostures + } + + DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + verify(mMockDeviceStateManager, never()).registerCallback(any(), any()) + } + + companion object { + // Supported device states configuration. + private enum class SupportedDeviceStates { + CLOSED, HALF_OPENED, OPENED, REAR_DISPLAY, CONCURRENT; + + override fun toString() = ordinal.toString() + + fun toDeviceState(): DeviceState = + DeviceState(DeviceState.Configuration.Builder(ordinal, name).build()) + } + + // Map of supported device states supplied by DeviceStateManager to WM Jetpack posture. + private val DEVICE_STATE_POSTURES = + arrayOf( + "${SupportedDeviceStates.CLOSED}:$COMMON_STATE_NO_FOLDING_FEATURES", + "${SupportedDeviceStates.HALF_OPENED}:$COMMON_STATE_HALF_OPENED", + "${SupportedDeviceStates.OPENED}:$COMMON_STATE_FLAT", + "${SupportedDeviceStates.REAR_DISPLAY}:$COMMON_STATE_NO_FOLDING_FEATURES", + "${SupportedDeviceStates.CONCURRENT}:$COMMON_STATE_USE_BASE_STATE", + ) + private val DEVICE_STATE_HALF_OPENED = SupportedDeviceStates.HALF_OPENED.toDeviceState() + private val DEVICE_STATE_OPENED = SupportedDeviceStates.OPENED.toDeviceState() + + // WindowsManager Jetpack display features. + private val DISPLAY_FEATURES = "fold-[1104,0,1104,1848]" + private val DISPLAY_FEATURES_HALF_OPENED_HINGE = "$DISPLAY_FEATURES-half-opened" + private val HALF_OPENED_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString( + DISPLAY_FEATURES, + COMMON_STATE_HALF_OPENED, + ) + private val UNKNOWN_STATE_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString( + DISPLAY_FEATURES, + COMMON_STATE_UNKNOWN, + ) + } +} diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index 7fab371cb790..bc4916a607a3 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -535,7 +535,8 @@ public class TaskFragmentContainerTest { // container1. container2.setInfo(mTransaction, mInfo); - assertTrue(container2.hasActivity(mActivity.getActivityToken())); + assertTrue(container1.hasActivity(mActivity.getActivityToken())); + assertFalse(container2.hasActivity(mActivity.getActivityToken())); // When the pending appeared record is removed from container1, we respect the appeared // record in container2. container1.removePendingAppearedActivity(mActivity.getActivityToken()); diff --git a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml index 045b975a854e..462a49ccb1eb 100644 --- a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml +++ b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml @@ -99,11 +99,11 @@ </LinearLayout> - <FrameLayout + + <LinearLayout android:minHeight="@dimen/letterbox_restart_dialog_button_height" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" - style="?android:attr/buttonBarButtonStyle" android:layout_gravity="end"> <Button @@ -133,7 +133,7 @@ android:text="@string/letterbox_restart_restart" android:contentDescription="@string/letterbox_restart_restart"/> - </FrameLayout> + </LinearLayout> </LinearLayout> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java new file mode 100644 index 000000000000..26aae2d2aa78 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared; + +import com.android.wm.shell.shared.annotations.ExternalThread; + +/** + * Listener to get focus-related transition callbacks. + */ +@ExternalThread +public interface FocusTransitionListener { + /** + * Called when a transition changes the top, focused display. + */ + void onFocusedDisplayChanged(int displayId); +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl new file mode 100644 index 000000000000..b91d5b6e2769 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared; + +/** + * Listener interface that to get focus-related transition callbacks. + */ +oneway interface IFocusTransitionListener { + + /** + * Called when a transition changes the top, focused display. + */ + void onFocusedDisplayChanged(int displayId); +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl index 3256abf09116..02615a96a86c 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl @@ -20,6 +20,7 @@ import android.view.SurfaceControl; import android.window.RemoteTransition; import android.window.TransitionFilter; +import com.android.wm.shell.shared.IFocusTransitionListener; import com.android.wm.shell.shared.IHomeTransitionListener; /** @@ -59,4 +60,9 @@ interface IShellTransitions { */ oneway void registerRemoteForTakeover(in TransitionFilter filter, in RemoteTransition remoteTransition) = 6; + + /** + * Set listener that will receive callbacks about transitions involving focus switch. + */ + oneway void setFocusTransitionListener(in IFocusTransitionListener listener) = 7; } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java index 6d4ab4c1bd09..2db4311fb771 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java @@ -22,6 +22,8 @@ import android.window.TransitionFilter; import com.android.wm.shell.shared.annotations.ExternalThread; +import java.util.concurrent.Executor; + /** * Interface to manage remote transitions. */ @@ -44,4 +46,15 @@ public interface ShellTransitions { * Unregisters a remote transition for all operations. */ default void unregisterRemote(@NonNull RemoteTransition remoteTransition) {} + + /** + * Sets listener that will receive callbacks about transitions involving focus switch. + */ + default void setFocusTransitionListener(@NonNull FocusTransitionListener listener, + Executor executor) {} + + /** + * Unsets listener that will receive callbacks about transitions involving focus switch. + */ + default void unsetFocusTransitionListener(@NonNull FocusTransitionListener listener) {} } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index bec2ea58e106..4227a6e2903f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -123,6 +123,7 @@ import com.android.wm.shell.sysui.ShellInterface; import com.android.wm.shell.taskview.TaskViewFactory; import com.android.wm.shell.taskview.TaskViewFactoryController; import com.android.wm.shell.taskview.TaskViewTransitions; +import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.HomeTransitionObserver; import com.android.wm.shell.transition.MixedTransitionHandler; import com.android.wm.shell.transition.Transitions; @@ -742,14 +743,15 @@ public abstract class WMShellBaseModule { @ShellMainThread Handler mainHandler, @ShellAnimationThread ShellExecutor animExecutor, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - HomeTransitionObserver homeTransitionObserver) { + HomeTransitionObserver homeTransitionObserver, + FocusTransitionObserver focusTransitionObserver) { if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) { // TODO(b/238217847): Force override shell init if registration is disabled shellInit = new ShellInit(mainExecutor); } return new Transitions(context, shellInit, shellCommandHandler, shellController, organizer, pool, displayController, mainExecutor, mainHandler, animExecutor, - rootTaskDisplayAreaOrganizer, homeTransitionObserver); + rootTaskDisplayAreaOrganizer, homeTransitionObserver, focusTransitionObserver); } @WMSingleton @@ -761,6 +763,12 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides + static FocusTransitionObserver provideFocusTransitionObserver() { + return new FocusTransitionObserver(); + } + + @WMSingleton + @Provides static TaskViewTransitions provideTaskViewTransitions(Transitions transitions) { return new TaskViewTransitions(transitions); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 968f40c3df5d..afa27f9f1309 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -43,6 +43,7 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.DragEvent import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE +import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_NONE import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_FRONT @@ -1061,7 +1062,10 @@ class DesktopTasksController( // Check if freeform task launch during recents should be handled shouldHandleMidRecentsFreeformLaunch -> handleMidRecentsFreeformTaskLaunch(task) // Check if the closing task needs to be handled - TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task) + TransitionUtil.isClosingType(request.type) -> handleTaskClosing( + task, + request.type + ) // Check if the top task shouldn't be allowed to enter desktop mode isIncompatibleTask(task) -> handleIncompatibleTaskLaunch(task) // Check if fullscreen task should be updated @@ -1288,7 +1292,10 @@ class DesktopTasksController( } /** Handle task closing by removing wallpaper activity if it's the last active task */ - private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? { + private fun handleTaskClosing( + task: RunningTaskInfo, + transitionType: Int + ): WindowContainerTransaction? { logV("handleTaskClosing") if (!isDesktopModeShowing(task.displayId)) return null @@ -1301,9 +1308,10 @@ class DesktopTasksController( removeWallpaperActivity(wct) } taskRepository.addClosingTask(task.displayId, task.taskId) - // If a CLOSE or TO_BACK is triggered on a desktop task, remove the task. + // If a CLOSE is triggered on a desktop task, remove the task. if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue() && - taskRepository.isVisibleTask(task.taskId) + taskRepository.isVisibleTask(task.taskId) && + transitionType == TRANSIT_CLOSE ) { wct.removeTask(task.token) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index 0841628853a3..4796c4d0655a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -16,16 +16,19 @@ package com.android.wm.shell.desktopmode +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.Context import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager +import android.view.WindowManager.TRANSIT_TO_BACK import android.window.TransitionInfo import android.window.WindowContainerTransaction +import android.window.flags.DesktopModeFlags +import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY import com.android.internal.protolog.ProtoLog import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE -import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions @@ -64,6 +67,30 @@ class DesktopTasksTransitionObserver( ) { // TODO: b/332682201 Update repository state updateWallpaperToken(info) + + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { + handleBackNavigation(info) + } + } + + private fun handleBackNavigation(info: TransitionInfo) { + // When default back navigation happens, transition type is TO_BACK and the change is + // TO_BACK. Mark the task going to back as minimized. + if (info.type == TRANSIT_TO_BACK) { + for (change in info.changes) { + val taskInfo = change.taskInfo + if (taskInfo == null || taskInfo.taskId == -1) { + continue + } + + if (desktopModeTaskRepository.getVisibleTaskCount(taskInfo.displayId) > 0 && + change.mode == TRANSIT_TO_BACK && + taskInfo.windowingMode == WINDOWING_MODE_FREEFORM + ) { + desktopModeTaskRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId) + } + } + } } override fun onTransitionStarting(transition: IBinder) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java new file mode 100644 index 000000000000..2f5059f3161c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java @@ -0,0 +1,142 @@ +/* + * 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.wm.shell.transition; + +import static android.view.Display.INVALID_DISPLAY; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; + +import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions; +import static com.android.wm.shell.transition.Transitions.TransitionObserver; + +import android.annotation.NonNull; +import android.app.ActivityManager.RunningTaskInfo; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; +import android.view.SurfaceControl; +import android.window.TransitionInfo; + +import com.android.wm.shell.shared.FocusTransitionListener; +import com.android.wm.shell.shared.IFocusTransitionListener; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * The {@link TransitionObserver} that observes for transitions involving focus switch. + * It reports transitions to callers outside of the process via {@link IFocusTransitionListener}, + * and callers within the process via {@link FocusTransitionListener}. + */ +public class FocusTransitionObserver implements TransitionObserver { + private static final String TAG = FocusTransitionObserver.class.getSimpleName(); + + private IFocusTransitionListener mRemoteListener; + private final Map<FocusTransitionListener, Executor> mLocalListeners = + new HashMap<>(); + + private int mFocusedDisplayId = INVALID_DISPLAY; + + public FocusTransitionObserver() {} + + @Override + public void onTransitionReady(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + final List<TransitionInfo.Change> changes = info.getChanges(); + for (int i = changes.size() - 1; i >= 0; i--) { + final TransitionInfo.Change change = changes.get(i); + final RunningTaskInfo task = change.getTaskInfo(); + if (task != null && task.isFocused && change.hasFlags(FLAG_MOVED_TO_TOP)) { + if (mFocusedDisplayId != task.displayId) { + mFocusedDisplayId = task.displayId; + notifyFocusedDisplayChanged(); + } + return; + } + } + } + + @Override + public void onTransitionStarting(@NonNull IBinder transition) {} + + @Override + public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {} + + @Override + public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {} + + /** + * Sets the focus transition listener that receives any transitions resulting in focus switch. + * This is for calls from outside the Shell, within the host process. + * + */ + public void setLocalFocusTransitionListener(FocusTransitionListener listener, + Executor executor) { + if (!enableDisplayFocusInShellTransitions()) { + return; + } + mLocalListeners.put(listener, executor); + executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId)); + } + + /** + * Sets the focus transition listener that receives any transitions resulting in focus switch. + * This is for calls from outside the Shell, within the host process. + * + */ + public void unsetLocalFocusTransitionListener(FocusTransitionListener listener) { + if (!enableDisplayFocusInShellTransitions()) { + return; + } + mLocalListeners.remove(listener); + } + + /** + * Sets the focus transition listener that receives any transitions resulting in focus switch. + * This is for calls from outside the host process. + */ + public void setRemoteFocusTransitionListener(Transitions transitions, + IFocusTransitionListener listener) { + if (!enableDisplayFocusInShellTransitions()) { + return; + } + mRemoteListener = listener; + notifyFocusedDisplayChangedToRemote(); + } + + /** + * Notifies the listener that display focus has changed. + */ + public void notifyFocusedDisplayChanged() { + notifyFocusedDisplayChangedToRemote(); + mLocalListeners.forEach((listener, executor) -> + executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId))); + } + + private void notifyFocusedDisplayChangedToRemote() { + if (mRemoteListener != null) { + try { + mRemoteListener.onFocusedDisplayChanged(mFocusedDisplayId); + } catch (RemoteException e) { + Slog.w(TAG, "Failed call notifyFocusedDisplayChangedToRemote", e); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index d03832d3e85e..d280dcd252b4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -87,6 +87,8 @@ import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.FocusTransitionListener; +import com.android.wm.shell.shared.IFocusTransitionListener; import com.android.wm.shell.shared.IHomeTransitionListener; import com.android.wm.shell.shared.IShellTransitions; import com.android.wm.shell.shared.ShellTransitions; @@ -103,6 +105,7 @@ import com.android.wm.shell.transition.tracing.TransitionTracer; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.concurrent.Executor; /** * Plays transition animations. Within this player, each transition has a lifecycle. @@ -224,6 +227,7 @@ public class Transitions implements RemoteCallable<Transitions>, private final ArrayList<TransitionObserver> mObservers = new ArrayList<>(); private HomeTransitionObserver mHomeTransitionObserver; + private FocusTransitionObserver mFocusTransitionObserver; /** List of {@link Runnable} instances to run when the last active transition has finished. */ private final ArrayList<Runnable> mRunWhenIdleQueue = new ArrayList<>(); @@ -309,10 +313,12 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, - @NonNull HomeTransitionObserver observer) { + @NonNull HomeTransitionObserver homeTransitionObserver, + @NonNull FocusTransitionObserver focusTransitionObserver) { this(context, shellInit, new ShellCommandHandler(), shellController, organizer, pool, displayController, mainExecutor, mainHandler, animExecutor, - new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), observer); + new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), + homeTransitionObserver, focusTransitionObserver); } public Transitions(@NonNull Context context, @@ -326,7 +332,8 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer, - @NonNull HomeTransitionObserver observer) { + @NonNull HomeTransitionObserver homeTransitionObserver, + @NonNull FocusTransitionObserver focusTransitionObserver) { mOrganizer = organizer; mContext = context; mMainExecutor = mainExecutor; @@ -345,7 +352,8 @@ public class Transitions implements RemoteCallable<Transitions>, mHandlers.add(mRemoteTransitionHandler); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote"); shellInit.addInitCallback(this::onInit, this); - mHomeTransitionObserver = observer; + mHomeTransitionObserver = homeTransitionObserver; + mFocusTransitionObserver = focusTransitionObserver; if (android.tracing.Flags.perfettoTransitionTracing()) { mTransitionTracer = new PerfettoTransitionTracer(); @@ -384,6 +392,8 @@ public class Transitions implements RemoteCallable<Transitions>, mShellCommandHandler.addCommandCallback("transitions", this, this); mShellCommandHandler.addDumpCallback(this::dump, this); + + registerObserver(mFocusTransitionObserver); } public boolean isRegistered() { @@ -1573,6 +1583,21 @@ public class Transitions implements RemoteCallable<Transitions>, mMainExecutor.execute( () -> mRemoteTransitionHandler.removeFiltered(remoteTransition)); } + + @Override + public void setFocusTransitionListener(FocusTransitionListener listener, + Executor executor) { + mMainExecutor.execute(() -> + mFocusTransitionObserver.setLocalFocusTransitionListener(listener, executor)); + + } + + @Override + public void unsetFocusTransitionListener(FocusTransitionListener listener) { + mMainExecutor.execute(() -> + mFocusTransitionObserver.unsetLocalFocusTransitionListener(listener)); + + } } /** @@ -1634,6 +1659,15 @@ public class Transitions implements RemoteCallable<Transitions>, } @Override + public void setFocusTransitionListener(IFocusTransitionListener listener) { + executeRemoteCallWithTaskPermission(mTransitions, "setFocusTransitionListener", + (transitions) -> { + transitions.mFocusTransitionObserver.setRemoteFocusTransitionListener( + transitions, listener); + }); + } + + @Override public SurfaceControl getHomeTaskOverlayContainer() { SurfaceControl[] result = new SurfaceControl[1]; executeRemoteCallWithTaskPermission(mTransitions, "getHomeTaskOverlayContainer", diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index ee545209904f..29aea006c076 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -2086,16 +2086,13 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) + fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_removesTask() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - assertNotNull(result, "Should handle request").assertRemoveAt(0, task.token) + assertNull(result, "Should not handle request") } @Test @@ -2137,26 +2134,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_backTransition_singleTask_withWallpaper_withBackNav_removesWallpaperAndTask() { - val task = setUpFreeformTask() - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - result.assertRemoveAt(index = 1, task.token) - } - - @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_backTransition_singleTaskWithToken_noBackNav_removesWallpaper() { + fun handleRequest_backTransition_singleTaskWithToken_removesWallpaper() { val task = setUpFreeformTask() val wallpaperToken = MockToken().token() @@ -2183,23 +2162,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_backTransition_multipleTasks_withWallpaper_withBackNav_removesTask() { - val task1 = setUpFreeformTask() - setUpFreeformTask() - - taskRepository.wallpaperActivityToken = MockToken().token() - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task1.token) - } - - @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun handleRequest_backTransition_multipleTasks_noBackNav_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -2226,29 +2189,11 @@ class DesktopTasksControllerTest : ShellTestCase() { // Should create remove wallpaper transaction assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - result.assertRemoveAt(index = 1, task1.token) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_backTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() { - val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) } @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) fun handleRequest_backTransition_multipleTasksSingleNonMinimized_removesWallpaperAndTask() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -2261,23 +2206,6 @@ class DesktopTasksControllerTest : ShellTestCase() { // Should create remove wallpaper transaction assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - result.assertRemoveAt(index = 1, task1.token) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_backTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() { - val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt new file mode 100644 index 000000000000..c989d1640f80 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -0,0 +1,138 @@ +/* + * 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.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.platform.test.annotations.EnableFlags +import android.view.Display.DEFAULT_DISPLAY +import android.view.WindowManager.TRANSIT_TO_BACK +import android.window.IWindowContainerToken +import android.window.TransitionInfo +import android.window.TransitionInfo.Change +import android.window.WindowContainerToken +import com.android.modules.utils.testing.ExtendedMockitoRule +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.Transitions +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class DesktopTasksTransitionObserverTest { + + @JvmField + @Rule + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this) + .mockStatic(DesktopModeStatus::class.java) + .build()!! + + private val testExecutor = mock<ShellExecutor>() + private val mockShellInit = mock<ShellInit>() + private val transitions = mock<Transitions>() + private val context = mock<Context>() + private val shellTaskOrganizer = mock<ShellTaskOrganizer>() + private val taskRepository = mock<DesktopModeTaskRepository>() + + private lateinit var transitionObserver: DesktopTasksTransitionObserver + private lateinit var shellInit: ShellInit + + @Before + fun setup() { + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + shellInit = spy(ShellInit(testExecutor)) + + transitionObserver = + DesktopTasksTransitionObserver( + context, taskRepository, transitions, shellTaskOrganizer, shellInit + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun backNavigation_taskMinimized() { + val task = createTaskInfo(1) + whenever(taskRepository.getVisibleTaskCount(any())).thenReturn(1) + + transitionObserver.onTransitionReady( + transition = mock(), + info = + createBackNavigationTransition(task), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(taskRepository).minimizeTask(task.displayId, task.taskId) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun backNavigation_nullTaskInfo_taskNotMinimized() { + val task = createTaskInfo(1) + whenever(taskRepository.getVisibleTaskCount(any())).thenReturn(1) + + transitionObserver.onTransitionReady( + transition = mock(), + info = + createBackNavigationTransition(null), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(taskRepository, never()).minimizeTask(task.displayId, task.taskId) + } + + private fun createBackNavigationTransition( + task: RunningTaskInfo? + ): TransitionInfo { + return TransitionInfo(TRANSIT_TO_BACK, 0 /* flags */).apply { + addChange( + Change(mock(), mock()).apply { + mode = TRANSIT_TO_BACK + parent = null + taskInfo = task + flags = flags + } + ) + } + } + + private fun createTaskInfo(id: Int) = + RunningTaskInfo().apply { + taskId = id + displayId = DEFAULT_DISPLAY + configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + token = WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)) + baseIntent = Intent().apply { + component = ComponentName("package", "component.name") + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index a6c16c43c8cb..67eda8bfecd1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -74,6 +74,7 @@ import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.DefaultMixedHandler; +import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.HomeTransitionObserver; import com.android.wm.shell.transition.Transitions; @@ -429,7 +430,8 @@ public class StageCoordinatorTests extends ShellTestCase { ShellInit shellInit = new ShellInit(mMainExecutor); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mTaskOrganizer, mTransactionPool, mock(DisplayController.class), mMainExecutor, - mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mock(FocusTransitionObserver.class)); shellInit.init(); return t; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java new file mode 100644 index 000000000000..d37b4cf4b4b3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java @@ -0,0 +1,155 @@ +/* + * 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.wm.shell.transition; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager.RunningTaskInfo; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.TransitionInfo.TransitionMode; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.window.flags.Flags; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.shared.IFocusTransitionListener; +import com.android.wm.shell.shared.TransactionPool; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests for the focus transition observer. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +@RequiresFlagsEnabled(Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS) +public class FocusTransitionObserverTest extends ShellTestCase { + + static final int SECONDARY_DISPLAY_ID = 1; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private IFocusTransitionListener mListener; + private Transitions mTransition; + private FocusTransitionObserver mFocusTransitionObserver; + + @Before + public void setUp() { + mListener = mock(IFocusTransitionListener.class); + when(mListener.asBinder()).thenReturn(mock(IBinder.class)); + + mFocusTransitionObserver = new FocusTransitionObserver(); + mTransition = + new Transitions(InstrumentationRegistry.getInstrumentation().getTargetContext(), + mock(ShellInit.class), mock(ShellController.class), + mock(ShellTaskOrganizer.class), mock(TransactionPool.class), + mock(DisplayController.class), new TestShellExecutor(), + new Handler(Looper.getMainLooper()), new TestShellExecutor(), + mock(HomeTransitionObserver.class), + mFocusTransitionObserver); + mFocusTransitionObserver.setRemoteFocusTransitionListener(mTransition, mListener); + } + + @Test + public void testTransitionWithMovedToFrontFlagChangesDisplayFocus() throws RemoteException { + final IBinder binder = mock(IBinder.class); + final SurfaceControl.Transaction tx = mock(SurfaceControl.Transaction.class); + + // Open a task on the default display, which doesn't change display focus because the + // default display already has it. + TransitionInfo info = mock(TransitionInfo.class); + final List<TransitionInfo.Change> changes = new ArrayList<>(); + setupChange(changes, 123 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY, + true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx); + verify(mListener, never()).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID); + clearInvocations(mListener); + + // Open a new task on the secondary display and verify display focus changes to the display. + changes.clear(); + setupChange(changes, 456 /* taskId */, TRANSIT_OPEN, SECONDARY_DISPLAY_ID, + true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx); + verify(mListener, times(1)).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID); + clearInvocations(mListener); + + // Open the first task to front and verify display focus goes back to the default display. + changes.clear(); + setupChange(changes, 123 /* taskId */, TRANSIT_TO_FRONT, DEFAULT_DISPLAY, + true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx); + verify(mListener, times(1)).onFocusedDisplayChanged(DEFAULT_DISPLAY); + clearInvocations(mListener); + + // Open another task on the default display and verify no display focus switch as it's + // already on the default display. + changes.clear(); + setupChange(changes, 789 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY, + true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx); + verify(mListener, never()).onFocusedDisplayChanged(DEFAULT_DISPLAY); + } + + private void setupChange(List<TransitionInfo.Change> changes, int taskId, + @TransitionMode int mode, int displayId, boolean focused) { + TransitionInfo.Change change = mock(TransitionInfo.Change.class); + RunningTaskInfo taskInfo = mock(RunningTaskInfo.class); + taskInfo.taskId = taskId; + taskInfo.isFocused = focused; + when(change.hasFlags(FLAG_MOVED_TO_TOP)).thenReturn(focused); + taskInfo.displayId = displayId; + when(change.getTaskInfo()).thenReturn(taskInfo); + when(change.getMode()).thenReturn(mode); + changes.add(change); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java index 8f49de0a98fb..8dfdfb4dcbcf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java @@ -100,7 +100,8 @@ public class HomeTransitionObserverTest extends ShellTestCase { mHomeTransitionObserver = new HomeTransitionObserver(mContext, mMainExecutor); mTransition = new Transitions(mContext, mock(ShellInit.class), mock(ShellController.class), mOrganizer, mTransactionPool, mDisplayController, mMainExecutor, - mMainHandler, mAnimExecutor, mHomeTransitionObserver); + mMainHandler, mAnimExecutor, mHomeTransitionObserver, + mock(FocusTransitionObserver.class)); mHomeTransitionObserver.setHomeTransitionListener(mTransition, mListener); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index aea14b900647..6cde0569796d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -158,7 +158,8 @@ public class ShellTransitionTests extends ShellTestCase { ShellInit shellInit = mock(ShellInit.class); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mock(FocusTransitionObserver.class)); // One from Transitions, one from RootTaskDisplayAreaOrganizer verify(shellInit).addInitCallback(any(), eq(t)); verify(shellInit).addInitCallback(any(), isA(RootTaskDisplayAreaOrganizer.class)); @@ -170,7 +171,8 @@ public class ShellTransitionTests extends ShellTestCase { ShellController shellController = mock(ShellController.class); final Transitions t = new Transitions(mContext, shellInit, shellController, mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mock(FocusTransitionObserver.class)); shellInit.init(); verify(shellController, times(1)).addExternalInterface( eq(ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS), any(), any()); @@ -1238,7 +1240,8 @@ public class ShellTransitionTests extends ShellTestCase { final Transitions transitions = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mock(FocusTransitionObserver.class)); final RecentsTransitionHandler recentsHandler = new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions, mock(RecentTasksController.class), mock(HomeTransitionObserver.class)); @@ -1780,7 +1783,8 @@ public class ShellTransitionTests extends ShellTestCase { ShellInit shellInit = new ShellInit(mMainExecutor); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mock(FocusTransitionObserver.class)); shellInit.init(); return t; } diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt index 504e3290b0ae..3ed33db2222e 100644 --- a/libs/appfunctions/api/current.txt +++ b/libs/appfunctions/api/current.txt @@ -3,13 +3,15 @@ package com.google.android.appfunctions.sidecar { public final class AppFunctionManager { ctor public AppFunctionManager(android.content.Context); - method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @Deprecated public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); } public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE"; field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java index b1dd4676a35e..815fe05cc3ab 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java @@ -19,12 +19,12 @@ package com.google.android.appfunctions.sidecar; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.content.Context; +import android.os.CancellationSignal; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; - /** * Provides app functions related functionalities. * @@ -45,7 +45,7 @@ public final class AppFunctionManager { * * @param context A {@link Context}. * @throws java.lang.IllegalStateException if the underlying {@link - * android.app.appfunctions.AppFunctionManager} is not found. + * android.app.appfunctions.AppFunctionManager} is not found. */ public AppFunctionManager(Context context) { mContext = Objects.requireNonNull(context); @@ -66,6 +66,7 @@ public final class AppFunctionManager { public void executeAppFunction( @NonNull ExecuteAppFunctionRequest sidecarRequest, @NonNull @CallbackExecutor Executor executor, + @NonNull CancellationSignal cancellationSignal, @NonNull Consumer<ExecuteAppFunctionResponse> callback) { Objects.requireNonNull(sidecarRequest); Objects.requireNonNull(executor); @@ -74,9 +75,40 @@ public final class AppFunctionManager { android.app.appfunctions.ExecuteAppFunctionRequest platformRequest = SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest); mManager.executeAppFunction( - platformRequest, executor, (platformResponse) -> { - callback.accept(SidecarConverter.getSidecarExecuteAppFunctionResponse( - platformResponse)); + platformRequest, + executor, + cancellationSignal, + (platformResponse) -> { + callback.accept( + SidecarConverter.getSidecarExecuteAppFunctionResponse( + platformResponse)); }); } + + /** + * Executes the app function. + * + * <p>Proxies request and response to the underlying {@link + * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and + * response in the appropriate type required by the function. + * + * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor, + * CancellationSignal, Consumer)} instead. This method will be removed once usage references + * are updated. + */ + @Deprecated + public void executeAppFunction( + @NonNull ExecuteAppFunctionRequest sidecarRequest, + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + Objects.requireNonNull(sidecarRequest); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + + executeAppFunction( + sidecarRequest, + executor, + new CancellationSignal(), + callback); + } } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java index 65959dfdf561..6023c977bd76 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java @@ -25,6 +25,7 @@ import android.app.Service; import android.content.Intent; import android.os.Binder; import android.os.IBinder; +import android.os.CancellationSignal; import java.util.function.Consumer; @@ -69,10 +70,11 @@ public abstract class AppFunctionService extends Service { private final Binder mBinder = android.app.appfunctions.AppFunctionService.createBinder( /* context= */ this, - /* onExecuteFunction= */ (platformRequest, callback) -> { + /* onExecuteFunction= */ (platformRequest, cancellationSignal, callback) -> { AppFunctionService.this.onExecuteFunction( SidecarConverter.getSidecarExecuteAppFunctionRequest( platformRequest), + cancellationSignal, (sidecarResponse) -> { callback.accept( SidecarConverter.getPlatformExecuteAppFunctionResponse( @@ -105,9 +107,42 @@ public abstract class AppFunctionService extends Service { * result using the callback, no matter if the execution was successful or not. * * @param request The function execution request. + * @param cancellationSignal A {@link CancellationSignal} to cancel the request. * @param callback A callback to report back the result. */ @MainThread + public void onExecuteFunction( + @NonNull ExecuteAppFunctionRequest request, + @NonNull CancellationSignal cancellationSignal, + @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + onExecuteFunction(request, callback); + } + + /** + * Called by the system to execute a specific app function. + * + * <p>This method is triggered when the system requests your AppFunctionService to handle a + * particular function you have registered and made available. + * + * <p>To ensure proper routing of function requests, assign a unique identifier to each + * function. This identifier doesn't need to be globally unique, but it must be unique within + * your app. For example, a function to order food could be identified as "orderFood". In most + * cases this identifier should come from the ID automatically generated by the AppFunctions + * SDK. You can determine the specific function to invoke by calling {@link + * ExecuteAppFunctionRequest#getFunctionIdentifier()}. + * + * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker + * thread and dispatch the result with the given callback. You should always report back the + * result using the callback, no matter if the execution was successful or not. + * + * @param request The function execution request. + * @param callback A callback to report back the result. + * + * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal, + * Consumer)} instead. This method will be removed once usage references are updated. + */ + @MainThread + @Deprecated public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull Consumer<ExecuteAppFunctionResponse> callback); diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index d4851e1ad698..af07686f064c 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -33,6 +33,7 @@ android_library { "SettingsLibBarChartPreference", "SettingsLibButtonPreference", "SettingsLibBulletPreference", + "SettingsLibCardPreference", "SettingsLibCollapsingToolbarBaseActivity", "SettingsLibDeviceStateRotationLock", "SettingsLibDisplayUtils", diff --git a/packages/SettingsLib/CardPreference/Android.bp b/packages/SettingsLib/CardPreference/Android.bp new file mode 100644 index 000000000000..1d871d168ee5 --- /dev/null +++ b/packages/SettingsLib/CardPreference/Android.bp @@ -0,0 +1,33 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_library { + name: "SettingsLibCardPreference", + use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + resource_dirs: ["res"], + + static_libs: [ + "androidx.annotation_annotation", + "androidx.preference_preference", + "SettingsLibSettingsTheme", + ], + sdk_version: "system_current", + min_sdk_version: "21", + apex_available: [ + "//apex_available:platform", + ], +} diff --git a/packages/SettingsLib/CardPreference/AndroidManifest.xml b/packages/SettingsLib/CardPreference/AndroidManifest.xml new file mode 100644 index 000000000000..717f66e0296c --- /dev/null +++ b/packages/SettingsLib/CardPreference/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.settingslib.widget.preference.card"> + + <uses-sdk android:minSdkVersion="21" /> + +</manifest> diff --git a/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml b/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml new file mode 100644 index 000000000000..716ed412eb5c --- /dev/null +++ b/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<com.google.android.material.card.MaterialCardView + xmlns:android="http://schemas.android.com/apk/res/android" + style="@style/SettingsLibCardStyle"> + + <LinearLayout + android:id="@+id/card_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:baselineAligned="false" + android:minHeight="@dimen/settingslib_expressive_space_large3" + android:paddingStart="@dimen/settingslib_expressive_space_small1" + android:paddingEnd="@dimen/settingslib_expressive_space_small1" + android:orientation="horizontal" + android:gravity="center_vertical"> + + <LinearLayout + android:id="@+id/icon_frame" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="@dimen/settingslib_expressive_space_medium3" + android:minHeight="@dimen/settingslib_expressive_space_medium3" + android:gravity="center" + android:orientation="horizontal"> + + <ImageView + android:id="@android:id/icon" + android:src="@drawable/settingslib_arrow_drop_down" + android:layout_width="@dimen/settingslib_expressive_space_medium3" + android:layout_height="@dimen/settingslib_expressive_space_medium3" + android:scaleType="centerInside"/> + + </LinearLayout> + + <LinearLayout + android:id="@+id/text_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingHorizontal="@dimen/settingslib_expressive_space_small1" + android:paddingVertical="@dimen/settingslib_expressive_space_small2" + android:orientation="vertical"> + + <TextView + android:id="@android:id/title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hyphenationFrequency="normalFast" + android:lineBreakWordStyle="phrase" + android:textAppearance="@style/TextAppearance.CardTitle.SettingsLib"/> + + <TextView + android:id="@android:id/summary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hyphenationFrequency="normalFast" + android:lineBreakWordStyle="phrase" + android:textAppearance="@style/TextAppearance.CardSummary.SettingsLib"/> + + </LinearLayout> + + <ImageView + android:id="@android:id/closeButton" + android:layout_width="@dimen/settingslib_expressive_space_medium4" + android:layout_height="@dimen/settingslib_expressive_space_medium4" + android:padding="@dimen/settingslib_expressive_space_extrasmall4" + android:layout_gravity="center" + android:src="@drawable/settingslib_expressive_icon_close" + android:background="?android:attr/selectableItemBackground" /> + + </LinearLayout> + +</com.google.android.material.card.MaterialCardView>
\ No newline at end of file diff --git a/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml b/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml new file mode 100644 index 000000000000..4cbdea52d439 --- /dev/null +++ b/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <style name="TextAppearance.CardTitle.SettingsLib" + parent="@style/TextAppearance.PreferenceTitle.SettingsLib"> + <item name="android:textColor">@color/settingslib_materialColorOnPrimary</item> + <item name="android:textSize">20sp</item> + </style> + + <style name="TextAppearance.CardSummary.SettingsLib" + parent="@style/TextAppearance.PreferenceSummary.SettingsLib"> + <item name="android:textColor">@color/settingslib_materialColorOnSecondary</item> + <item name="android:textSize">14sp</item> + </style> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt b/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt new file mode 100644 index 000000000000..eb14746a0f22 --- /dev/null +++ b/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import com.android.settingslib.widget.preference.card.R + +/** + * The CardPreference shows a card like suggestion in homepage, which also support dismiss. + */ +class CardPreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : Preference(context, attrs, defStyleAttr, defStyleRes) { + + init { + layoutResource = R.layout.settingslib_expressive_preference_card + } + private var dismissible = false + set(value) { + if (field != value) { + field = value + notifyChanged() + } + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + holder.isDividerAllowedBelow = false + holder.isDividerAllowedAbove = false + + holder.findViewById(android.R.id.closeButton)?.let { dismissButton -> + dismissButton.visibility = if (dismissible) View.VISIBLE else View.GONE + dismissButton.setOnClickListener { + isVisible = false + } + } + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml index 816433c1a18b..dc2eb648f8c1 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml @@ -193,4 +193,15 @@ <item name="android:layout_gravity">center</item> <item name="trackTint">@color/settingslib_expressive_color_main_switch_track</item> </style> + + <style name="SettingsLibCardStyle" parent=""> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_marginHorizontal">?android:attr/listPreferredItemPaddingStart</item> + <item name="android:layout_marginVertical">@dimen/settingslib_expressive_space_extrasmall4</item> + <item name="cardBackgroundColor">@color/settingslib_materialColorPrimary</item> + <item name="cardCornerRadius">@dimen/settingslib_expressive_radius_extralarge3</item> + <item name="cardElevation">0dp</item> + <item name="rippleColor">?android:attr/colorControlHighlight</item> + </style> </resources>
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt index 460bf9993b41..965c97124329 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt @@ -162,3 +162,6 @@ internal fun rememberSettingsTypography(): Typography { /** Creates a new [TextStyle] which font weight set to medium. */ internal fun TextStyle.toMediumWeight() = copy(fontWeight = FontWeight.Medium, letterSpacing = 0.01.em) + +internal fun TextStyle.toSemiBoldWeight() = + copy(fontWeight = FontWeight.SemiBold, letterSpacing = 0.01.em) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt index 7e1df1694b10..5bb57b8ed1df 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt @@ -110,7 +110,7 @@ private fun RowScope.ActionButton(actionButton: ActionButton) { shape = RectangleShape, colors = ButtonDefaults.filledTonalButtonColors( containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimary, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, disabledContainerColor = MaterialTheme.colorScheme.surface, ), contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp), diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt index 94d2c210daab..f99d20669183 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt @@ -41,9 +41,7 @@ import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled internal fun NavigateBack() { val navController = LocalNavController.current val contentDescription = stringResource(R.string.abc_action_bar_up_description) - BackAction(contentDescription) { - navController.navigateBack() - } + BackAction(contentDescription) { navController.navigateBack() } } /** Action that collapses the search bar. */ @@ -55,15 +53,35 @@ internal fun CollapseAction(onClick: () -> Unit) { @Composable private fun BackAction(contentDescription: String, onClick: () -> Unit) { - IconButton(onClick) { + IconButton( + onClick = onClick, + modifier = + if (isSpaExpressiveEnabled) + Modifier + .padding( + start = SettingsDimension.paddingLarge, + end = SettingsDimension.paddingSmall, + top = SettingsDimension.paddingExtraSmall, + bottom = SettingsDimension.paddingExtraSmall, + ) + .size(SettingsDimension.actionIconWidth, SettingsDimension.actionIconHeight) + .clip(SettingsShape.CornerExtraLarge) + else Modifier, + ) { Icon( imageVector = Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = contentDescription, - modifier = if (isSpaExpressiveEnabled) Modifier - .size(SettingsDimension.actionIconWidth, SettingsDimension.actionIconHeight) - .clip(SettingsShape.CornerExtraLarge) - .background(MaterialTheme.colorScheme.surfaceContainerHigh) - .padding(SettingsDimension.actionIconPadding) else Modifier + modifier = + if (isSpaExpressiveEnabled) + Modifier + .size( + SettingsDimension.actionIconWidth, + SettingsDimension.actionIconHeight, + ) + .clip(SettingsShape.CornerExtraLarge) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .padding(SettingsDimension.actionIconPadding) + else Modifier, ) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt index 2ae3b569bc70..2c55779c9a01 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt @@ -78,7 +78,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled import com.android.settingslib.spa.framework.theme.settingsBackground +import com.android.settingslib.spa.framework.theme.toSemiBoldWeight import kotlin.math.abs import kotlin.math.max import kotlin.math.roundToInt @@ -116,8 +118,12 @@ internal fun CustomizedLargeTopAppBar( ) { TwoRowsTopAppBar( title = { Title(title = title, maxLines = 3) }, - titleTextStyle = MaterialTheme.typography.displaySmall, - smallTitleTextStyle = MaterialTheme.typography.titleMedium, + titleTextStyle = + if (isSpaExpressiveEnabled) MaterialTheme.typography.displaySmall.toSemiBoldWeight() + else MaterialTheme.typography.displaySmall, + smallTitleTextStyle = + if (isSpaExpressiveEnabled) MaterialTheme.typography.titleLarge.toSemiBoldWeight() + else MaterialTheme.typography.titleLarge, titleBottomPadding = LargeTitleBottomPadding, smallTitle = { Title(title = title, maxLines = 1) }, modifier = modifier, @@ -136,7 +142,9 @@ private fun Title(title: String, maxLines: Int = Int.MAX_VALUE) { text = title, modifier = Modifier.padding( - start = SettingsDimension.itemPaddingAround, + start = + if (isSpaExpressiveEnabled) SettingsDimension.paddingExtraSmall + else SettingsDimension.itemPaddingAround, end = SettingsDimension.itemPaddingEnd, ) .semantics { heading() }, @@ -194,7 +202,7 @@ private class TopAppBarColors( return lerp( containerColor, scrolledContainerColor, - FastOutLinearInEasing.transform(colorTransitionFraction) + FastOutLinearInEasing.transform(colorTransitionFraction), ) } @@ -241,7 +249,7 @@ private fun SingleRowTopAppBar( Row( horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, - content = actions + content = actions, ) } @@ -296,7 +304,7 @@ private fun TwoRowsTopAppBar( windowInsets: WindowInsets, colors: TopAppBarColors, pinnedHeight: Dp, - scrollBehavior: TopAppBarScrollBehavior? + scrollBehavior: TopAppBarScrollBehavior?, ) { if (MaxHeightWithoutTitle <= pinnedHeight) { throw IllegalArgumentException( @@ -333,7 +341,7 @@ private fun TwoRowsTopAppBar( Row( horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, - content = actions + content = actions, ) } val topTitleAlpha = { TopTitleAlphaEasing.transform(colorTransitionFraction()) } @@ -356,9 +364,9 @@ private fun TwoRowsTopAppBar( scrollBehavior.state, velocity, scrollBehavior.flingAnimationSpec, - scrollBehavior.snapAnimationSpec + scrollBehavior.snapAnimationSpec, ) - } + }, ) } else { Modifier @@ -412,7 +420,8 @@ private fun TwoRowsTopAppBar( val measuredMaxHeightPx = density.run { MaxHeightWithoutTitle.toPx() + - coordinates.size.height.toFloat() + coordinates.size.height.toFloat() + + titleBaselineHeight.toPx() } // Allow larger max height for multi-line title, but do not reduce // max height to prevent flaky. @@ -430,7 +439,7 @@ private fun TwoRowsTopAppBar( titleBottomPadding = titleBottomPaddingPx, hideTitleSemantics = hideBottomRowSemantics, navigationIcon = {}, - actions = {} + actions = {}, ) } } @@ -485,7 +494,7 @@ private fun TopAppBarLayout( Box(Modifier.layoutId("navigationIcon").padding(start = TopAppBarHorizontalPadding)) { CompositionLocalProvider( LocalContentColor provides navigationIconContentColor, - content = navigationIcon + content = navigationIcon, ) } Box( @@ -504,18 +513,18 @@ private fun TopAppBarLayout( fontScale = if (titleScaleDisabled) 1f else fontScale, ) }, - content = title + content = title, ) } } Box(Modifier.layoutId("actionIcons").padding(end = TopAppBarHorizontalPadding)) { CompositionLocalProvider( LocalContentColor provides actionIconContentColor, - content = actions + content = actions, ) } }, - modifier = modifier + modifier = modifier, ) { measurables, constraints -> val navigationIconPlaceable = measurables @@ -552,7 +561,7 @@ private fun TopAppBarLayout( // Navigation icon navigationIconPlaceable.placeRelative( x = 0, - y = (layoutHeight - navigationIconPlaceable.height) / 2 + y = (layoutHeight - navigationIconPlaceable.height) / 2, ) // Title @@ -570,17 +579,17 @@ private fun TopAppBarLayout( titlePlaceable.height - max( 0, - titleBottomPadding - titlePlaceable.height + titleBaseline + titleBottomPadding - titlePlaceable.height + titleBaseline, ) // Arrangement.Top else -> 0 - } + }, ) // Action icons actionIconsPlaceable.placeRelative( x = constraints.maxWidth - actionIconsPlaceable.width, - y = (layoutHeight - actionIconsPlaceable.height) / 2 + y = (layoutHeight - actionIconsPlaceable.height) / 2, ) } } @@ -595,7 +604,7 @@ private suspend fun settleAppBar( state: TopAppBarState, velocity: Float, flingAnimationSpec: DecayAnimationSpec<Float>?, - snapAnimationSpec: AnimationSpec<Float>? + snapAnimationSpec: AnimationSpec<Float>?, ): Velocity { // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar, // and just return Zero Velocity. @@ -609,20 +618,18 @@ private suspend fun settleAppBar( // continue the motion to expand or collapse the app bar. if (flingAnimationSpec != null && abs(velocity) > 1f) { var lastValue = 0f - AnimationState( - initialValue = 0f, - initialVelocity = velocity, - ) - .animateDecay(flingAnimationSpec) { - val delta = value - lastValue - val initialHeightOffset = state.heightOffset - state.heightOffset = initialHeightOffset + delta - val consumed = abs(initialHeightOffset - state.heightOffset) - lastValue = value - remainingVelocity = this.velocity - // avoid rounding errors and stop if anything is unconsumed - if (abs(delta - consumed) > 0.5f) this.cancelAnimation() - } + AnimationState(initialValue = 0f, initialVelocity = velocity).animateDecay( + flingAnimationSpec + ) { + val delta = value - lastValue + val initialHeightOffset = state.heightOffset + state.heightOffset = initialHeightOffset + delta + val consumed = abs(initialHeightOffset - state.heightOffset) + lastValue = value + remainingVelocity = this.velocity + // avoid rounding errors and stop if anything is unconsumed + if (abs(delta - consumed) > 0.5f) this.cancelAnimation() + } } // Snap if animation specs were provided. if (snapAnimationSpec != null) { @@ -633,7 +640,7 @@ private suspend fun settleAppBar( } else { state.heightOffsetLimit }, - animationSpec = snapAnimationSpec + animationSpec = snapAnimationSpec, ) { state.heightOffset = value } @@ -647,9 +654,10 @@ private suspend fun settleAppBar( // Medium or Large app bar. private val TopTitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f) -internal val MaxHeightWithoutTitle = 124.dp +internal val MaxHeightWithoutTitle = if (isSpaExpressiveEnabled) 84.dp else 124.dp internal val DefaultTitleHeight = 52.dp internal val ContainerHeight = 56.dp +private val titleBaselineHeight = if (isSpaExpressiveEnabled) 8.dp else 0.dp private val LargeTitleBottomPadding = 28.dp private val TopAppBarHorizontalPadding = 4.dp diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java index 58dc8c7aad6c..e7c7476d4797 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java @@ -45,6 +45,7 @@ import java.lang.annotation.RetentionPolicy; DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS, DeviceSettingId.DEVICE_SETTING_ID_DEVICE_DETAILS_FOOTER, DeviceSettingId.DEVICE_SETTING_ID_ANC, + DeviceSettingId.DEVICE_SETTING_ID_GENERAL_BLUETOOTH_DEVICE_HEADER, }, open = true) public @interface DeviceSettingId { @@ -114,6 +115,9 @@ public @interface DeviceSettingId { /** Device setting ID for "More Settings" page. */ int DEVICE_SETTING_ID_MORE_SETTINGS = 21; + /** Device setting ID for general bluetooth device header. */ + int DEVICE_SETTING_ID_GENERAL_BLUETOOTH_DEVICE_HEADER = 22; + /** Device setting ID for ANC. */ int DEVICE_SETTING_ID_ANC = 1001; } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt index 38183d5a01fd..da01b3bcaafb 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt @@ -32,9 +32,9 @@ import android.os.Parcelable */ data class DeviceSettingItem( @DeviceSettingId val settingId: Int, - val packageName: String, - val className: String, - val intentAction: String, + val packageName: String? = null, + val className: String? = null, + val intentAction: String? = null, val preferenceKey: String? = null, val highlighted: Boolean = false, val extras: Bundle = Bundle.EMPTY, @@ -62,11 +62,11 @@ data class DeviceSettingItem( parcel.run { DeviceSettingItem( settingId = readInt(), - packageName = readString() ?: "", - className = readString() ?: "", - intentAction = readString() ?: "", + packageName = readString(), + className = readString(), + intentAction = readString(), highlighted = readBoolean(), - preferenceKey = readString() ?: "", + preferenceKey = readString(), extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY, ) } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.aidl new file mode 100644 index 000000000000..d8378067b115 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.bluetooth.devicesettings; + +parcelable DeviceSettingsConfigServiceStatus;
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt new file mode 100644 index 000000000000..ae867713c831 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.bluetooth.devicesettings + +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable + +/** + * A data class representing a device settings config service status. + * + * @property success Whether the status is succeed. + * @property extras Extra bundle + */ +data class DeviceSettingsConfigServiceStatus( + val success: Boolean, + val extras: Bundle = Bundle.EMPTY, +) : Parcelable { + + override fun describeContents(): Int = 0 + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.run { + writeBoolean(success) + writeBundle(extras) + } + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator<DeviceSettingsConfigServiceStatus> = + object : Parcelable.Creator<DeviceSettingsConfigServiceStatus> { + override fun createFromParcel(parcel: Parcel) = + parcel.run { + DeviceSettingsConfigServiceStatus( + success = readBoolean(), + extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY, + ) + } + + override fun newArray(size: Int): Array<DeviceSettingsConfigServiceStatus?> { + return arrayOfNulls(size) + } + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt index 977849e75556..77d790e7f773 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt @@ -21,7 +21,7 @@ import android.os.Parcel import android.os.Parcelable /** - * A data class representing a device settings item in bluetooth device details config. + * A data class representing a device settings provider service status. * * @property enabled Whether the service is enabled. * @property extras Extra bundle diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl index 647611ed8ef4..9cf49070a62c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl @@ -17,8 +17,8 @@ package com.android.settingslib.bluetooth.devicesettings; import com.android.settingslib.bluetooth.devicesettings.DeviceInfo; -import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig; +import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback; interface IDeviceSettingsConfigProviderService { - DeviceSettingsConfig getDeviceSettingsConfig(in DeviceInfo device); + oneway void getDeviceSettingsConfig(in DeviceInfo device, in IGetDeviceSettingsConfigCallback callback); }
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.aidl new file mode 100644 index 000000000000..403cdd9e4d70 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.aidl @@ -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.settingslib.bluetooth.devicesettings; + +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig; +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfigServiceStatus; + +interface IGetDeviceSettingsConfigCallback { + oneway void onResult(in DeviceSettingsConfigServiceStatus status, in DeviceSettingsConfig config) = 0; +}
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt index 3d8ff86c9377..4af0504bd73a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt @@ -33,12 +33,15 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId import com.android.settingslib.bluetooth.devicesettings.DeviceSettingPreferenceState import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfigServiceStatus import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService +import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback import com.android.settingslib.bluetooth.devicesettings.data.model.ServiceConnectionStatus import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.resume import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -63,6 +66,7 @@ import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.plus +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext @OptIn(ExperimentalCoroutinesApi::class) @@ -74,22 +78,22 @@ class DeviceSettingServiceConnection( private val backgroundCoroutineContext: CoroutineContext, ) { data class EndPoint( - private val packageName: String, + private val packageName: String?, private val className: String?, - private val intentAction: String, + private val intentAction: String?, ) { - fun toIntent(): Intent = - Intent().apply { + fun toIntent(): Intent? { + if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(intentAction)) { + return null + } + return Intent().apply { if (className.isNullOrBlank()) { setPackage(packageName) } else { - setClassName(packageName, className) + setClassName(packageName!!, className) } setAction(intentAction) } - - fun isValid(): Boolean { - return !TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(intentAction) } } @@ -126,8 +130,9 @@ class DeviceSettingServiceConnection( when (it) { is ServiceConnectionStatus.Connected -> flowOf( - it.service.getDeviceSettingsConfig( - deviceInfo { setBluetoothAddress(cachedDevice.address) } + getDeviceSettingsConfigFromService( + deviceInfo { setBluetoothAddress(cachedDevice.address) }, + it.service, ) ) ServiceConnectionStatus.Connecting -> flowOf() @@ -137,6 +142,27 @@ class DeviceSettingServiceConnection( .first() } + private suspend fun getDeviceSettingsConfigFromService( + deviceInfo: DeviceInfo, + service: IDeviceSettingsConfigProviderService, + ): DeviceSettingsConfig? = suspendCancellableCoroutine { continuation -> + service.getDeviceSettingsConfig( + deviceInfo, + object : IGetDeviceSettingsConfigCallback.Stub() { + override fun onResult( + status: DeviceSettingsConfigServiceStatus, + config: DeviceSettingsConfig?, + ) { + if (!status.success) { + continuation.resume(null) + } else { + continuation.resume(config) + } + } + }, + ) + } + private val settingIdToItemMapping = flow { if (!isServiceEnabled.await()) { @@ -160,6 +186,12 @@ class DeviceSettingServiceConnection( } .shareIn(scope = coroutineScope, started = SharingStarted.WhileSubscribed(), replay = 1) + private val services = + ConcurrentHashMap< + EndPoint, + StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>, + >() + /** Gets [DeviceSettingsConfig] for the device, return null when failed. */ suspend fun getDeviceSettingsConfig(): DeviceSettingsConfig? { if (!isServiceEnabled.await()) { @@ -222,24 +254,23 @@ class DeviceSettingServiceConnection( ) } } - ?.filter { it.isValid() } ?.distinct() - ?.associateBy( - { it }, - { endpoint -> - services.computeIfAbsent(endpoint) { - getService( - endpoint.toIntent(), - IDeviceSettingsProviderService.Stub::asInterface, - ) - .stateIn( - coroutineScope.plus(backgroundCoroutineContext), - SharingStarted.WhileSubscribed(), - ServiceConnectionStatus.Connecting, - ) - } - }, - ) + ?.mapNotNull { endpoint -> + endpoint.toIntent()?.let { intent -> + Pair( + endpoint, + services.computeIfAbsent(endpoint) { + getService(intent, IDeviceSettingsProviderService.Stub::asInterface) + .stateIn( + coroutineScope.plus(backgroundCoroutineContext), + SharingStarted.WhileSubscribed(), + ServiceConnectionStatus.Connecting, + ) + }, + ) + } + } + ?.toMap() private fun getDeviceSettingsFromService( cachedDevice: CachedBluetoothDevice, @@ -320,11 +351,5 @@ class DeviceSettingServiceConnection( const val CONFIG_SERVICE_PACKAGE_NAME = "DEVICE_SETTINGS_CONFIG_PACKAGE_NAME" const val CONFIG_SERVICE_CLASS_NAME = "DEVICE_SETTINGS_CONFIG_CLASS" const val CONFIG_SERVICE_INTENT_ACTION = "DEVICE_SETTINGS_CONFIG_ACTION" - - val services = - ConcurrentHashMap< - EndPoint, - StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>, - >() } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt new file mode 100644 index 000000000000..3149acf6fc3c --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.bluetooth.devicesettings + +import android.os.Bundle +import android.os.Parcel +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DeviceSettingsConfigServiceStatusTest { + + @Test + fun parcelOperation() { + val item = + DeviceSettingsConfigServiceStatus( + success = true, + extras = Bundle().apply { putString("key1", "value1") }, + ) + + val fromParcel = writeAndRead(item) + + assertThat(fromParcel.success).isEqualTo(item.success) + assertThat(fromParcel.extras.getString("key1")).isEqualTo(item.extras.getString("key1")) + } + + private fun writeAndRead( + item: DeviceSettingsConfigServiceStatus + ): DeviceSettingsConfigServiceStatus { + val parcel = Parcel.obtain() + item.writeToParcel(parcel, 0) + parcel.setDataPosition(0) + return DeviceSettingsConfigServiceStatus.CREATOR.createFromParcel(parcel) + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt index 0cb6bc1b1261..4e62fd3b27c5 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt @@ -33,10 +33,12 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfigServiceStatus import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsProviderServiceStatus import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService +import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreferenceState import com.android.settingslib.bluetooth.devicesettings.ToggleInfo @@ -53,7 +55,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test @@ -140,15 +141,10 @@ class DeviceSettingRepositoryTest { ) } - @After - fun clean() { - DeviceSettingServiceConnection.services.clear() - } - @Test fun getDeviceSettingsConfig_withMetadata_success() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService1.serviceStatus) .thenReturn(DeviceSettingsProviderServiceStatus(true)) `when`(settingProviderService2.serviceStatus) @@ -179,7 +175,7 @@ class DeviceSettingRepositoryTest { ) ) .thenReturn("".toByteArray()) - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService1.serviceStatus) .thenReturn(DeviceSettingsProviderServiceStatus(true)) `when`(settingProviderService2.serviceStatus) @@ -194,7 +190,7 @@ class DeviceSettingRepositoryTest { @Test fun getDeviceSettingsConfig_providerServiceNotEnabled_returnNull() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService1.serviceStatus) .thenReturn(DeviceSettingsProviderServiceStatus(false)) `when`(settingProviderService2.serviceStatus) @@ -209,7 +205,7 @@ class DeviceSettingRepositoryTest { @Test fun getDeviceSettingsConfig_bindingServiceFail_returnNull() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) doReturn(false).`when`(context).bindService(any(), anyInt(), any(), any()) val config = underTest.getDeviceSettingsConfig(cachedDevice) @@ -221,7 +217,7 @@ class DeviceSettingRepositoryTest { @Test fun getDeviceSetting_actionSwitchPreference_success() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { input -> input @@ -247,7 +243,7 @@ class DeviceSettingRepositoryTest { @Test fun getDeviceSetting_multiTogglePreference_success() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then { input -> input @@ -273,7 +269,7 @@ class DeviceSettingRepositoryTest { @Test fun getDeviceSetting_helpPreference_success() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then { input -> input @@ -299,6 +295,7 @@ class DeviceSettingRepositoryTest { @Test fun getDeviceSetting_noConfig_returnNull() { testScope.runTest { + setUpConfigService(false, null) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { input -> input @@ -320,7 +317,7 @@ class DeviceSettingRepositoryTest { @Test fun updateDeviceSettingState_switchState_success() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { input -> input @@ -358,7 +355,7 @@ class DeviceSettingRepositoryTest { @Test fun updateDeviceSettingState_multiToggleState_success() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then { input -> input @@ -459,6 +456,17 @@ class DeviceSettingRepositoryTest { assertThat(actual.settingId).isEqualTo(serviceResponse.settingId) } + private fun setUpConfigService(success: Boolean, config: DeviceSettingsConfig?) { + `when`(configService.getDeviceSettingsConfig(any(), any())).then { input -> + input + .getArgument<IGetDeviceSettingsConfigCallback>(1) + .onResult( + DeviceSettingsConfigServiceStatus(success = success), + config + ) + } + } + private companion object { const val BLUETOOTH_ADDRESS = "12:34:56:78" const val CONFIG_SERVICE_PACKAGE_NAME = "com.android.fake.configservice" diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index d7109398b956..5e31da411e49 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -164,6 +164,7 @@ public class SecureSettings { Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, Settings.Secure.SHOW_NOTIFICATION_SNOOZE, Settings.Secure.NOTIFICATION_HISTORY_ENABLED, Settings.Secure.ZEN_DURATION, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index fa16a44f4592..b3f73749f393 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -243,6 +243,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SHOW_NOTIFICATION_SNOOZE, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.NOTIFICATION_HISTORY_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ZEN_DURATION, ANY_INTEGER_VALIDATOR); diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index f8383d94b1ab..88cc152b3819 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -40,16 +40,6 @@ flag { } flag { - name: "notification_minimalism_prototype" - namespace: "systemui" - description: "Prototype of notification minimalism; the new 'Intermediate' lockscreen customization proposal." - bug: "330387368" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "notification_view_flipper_pausing_v2" namespace: "systemui" description: "Pause ViewFlippers inside Notification custom layouts when the shade is closed." diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt index a5f8057b524f..20efea513b3a 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt @@ -28,11 +28,11 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonColors import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import com.android.compose.theme.LocalAndroidColorScheme @Composable fun PlatformButton( @@ -100,12 +100,7 @@ fun PlatformIconButton( @DrawableRes iconResource: Int, contentDescription: String?, ) { - IconButton( - modifier = modifier, - onClick = onClick, - enabled = enabled, - colors = colors, - ) { + IconButton(modifier = modifier, onClick = onClick, enabled = enabled, colors = colors) { Icon( painter = painterResource(id = iconResource), contentDescription = contentDescription, @@ -118,7 +113,7 @@ private val ButtonPaddings = PaddingValues(horizontal = 16.dp, vertical = 8.dp) @Composable private fun filledButtonColors(): ButtonColors { - val colors = LocalAndroidColorScheme.current + val colors = MaterialTheme.colorScheme return ButtonDefaults.buttonColors( containerColor = colors.primary, contentColor = colors.onPrimary, @@ -127,27 +122,22 @@ private fun filledButtonColors(): ButtonColors { @Composable private fun outlineButtonColors(): ButtonColors { - return ButtonDefaults.outlinedButtonColors( - contentColor = LocalAndroidColorScheme.current.onSurface, - ) + return ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.onSurface) } @Composable private fun iconButtonColors(): IconButtonColors { return IconButtonDefaults.filledIconButtonColors( - contentColor = LocalAndroidColorScheme.current.onSurface, + contentColor = MaterialTheme.colorScheme.onSurface ) } @Composable private fun outlineButtonBorder(): BorderStroke { - return BorderStroke( - width = 1.dp, - color = LocalAndroidColorScheme.current.primary, - ) + return BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.primary) } @Composable private fun textButtonColors(): ButtonColors { - return ButtonDefaults.textButtonColors(contentColor = LocalAndroidColorScheme.current.primary) + return ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.primary) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt index 0b9669410b8e..69ca0a5f476c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt @@ -38,7 +38,6 @@ import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.layoutId import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.android.compose.theme.LocalAndroidColorScheme import kotlin.math.roundToInt /** @@ -69,7 +68,7 @@ fun AlertDialogContent( Modifier.defaultMinSize(minWidth = defaultSize, minHeight = defaultSize), propagateMinConstraints = true, ) { - val iconColor = LocalAndroidColorScheme.current.primary + val iconColor = MaterialTheme.colorScheme.primary CompositionLocalProvider(LocalContentColor provides iconColor) { icon() } } @@ -77,7 +76,7 @@ fun AlertDialogContent( } // Title. - val titleColor = LocalAndroidColorScheme.current.onSurface + val titleColor = MaterialTheme.colorScheme.onSurface CompositionLocalProvider(LocalContentColor provides titleColor) { ProvideTextStyle( MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center) @@ -88,7 +87,7 @@ fun AlertDialogContent( Spacer(Modifier.height(16.dp)) // Content. - val contentColor = LocalAndroidColorScheme.current.onSurfaceVariant + val contentColor = MaterialTheme.colorScheme.onSurfaceVariant Box { CompositionLocalProvider(LocalContentColor provides contentColor) { ProvideTextStyle( @@ -169,7 +168,7 @@ private fun AlertDialogButtons( negative.width - positive.width - horizontalSpacing.roundToInt(), - 0 + 0, ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt index ba689179c33d..29035ce2aa0a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt @@ -38,7 +38,6 @@ import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.scene.data.repository.Idle import com.android.systemui.scene.data.repository.Transition import com.android.systemui.scene.data.repository.setSceneTransition -import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.testKosmos @@ -76,7 +75,6 @@ class KeyguardDismissActionInteractorTest : SysuiTestCase() { transitionInteractor = kosmos.keyguardTransitionInteractor, dismissInteractor = dismissInteractor, applicationScope = testScope.backgroundScope, - sceneInteractor = { kosmos.sceneInteractor }, deviceUnlockedInteractor = { kosmos.deviceUnlockedInteractor }, powerInteractor = kosmos.powerInteractor, alternateBouncerInteractor = kosmos.alternateBouncerInteractor, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt index 7b87aeb60c13..d772e3effbeb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt @@ -42,7 +42,8 @@ import com.android.systemui.statusbar.notification.collection.modifyEntry import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.domain.interactor.lockScreenNotificationMinimalismSetting +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.testKosmos import com.android.systemui.util.settings.FakeSettings @@ -66,7 +67,7 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) -@EnableFlags(NotificationMinimalismPrototype.FLAG_NAME) +@EnableFlags(NotificationMinimalism.FLAG_NAME) class LockScreenMinimalismCoordinatorTest : SysuiTestCase() { private val kosmos = @@ -76,7 +77,7 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() { mock<SysuiStatusBarStateController>().also { mock -> doAnswer { statusBarState.ordinal }.whenever(mock).state } - fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1) + lockScreenNotificationMinimalismSetting = true } private val notifPipeline: NotifPipeline = mock() private var statusBarState: StatusBarState = StatusBarState.KEYGUARD @@ -193,7 +194,7 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() { kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse() assertThat(promoter.shouldPromoteToTopLevel(child2)) - .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen) + .isEqualTo(NotificationMinimalism.ungroupTopUnseen) assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse() assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse() @@ -201,7 +202,7 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() { kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue() assertThat(promoter.shouldPromoteToTopLevel(child2)) - .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen) + .isEqualTo(NotificationMinimalism.ungroupTopUnseen) assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse() assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt index 2159b864d2a2..ea2e25e8eb1c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt @@ -21,7 +21,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest @@ -57,7 +57,7 @@ class SeenNotificationsInteractorTest : SysuiTestCase() { } @Test - @EnableFlags(NotificationMinimalismPrototype.FLAG_NAME) + @EnableFlags(NotificationMinimalism.FLAG_NAME) fun topOngoingAndUnseenNotification() = runTest { val entry1 = NotificationEntryBuilder().setTag("entry1").build() val entry2 = NotificationEntryBuilder().setTag("entry2").build() @@ -91,4 +91,17 @@ class SeenNotificationsInteractorTest : SysuiTestCase() { testScheduler.runCurrent() assertThat(settingEnabled).isTrue() } + + fun testLockScreenNotificationMinimalismSetting() = runTest { + val settingEnabled by + collectLastValue(underTest.isLockScreenNotificationMinimalismEnabled()) + + kosmos.lockScreenNotificationMinimalismSetting = false + testScheduler.runCurrent() + assertThat(settingEnabled).isFalse() + + kosmos.lockScreenNotificationMinimalismSetting = true + testScheduler.runCurrent() + assertThat(settingEnabled).isTrue() + } } diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp index 8f55961af4e9..0f1da509468a 100644 --- a/packages/SystemUI/shared/Android.bp +++ b/packages/SystemUI/shared/Android.bp @@ -70,6 +70,7 @@ android_library { "jsr330", "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", "//frameworks/libs/systemui:msdl", + "//frameworks/libs/systemui:view_capture", ], resource_dirs: [ "res", diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java index f358ba2d3ccd..4db6ab6ea579 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java @@ -16,6 +16,9 @@ package com.android.systemui.shared.rotation; +import static com.android.app.viewcapture.ViewCaptureFactory.getViewCaptureAwareWindowManagerInstance; +import static com.android.systemui.Flags.enableViewCaptureTracing; + import android.annotation.DimenRes; import android.annotation.IdRes; import android.annotation.LayoutRes; @@ -30,7 +33,6 @@ import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.FrameLayout; @@ -38,6 +40,7 @@ import android.widget.FrameLayout; import androidx.annotation.BoolRes; import androidx.core.view.OneShotPreDrawListener; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.shared.rotation.FloatingRotationButtonPositionCalculator.Position; /** @@ -47,7 +50,7 @@ public class FloatingRotationButton implements RotationButton { private static final int MARGIN_ANIMATION_DURATION_MILLIS = 300; - private final WindowManager mWindowManager; + private final ViewCaptureAwareWindowManager mWindowManager; private final ViewGroup mKeyButtonContainer; private final FloatingRotationButtonView mKeyButtonView; @@ -88,7 +91,8 @@ public class FloatingRotationButton implements RotationButton { @DimenRes int taskbarBottomMargin, @DimenRes int buttonDiameter, @DimenRes int rippleMaxWidth, @BoolRes int floatingRotationBtnPositionLeftResource) { mContext = context; - mWindowManager = mContext.getSystemService(WindowManager.class); + mWindowManager = getViewCaptureAwareWindowManagerInstance(mContext, + enableViewCaptureTracing()); mKeyButtonContainer = (ViewGroup) LayoutInflater.from(mContext).inflate(layout, null); mKeyButtonView = mKeyButtonContainer.findViewById(keyButtonId); mKeyButtonView.setVisibility(View.VISIBLE); diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 820c102e81d8..47f0ecfb237a 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -34,12 +34,14 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.core.StatusBarSimpleFragment import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection import javax.inject.Inject @@ -57,7 +59,7 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // Internal notification frontend dependencies NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token - NotificationMinimalismPrototype.token dependsOn NotificationThrottleHun.token + NotificationMinimalism.token dependsOn NotificationThrottleHun.token ModesEmptyShadeFix.token dependsOn FooterViewRefactor.token ModesEmptyShadeFix.token dependsOn modesUi @@ -73,6 +75,8 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // Status bar chip dependencies statusBarCallChipNotificationIconToken dependsOn statusBarUseReposForCallChipToken statusBarCallChipNotificationIconToken dependsOn statusBarScreenSharingChipsToken + + StatusBarConnectedDisplays.token dependsOn StatusBarSimpleFragment.token } private inline val politeNotifications diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt index ea80911335fa..18b14951ee70 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt @@ -28,7 +28,6 @@ import com.android.systemui.keyguard.shared.model.KeyguardDone import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -60,7 +59,6 @@ constructor( transitionInteractor: KeyguardTransitionInteractor, val dismissInteractor: KeyguardDismissInteractor, @Application private val applicationScope: CoroutineScope, - sceneInteractor: Lazy<SceneInteractor>, deviceUnlockedInteractor: Lazy<DeviceUnlockedInteractor>, powerInteractor: PowerInteractor, alternateBouncerInteractor: AlternateBouncerInteractor, @@ -102,20 +100,20 @@ constructor( private val isOnShadeWhileUnlocked: Flow<Boolean> = if (SceneContainerFlag.isEnabled) { combine( - sceneInteractor.get().currentScene, + shadeInteractor.get().isAnyExpanded, deviceUnlockedInteractor.get().deviceUnlockStatus, - ) { scene, unlockStatus -> - unlockStatus.isUnlocked && - (scene == Scenes.QuickSettings || scene == Scenes.Shade) + ) { isAnyExpanded, unlockStatus -> + isAnyExpanded && unlockStatus.isUnlocked } .distinctUntilChanged() } else if (ComposeBouncerFlags.isOnlyComposeBouncerEnabled()) { combine( - shadeInteractor.get().isAnyExpanded, - keyguardInteractor.get().isKeyguardDismissible, - ) { isAnyExpanded, keyguardDismissible -> - isAnyExpanded && keyguardDismissible - } + shadeInteractor.get().isAnyExpanded, + keyguardInteractor.get().isKeyguardDismissible, + ) { isAnyExpanded, keyguardDismissible -> + isAnyExpanded && keyguardDismissible + } + .distinctUntilChanged() } else { flow { error( diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt index 275f1eecd4db..39cedc36dbec 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt @@ -128,7 +128,7 @@ constructor( data: MediaData, immediately: Boolean, receivedSmartspaceCardLatency: Int, - isSsReactivated: Boolean + isSsReactivated: Boolean, ) { var reusedListener: PlaybackStateListener? = null @@ -183,7 +183,7 @@ constructor( override fun onSmartspaceMediaDataLoaded( key: String, data: SmartspaceMediaData, - shouldPrioritize: Boolean + shouldPrioritize: Boolean, ) { if (!mediaFlags.isPersistentSsCardEnabled()) return @@ -259,7 +259,9 @@ constructor( } override fun onPlaybackStateChanged(state: PlaybackState?) { - processState(state, dispatchEvents = true, currentResumption = resumption) + bgExecutor.execute { + processState(state, dispatchEvents = true, currentResumption = resumption) + } } override fun onSessionDestroyed() { @@ -276,6 +278,7 @@ constructor( } } + @WorkerThread private fun processState( state: PlaybackState?, dispatchEvents: Boolean, diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt index 228b57603bed..d413474fde90 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt @@ -54,6 +54,7 @@ import com.android.systemui.mediaprojection.MediaProjectionServiceHelper import com.android.systemui.mediaprojection.appselector.data.RecentTask import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController import com.android.systemui.res.R +import com.android.systemui.shared.system.ActivityManagerWrapper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.AsyncActivityLauncher import java.lang.IllegalArgumentException @@ -62,9 +63,10 @@ import javax.inject.Inject class MediaProjectionAppSelectorActivity( private val componentFactory: MediaProjectionAppSelectorComponent.Factory, private val activityLauncher: AsyncActivityLauncher, + private val activityManager: ActivityManagerWrapper, /** This is used to override the dependency in a screenshot test */ @VisibleForTesting - private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)? + private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?, ) : ChooserActivity(), MediaProjectionAppSelectorView, @@ -74,8 +76,9 @@ class MediaProjectionAppSelectorActivity( @Inject constructor( componentFactory: MediaProjectionAppSelectorComponent.Factory, - activityLauncher: AsyncActivityLauncher - ) : this(componentFactory, activityLauncher, listControllerFactory = null) + activityLauncher: AsyncActivityLauncher, + activityManager: ActivityManagerWrapper, + ) : this(componentFactory, activityLauncher, activityManager, listControllerFactory = null) private val lifecycleRegistry = LifecycleRegistry(this) override val lifecycle = lifecycleRegistry @@ -100,7 +103,7 @@ class MediaProjectionAppSelectorActivity( callingPackage = callingPackage, view = this, resultHandler = this, - isFirstStart = savedInstanceState == null + isFirstStart = savedInstanceState == null, ) component.lifecycleObservers.forEach { lifecycle.addObserver(it) } @@ -113,7 +116,7 @@ class MediaProjectionAppSelectorActivity( intent.configureChooserIntent( resources, component.hostUserHandle, - component.personalProfileUserHandle + component.personalProfileUserHandle, ) reviewGrantedConsentRequired = @@ -180,7 +183,13 @@ class MediaProjectionAppSelectorActivity( // is created and ready to be captured. val activityStarted = activityLauncher.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) { - returnSelectedApp(launchCookie, taskId = -1) + if (targetInfo.resolvedComponentName == callingActivity) { + // If attempting to launch the app used to launch the MediaProjection, then + // provide the task id since the launch cookie won't match the existing task + returnSelectedApp(launchCookie, taskId = activityManager.runningTask.taskId) + } else { + returnSelectedApp(launchCookie, taskId = -1) + } } // Rely on the ActivityManager to pop up a dialog regarding app suspension @@ -213,7 +222,7 @@ class MediaProjectionAppSelectorActivity( MediaProjectionServiceHelper.setReviewedConsentIfNeeded( RECORD_CANCEL, reviewGrantedConsentRequired, - /* projection= */ null + /* projection= */ null, ) if (isFinishing) { // Only log dismissed when actually finishing, and not when changing configuration. @@ -246,7 +255,7 @@ class MediaProjectionAppSelectorActivity( val resultReceiver = intent.getParcelableExtra( EXTRA_CAPTURE_REGION_RESULT_RECEIVER, - ResultReceiver::class.java + ResultReceiver::class.java, ) as ResultReceiver val captureRegion = MediaProjectionCaptureTarget(launchCookie, taskId) val data = Bundle().apply { putParcelable(KEY_CAPTURE_TARGET, captureRegion) } @@ -260,8 +269,8 @@ class MediaProjectionAppSelectorActivity( val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION) val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder) - projection.setLaunchCookie(launchCookie) - projection.setTaskId(taskId) + projection.launchCookie = launchCookie + projection.taskId = taskId val intent = Intent() intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder()) @@ -270,7 +279,7 @@ class MediaProjectionAppSelectorActivity( MediaProjectionServiceHelper.setReviewedConsentIfNeeded( RECORD_CONTENT_TASK, reviewGrantedConsentRequired, - projection + projection, ) } @@ -457,7 +466,7 @@ class MediaProjectionAppSelectorActivity( */ private class RecyclerViewExpandingAccessibilityDelegate( rdl: ResolverDrawerLayout, - view: RecyclerView + view: RecyclerView, ) : RecyclerViewAccessibilityDelegate(view) { private val delegate = AppListAccessibilityDelegate(rdl) @@ -465,7 +474,7 @@ class MediaProjectionAppSelectorActivity( override fun onRequestSendAccessibilityEvent( host: ViewGroup, child: View, - event: AccessibilityEvent + event: AccessibilityEvent, ): Boolean { super.onRequestSendAccessibilityEvent(host, child, event) return delegate.onRequestSendAccessibilityEvent(host, child, event) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java index 5ea9e6ae0a70..301ab2bcdd65 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java @@ -311,10 +311,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi mConfig = MobileMappings.Config.readConfig(mContext); mTelephonyManager = mTelephonyManager.createForSubscriptionId(mDefaultDataSubId); mSubIdTelephonyManagerMap.put(mDefaultDataSubId, mTelephonyManager); - InternetTelephonyCallback telephonyCallback = - new InternetTelephonyCallback(mDefaultDataSubId); - mSubIdTelephonyCallbackMap.put(mDefaultDataSubId, telephonyCallback); - mTelephonyManager.registerTelephonyCallback(mExecutor, telephonyCallback); + registerInternetTelephonyCallback(mTelephonyManager, mDefaultDataSubId); // Listen the connectivity changes mConnectivityManager.registerDefaultNetworkCallback(mConnectivityManagerNetworkCallback); mCanConfigWifi = canConfigWifi; @@ -346,6 +343,23 @@ public class InternetDialogController implements AccessPointController.AccessPoi mCallback = null; } + /** + * This is to generate and register the new callback to Telephony for uncached subscription id, + * then cache it. Telephony also cached this callback into + * {@link com.android.server.TelephonyRegistry}, so if subscription id and callback were cached + * already, it shall do nothing to avoid registering redundant callback to Telephony. + */ + private void registerInternetTelephonyCallback( + TelephonyManager telephonyManager, int subId) { + if (mSubIdTelephonyCallbackMap.containsKey(subId)) { + // Avoid to generate and register unnecessary callback to Telephony. + return; + } + InternetTelephonyCallback telephonyCallback = new InternetTelephonyCallback(subId); + mSubIdTelephonyCallbackMap.put(subId, telephonyCallback); + telephonyManager.registerTelephonyCallback(mExecutor, telephonyCallback); + } + boolean isAirplaneModeEnabled() { return mGlobalSettings.getInt(Settings.Global.AIRPLANE_MODE_ON, 0) != 0; } @@ -673,9 +687,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi int subId = subInfo.getSubscriptionId(); if (mSubIdTelephonyManagerMap.get(subId) == null) { TelephonyManager secondaryTm = mTelephonyManager.createForSubscriptionId(subId); - InternetTelephonyCallback telephonyCallback = new InternetTelephonyCallback(subId); - secondaryTm.registerTelephonyCallback(mExecutor, telephonyCallback); - mSubIdTelephonyCallbackMap.put(subId, telephonyCallback); + registerInternetTelephonyCallback(secondaryTm, subId); mSubIdTelephonyManagerMap.put(subId, secondaryTm); } return subId; @@ -1351,6 +1363,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi if (DEBUG) { Log.d(TAG, "DDS: defaultDataSubId:" + defaultDataSubId); } + if (SubscriptionManager.isUsableSubscriptionId(defaultDataSubId)) { // clean up old defaultDataSubId TelephonyCallback oldCallback = mSubIdTelephonyCallbackMap.get(mDefaultDataSubId); @@ -1366,9 +1379,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi // create for new defaultDataSubId mTelephonyManager = mTelephonyManager.createForSubscriptionId(defaultDataSubId); mSubIdTelephonyManagerMap.put(defaultDataSubId, mTelephonyManager); - InternetTelephonyCallback newCallback = new InternetTelephonyCallback(defaultDataSubId); - mSubIdTelephonyCallbackMap.put(defaultDataSubId, newCallback); - mTelephonyManager.registerTelephonyCallback(mHandler::post, newCallback); + registerInternetTelephonyCallback(mTelephonyManager, defaultDataSubId); mCallback.onSubscriptionsChanged(defaultDataSubId); } mDefaultDataSubId = defaultDataSubId; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.kt new file mode 100644 index 000000000000..54a18f764406 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.kt @@ -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.systemui.statusbar.core + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the status bar connected displays flag state. */ +@Suppress("NOTHING_TO_INLINE") +object StatusBarConnectedDisplays { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.statusBarConnectedDisplays() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is enabled. This will throw an exception if + * the flag is not enabled to ensure that the refactor author catches issues in testing. + * Caution!! Using this check incorrectly will cause crashes in nextfood builds! + */ + @JvmStatic + inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt index b342722ebb09..b67092ca9348 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt @@ -22,7 +22,7 @@ import com.android.internal.annotations.VisibleForTesting import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_USE_PEOPLE_FILTERING import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.notification.collection.NotificationClassificationFlag -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING import com.android.systemui.statusbar.notification.stack.BUCKET_FOREGROUND_SERVICE @@ -54,7 +54,7 @@ constructor(val proxy: DeviceConfigProxy, val context: Context) { fun getNotificationBuckets(): IntArray { if ( PriorityPeopleSection.isEnabled || - NotificationMinimalismPrototype.isEnabled || + NotificationMinimalism.isEnabled || NotificationClassificationFlag.isEnabled ) { // We don't need this list to be adaptive, it can be the superset of all features. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt index a621b2a02c5d..4e63b920a73d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt @@ -35,7 +35,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN import com.android.systemui.util.asIndenting @@ -77,7 +77,7 @@ constructor( private var unseenFilterEnabled = false override fun attach(pipeline: NotifPipeline) { - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) { + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) { return } pipeline.addPromoter(unseenNotifPromoter) @@ -129,26 +129,25 @@ constructor( } } - private fun unseenFeatureEnabled(): Flow<Boolean> { - // TODO(b/330387368): create LOCK_SCREEN_NOTIFICATION_MINIMALISM setting to use here? - // Or should we actually just repurpose using the existing setting? - if (NotificationMinimalismPrototype.isEnabled) { - return flowOf(true) + private fun minimalismFeatureSettingEnabled(): Flow<Boolean> { + if (!NotificationMinimalism.isEnabled) { + return flowOf(false) } - return seenNotificationsInteractor.isLockScreenShowOnlyUnseenNotificationsEnabled() + return seenNotificationsInteractor.isLockScreenNotificationMinimalismEnabled() } private suspend fun trackUnseenFilterSettingChanges() { - unseenFeatureEnabled().collectLatest { isSettingEnabled -> + // Only filter the seen notifs when the lock screen minimalism feature settings is on. + minimalismFeatureSettingEnabled().collectLatest { isMinimalismSettingEnabled -> // update local field and invalidate if necessary - if (isSettingEnabled != unseenFilterEnabled) { - unseenFilterEnabled = isSettingEnabled + if (isMinimalismSettingEnabled != unseenFilterEnabled) { + unseenFilterEnabled = isMinimalismSettingEnabled unseenNotifications.clear() unseenNotifPromoter.invalidateList("unseen setting changed") } // if the setting is enabled, then start tracking and filtering unseen notifications - logger.logTrackingUnseen(isSettingEnabled) - if (isSettingEnabled) { + logger.logTrackingUnseen(isMinimalismSettingEnabled) + if (isMinimalismSettingEnabled) { trackSeenNotifications() } } @@ -178,7 +177,7 @@ constructor( } private fun pickOutTopUnseenNotifs(list: List<ListEntry>) { - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return if (!unseenFilterEnabled) return // Only ever elevate a top unseen notification on keyguard, not even locked shade if (statusBarStateController.state != StatusBarState.KEYGUARD) { @@ -215,9 +214,9 @@ constructor( object : NotifPromoter(TAG) { override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean = when { - NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode() -> false + NotificationMinimalism.isUnexpectedlyInLegacyMode() -> false seenNotificationsInteractor.isTopOngoingNotification(child) -> true - !NotificationMinimalismPrototype.ungroupTopUnseen -> false + !NotificationMinimalism.ungroupTopUnseen -> false else -> seenNotificationsInteractor.isTopUnseenNotification(child) } } @@ -225,7 +224,7 @@ constructor( val topOngoingSectioner = object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) { override fun isInSection(entry: ListEntry): Boolean { - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false return entry.anyEntry { notificationEntry -> seenNotificationsInteractor.isTopOngoingNotification(notificationEntry) } @@ -235,7 +234,7 @@ constructor( val topUnseenSectioner = object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) { override fun isInSection(entry: ListEntry): Boolean { - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false return entry.anyEntry { notificationEntry -> seenNotificationsInteractor.isTopUnseenNotification(notificationEntry) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt index e44a77c30999..a4fa72942380 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt @@ -34,7 +34,10 @@ constructor( TAG, LogLevel.DEBUG, messageInitializer = { bool1 = trackingUnseen }, - messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." }, + messagePrinter = { + "${if (bool1) "Start" else "Stop"} " + + "tracking unseen notifications because of settings change." + }, ) fun logShadeVisible(numUnseen: Int) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt index 73ce48b2324a..96c260bb0852 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt @@ -25,7 +25,7 @@ import com.android.systemui.statusbar.notification.collection.SortBySectionTimeF import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection import javax.inject.Inject @@ -88,11 +88,10 @@ constructor( mCoordinators.add(hideLocallyDismissedNotifsCoordinator) mCoordinators.add(hideNotifsForOtherUsersCoordinator) mCoordinators.add(keyguardCoordinator) - if (NotificationMinimalismPrototype.isEnabled) { + if (NotificationMinimalism.isEnabled) { mCoordinators.add(lockScreenMinimalismCoordinator) - } else { - mCoordinators.add(unseenKeyguardCoordinator) } + mCoordinators.add(unseenKeyguardCoordinator) mCoordinators.add(rankingCoordinator) mCoordinators.add(colorizedFgsCoordinator) mCoordinators.add(deviceProvisionedCoordinator) @@ -125,11 +124,11 @@ constructor( } // Manually add Ordered Sections - if (NotificationMinimalismPrototype.isEnabled) { + if (NotificationMinimalism.isEnabled) { mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing } mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp - if (NotificationMinimalismPrototype.isEnabled) { + if (NotificationMinimalism.isEnabled) { mOrderedSections.add(lockScreenMinimalismCoordinator.topUnseenSectioner) // Top Unseen } mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt index bfea2ba6b839..cf1329c6b564 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt @@ -35,7 +35,6 @@ import com.android.systemui.statusbar.notification.collection.coordinator.dagger import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.headsUpEvents import com.android.systemui.util.asIndenting @@ -51,7 +50,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -87,7 +85,6 @@ constructor( private var unseenFilterEnabled = false override fun attach(pipeline: NotifPipeline) { - NotificationMinimalismPrototype.assertInLegacyMode() pipeline.addFinalizeFilter(unseenNotifFilter) pipeline.addCollectionListener(collectionListener) scope.launch { trackUnseenFilterSettingChanges() } @@ -253,10 +250,6 @@ constructor( } private fun unseenFeatureEnabled(): Flow<Boolean> { - if (NotificationMinimalismPrototype.isEnabled) { - // TODO(b/330387368): should this really just be turned off? If so, hide the setting. - return flowOf(false) - } return seenNotificationsInteractor.isLockScreenShowOnlyUnseenNotificationsEnabled() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java index 6d0148a24cf8..41419f31eb7a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java @@ -41,7 +41,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager; import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider; import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor; -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype; +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.kotlin.BooleanFlowOperators; @@ -170,7 +170,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { if (entry == null) { return false; } - boolean isTopUnseen = NotificationMinimalismPrototype.isEnabled() + boolean isTopUnseen = NotificationMinimalism.isEnabled() && (mSeenNotificationsInteractor.isTopUnseenNotification(entry) || mSeenNotificationsInteractor.isTopOngoingNotification(entry)); if (isTopUnseen || mHeadsUpManager.isHeadsUpEntry(entry.getKey())) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt index 29564326481f..1babe47559e5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt @@ -23,7 +23,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.util.printSection import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow @@ -57,25 +57,25 @@ constructor( /** Set the entry that is identified as the top ongoing notification. */ fun setTopOngoingNotification(entry: NotificationEntry?) { - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return notificationListRepository.topOngoingNotificationKey.value = entry?.key } /** Determine if the given notification is the top ongoing notification. */ fun isTopOngoingNotification(entry: NotificationEntry?): Boolean = - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) false else entry != null && notificationListRepository.topOngoingNotificationKey.value == entry.key /** Set the entry that is identified as the top unseen notification. */ fun setTopUnseenNotification(entry: NotificationEntry?) { - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return notificationListRepository.topUnseenNotificationKey.value = entry?.key } /** Determine if the given notification is the top unseen notification. */ fun isTopUnseenNotification(entry: NotificationEntry?): Boolean = - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) false else entry != null && notificationListRepository.topUnseenNotificationKey.value == entry.key fun dump(pw: IndentingPrintWriter) = @@ -120,4 +120,29 @@ constructor( // only track the most recent emission, if events are happening faster than they can be // consumed .conflate() + + fun isLockScreenNotificationMinimalismEnabled(): Flow<Boolean> = + secureSettings + // emit whenever the setting has changed + .observerFlow( + UserHandle.USER_ALL, + Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, + ) + // perform a query immediately + .onStart { emit(Unit) } + // for each change, lookup the new value + .map { + secureSettings.getIntForUser( + name = Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, + default = 1, + userHandle = UserHandle.USER_CURRENT, + ) == 1 + } + // don't emit anything if nothing has changed + .distinctUntilChanged() + // perform lookups on the bg thread pool + .flowOn(bgDispatcher) + // only track the most recent emission, if events are happening faster than they can be + // consumed + .conflate() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt index 06f3db504aaf..70bb2722c678 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt @@ -17,23 +17,23 @@ package com.android.systemui.statusbar.notification.shared import android.os.SystemProperties -import com.android.systemui.Flags +import com.android.server.notification.Flags import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils /** Helper for reading or using the minimalism prototype flag state. */ @Suppress("NOTHING_TO_INLINE") -object NotificationMinimalismPrototype { - const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE +object NotificationMinimalism { + const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM /** A token used for dependency declaration */ val token: FlagToken get() = FlagToken(FLAG_NAME, isEnabled) - /** Is the heads-up cycling animation enabled */ + /** Is the notification minimalism enabled */ @JvmStatic inline val isEnabled - get() = Flags.notificationMinimalismPrototype() + get() = Flags.notificationMinimalism() /** * The prototype will (by default) use a promoter to ensure that the top unseen notification is diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt index 06222fdb2761..3bc549543ef2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt @@ -29,7 +29,7 @@ import com.android.systemui.statusbar.StatusBarState.KEYGUARD import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.Compile import com.android.systemui.util.children @@ -74,7 +74,7 @@ constructor( /** Whether we allow keyguard to show less important notifications above the shelf. */ private val limitLockScreenToOneImportant - get() = NotificationMinimalismPrototype.isEnabled + get() = NotificationMinimalism.isEnabled /** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */ private var dividerHeight by notNull<Float>() @@ -406,7 +406,7 @@ constructor( fun updateResources() { maxKeyguardNotifications = infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count)) - maxNotificationsExcludesMedia = NotificationMinimalismPrototype.isEnabled + maxNotificationsExcludesMedia = NotificationMinimalism.isEnabled dividerHeight = max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat()) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt index e1dcc524c486..78edd3916a88 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt @@ -19,8 +19,10 @@ package com.android.systemui.statusbar.policy.ui.dialog import android.content.Intent import android.provider.Settings import android.util.Log +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource @@ -30,6 +32,7 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.android.compose.PlatformButton import com.android.compose.PlatformOutlinedButton +import com.android.compose.theme.PlatformTheme import com.android.internal.annotations.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.DialogCuj @@ -74,7 +77,7 @@ constructor( currentDialog?.dismiss() } - currentDialog = sysuiDialogFactory.create() { ModesDialogContent(it) } + currentDialog = sysuiDialogFactory.create { ModesDialogContent(it) } currentDialog ?.lifecycle ?.addObserver( @@ -91,28 +94,34 @@ constructor( @Composable private fun ModesDialogContent(dialog: SystemUIDialog) { - AlertDialogContent( - modifier = Modifier.semantics { - testTagsAsResourceId = true - }, - title = { - Text( - modifier = Modifier.testTag("modes_title"), - text = stringResource(R.string.zen_modes_dialog_title) - ) - }, - content = { ModeTileGrid(viewModel.get()) }, - neutralButton = { - PlatformOutlinedButton(onClick = { openSettings(dialog) }) { - Text(stringResource(R.string.zen_modes_dialog_settings)) - } - }, - positiveButton = { - PlatformButton(onClick = { dialog.dismiss() }) { - Text(stringResource(R.string.zen_modes_dialog_done)) - } - }, - ) + // TODO(b/369376884): The composable does correctly update when the theme changes + // while the dialog is open, but the background (which we don't control here) + // doesn't, which causes us to show things like white text on a white background. + // as a workaround, we remember the original theme and keep it on recomposition. + val isCurrentlyInDarkTheme = isSystemInDarkTheme() + val cachedDarkTheme = remember { isCurrentlyInDarkTheme } + PlatformTheme(isDarkTheme = cachedDarkTheme) { + AlertDialogContent( + modifier = Modifier.semantics { testTagsAsResourceId = true }, + title = { + Text( + modifier = Modifier.testTag("modes_title"), + text = stringResource(R.string.zen_modes_dialog_title), + ) + }, + content = { ModeTileGrid(viewModel.get()) }, + neutralButton = { + PlatformOutlinedButton(onClick = { openSettings(dialog) }) { + Text(stringResource(R.string.zen_modes_dialog_settings)) + } + }, + positiveButton = { + PlatformButton(onClick = { dialog.dismiss() }) { + Text(stringResource(R.string.zen_modes_dialog_done)) + } + }, + ) + } } @VisibleForTesting @@ -129,7 +138,7 @@ constructor( activityStarter.startActivity( ZEN_MODE_SETTINGS_INTENT, true /* dismissShade */, - animationController + animationController, ) } @@ -163,7 +172,7 @@ constructor( Log.w( TAG, "Cannot launch from dialog, the dialog is not present. " + - "Will launch activity without animating." + "Will launch activity without animating.", ) } @@ -172,11 +181,7 @@ constructor( if (animationController == null) { currentDialog?.dismiss() } - activityStarter.startActivity( - intent, - true, /* dismissShade */ - animationController, - ) + activityStarter.startActivity(intent, true, /* dismissShade */ animationController) } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt index 680df1584f89..dcf32a5f574d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt @@ -137,7 +137,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { MediaTestUtils.emptyMediaData.copy( app = PACKAGE, packageName = PACKAGE, - token = session.sessionToken + token = session.sessionToken, ) resumeData = mediaData.copy(token = null, active = false, resumption = true) @@ -237,7 +237,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // Assuming we're registered testOnMediaDataLoaded_registersPlaybackListener() - mediaCallbackCaptor.value.onPlaybackStateChanged( + onPlaybackStateChanged( PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() ) assertThat(mainExecutor.numPending()).isEqualTo(1) @@ -249,7 +249,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // Assuming we have a pending timeout testOnPlaybackStateChanged_schedulesTimeout_whenPaused() - mediaCallbackCaptor.value.onPlaybackStateChanged( + onPlaybackStateChanged( PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build() ) assertThat(mainExecutor.numPending()).isEqualTo(0) @@ -261,7 +261,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // Assuming we have a pending timeout testOnPlaybackStateChanged_schedulesTimeout_whenPaused() - mediaCallbackCaptor.value.onPlaybackStateChanged( + onPlaybackStateChanged( PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build() ) assertThat(mainExecutor.numPending()).isEqualTo(1) @@ -435,7 +435,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // When the playback state changes, and has different actions val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build() - mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) + onPlaybackStateChanged(playingState) assertThat(uiExecutor.runAllReady()).isEqualTo(1) // Then the callback is invoked @@ -448,7 +448,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { PlaybackState.CustomAction.Builder( "ACTION_1", "custom action 1", - android.R.drawable.ic_media_ff + android.R.drawable.ic_media_ff, ) .build() val pausedState = @@ -463,7 +463,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { PlaybackState.CustomAction.Builder( "ACTION_2", "custom action 2", - android.R.drawable.ic_media_rew + android.R.drawable.ic_media_rew, ) .build() val pausedStateTwoActions = @@ -472,7 +472,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { .addCustomAction(customOne) .addCustomAction(customTwo) .build() - mediaCallbackCaptor.value.onPlaybackStateChanged(pausedStateTwoActions) + onPlaybackStateChanged(pausedStateTwoActions) assertThat(uiExecutor.runAllReady()).isEqualTo(1) // Then the callback is invoked @@ -485,7 +485,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { loadMediaDataWithPlaybackState(stateWithActions) // When the playback state updates with the same actions - mediaCallbackCaptor.value.onPlaybackStateChanged(stateWithActions) + onPlaybackStateChanged(stateWithActions) // Then the callback is not invoked again verify(stateCallback, never()).invoke(eq(KEY), any()) @@ -512,7 +512,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { .setActions(PlaybackState.ACTION_PAUSE) .addCustomAction(customTwo) .build() - mediaCallbackCaptor.value.onPlaybackStateChanged(stateTwo) + onPlaybackStateChanged(stateTwo) // Then the callback is not invoked verify(stateCallback, never()).invoke(eq(KEY), any()) @@ -544,7 +544,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // When the playback state changes to playing val playingState = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build() - mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) + onPlaybackStateChanged(playingState) uiExecutor.runAllReady() // Then the callback is invoked @@ -561,7 +561,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // When the playback state is updated, but still not playing val playingState = PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build() - mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) + onPlaybackStateChanged(playingState) // Then the callback is not invoked verify(stateCallback, never()).invoke(eq(KEY), eq(playingState!!)) @@ -571,7 +571,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() { // When paused media is loaded testOnMediaDataLoaded_registersPlaybackListener() - mediaCallbackCaptor.value.onPlaybackStateChanged( + onPlaybackStateChanged( PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() ) verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor)) @@ -597,7 +597,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { val time = clock.currentTimeMillis() clock.setElapsedRealtime(time) testOnMediaDataLoaded_registersPlaybackListener() - mediaCallbackCaptor.value.onPlaybackStateChanged( + onPlaybackStateChanged( PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() ) verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor)) @@ -706,4 +706,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() { bgExecutor.runAllReady() uiExecutor.runAllReady() } + + private fun onPlaybackStateChanged(state: PlaybackState) { + mediaCallbackCaptor.value.onPlaybackStateChanged(state) + bgExecutor.runAllReady() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java index eea02eec7099..2f8f45cb0197 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -887,6 +888,34 @@ public class InternetDialogDelegateControllerTest extends SysuiTestCase { } @Test + public void getActiveAutoSwitchNonDdsSubId_registerCallbackForExistedSubId_notRegister() { + mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true); + + // Adds non DDS subId + SubscriptionInfo info = mock(SubscriptionInfo.class); + doReturn(SUB_ID2).when(info).getSubscriptionId(); + doReturn(false).when(info).isOpportunistic(); + when(mSubscriptionManager.getActiveSubscriptionInfo(anyInt())).thenReturn(info); + + mInternetDialogController.getActiveAutoSwitchNonDdsSubId(); + + // 1st time is onStart(), 2nd time is getActiveAutoSwitchNonDdsSubId() + verify(mTelephonyManager, times(2)).registerTelephonyCallback(any(), any()); + assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size() == 2); + + // Adds non DDS subId again + doReturn(SUB_ID2).when(info).getSubscriptionId(); + doReturn(false).when(info).isOpportunistic(); + when(mSubscriptionManager.getActiveSubscriptionInfo(anyInt())).thenReturn(info); + + mInternetDialogController.getActiveAutoSwitchNonDdsSubId(); + + // Does not add due to cached subInfo in mSubIdTelephonyCallbackMap. + verify(mTelephonyManager, times(2)).registerTelephonyCallback(any(), any()); + assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size() == 2); + } + + @Test public void getMobileNetworkSummary() { mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true); Resources res1 = mock(Resources.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt index 0407fc14d35a..ac7388281a15 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt @@ -24,7 +24,7 @@ import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_USE_PEOPLE_FILTERING import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection import com.android.systemui.util.DeviceConfigProxyFake import com.android.systemui.util.Utils @@ -42,7 +42,7 @@ import org.mockito.quality.Strictness @RunWith(AndroidJUnit4::class) @SmallTest // this class has no testable logic with either of these flags enabled -@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.FLAG_NAME) +@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalism.FLAG_NAME) class NotificationSectionsFeatureManagerTest : SysuiTestCase() { lateinit var manager: NotificationSectionsFeatureManager private val proxyFake = DeviceConfigProxyFake() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt index e2b283b06562..38bc758232a2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt @@ -22,7 +22,6 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.powerInteractor -import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -34,7 +33,6 @@ val Kosmos.keyguardDismissActionInteractor by transitionInteractor = keyguardTransitionInteractor, dismissInteractor = keyguardDismissInteractor, applicationScope = testScope.backgroundScope, - sceneInteractor = { sceneInteractor }, deviceUnlockedInteractor = { deviceUnlockedInteractor }, powerInteractor = powerInteractor, alternateBouncerInteractor = alternateBouncerInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt index b19e221d099c..3d2bd6cf49d4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt @@ -45,3 +45,17 @@ var Kosmos.lockScreenShowOnlyUnseenNotificationsSetting: Boolean UserHandle.USER_CURRENT, ) } + +var Kosmos.lockScreenNotificationMinimalismSetting: Boolean + get() = + fakeSettings.getIntForUser( + Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, + UserHandle.USER_CURRENT, + ) == 1 + set(value) { + fakeSettings.putIntForUser( + Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, + if (value) 1 else 0, + UserHandle.USER_CURRENT, + ) + } diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index c8f8c2a6b223..082459b8c863 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -21,11 +21,13 @@ import static com.android.server.appfunctions.AppFunctionExecutors.THREAD_POOL_E import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.WorkerThread; +import android.app.appfunctions.AppFunctionService; import android.app.appfunctions.AppFunctionStaticMetadataHelper; import android.app.appfunctions.ExecuteAppFunctionAidlRequest; import android.app.appfunctions.ExecuteAppFunctionResponse; import android.app.appfunctions.IAppFunctionManager; import android.app.appfunctions.IAppFunctionService; +import android.app.appfunctions.ICancellationCallback; import android.app.appfunctions.IExecuteAppFunctionCallback; import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback; import android.app.appsearch.AppSearchManager; @@ -37,11 +39,15 @@ import android.app.appsearch.observer.SchemaChangeInfo; import android.content.Context; import android.content.Intent; import android.os.Binder; +import android.os.CancellationSignal; +import android.os.IBinder; +import android.os.ICancellationSignal; import android.os.UserHandle; import android.text.TextUtils; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.infra.AndroidFuture; import com.android.server.SystemService.TargetUser; import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback; import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener; @@ -99,7 +105,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { } @Override - public void executeAppFunction( + public ICancellationSignal executeAppFunction( @NonNull ExecuteAppFunctionAidlRequest requestInternal, @NonNull IExecuteAppFunctionCallback executeAppFunctionCallback) { Objects.requireNonNull(requestInternal); @@ -120,11 +126,14 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { ExecuteAppFunctionResponse.RESULT_DENIED, exception.getMessage(), /* extras= */ null)); - return; + return null; } int callingUid = Binder.getCallingUid(); - int callingPid = Binder.getCallingUid(); + int callingPid = Binder.getCallingPid(); + + ICancellationSignal localCancelTransport = CancellationSignal.createTransport(); + THREAD_POOL_EXECUTOR.execute( () -> { try { @@ -132,12 +141,14 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { requestInternal, callingUid, callingPid, + localCancelTransport, safeExecuteAppFunctionCallback); } catch (Exception e) { safeExecuteAppFunctionCallback.onResult( mapExceptionToExecuteAppFunctionResponse(e)); } }); + return localCancelTransport; } @WorkerThread @@ -145,6 +156,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { ExecuteAppFunctionAidlRequest requestInternal, int callingUid, int callingPid, + ICancellationSignal localCancelTransport, SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) { UserHandle targetUser = requestInternal.getUserHandle(); // TODO(b/354956319): Add and honor the new enterprise policies. @@ -203,6 +215,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { requestInternal, serviceIntent, targetUser, + localCancelTransport, safeExecuteAppFunctionCallback, /* bindFlags= */ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE); @@ -219,8 +232,19 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { @NonNull ExecuteAppFunctionAidlRequest requestInternal, @NonNull Intent serviceIntent, @NonNull UserHandle targetUser, + @NonNull ICancellationSignal cancellationSignalTransport, @NonNull SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback, int bindFlags) { + CancellationSignal cancellationSignal = + CancellationSignal.fromTransport(cancellationSignalTransport); + ICancellationCallback cancellationCallback = + new ICancellationCallback.Stub() { + @Override + public void sendCancellationTransport( + @NonNull ICancellationSignal cancellationTransport) { + cancellationSignal.setRemote(cancellationTransport); + } + }; boolean bindServiceResult = mRemoteServiceCaller.runServiceCall( serviceIntent, @@ -236,6 +260,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { try { service.executeAppFunction( requestInternal.getClientRequest(), + cancellationCallback, new IExecuteAppFunctionCallback.Stub() { @Override public void onResult( diff --git a/services/core/java/android/os/BatteryStatsInternal.java b/services/core/java/android/os/BatteryStatsInternal.java index 0713999d4354..60b826b50045 100644 --- a/services/core/java/android/os/BatteryStatsInternal.java +++ b/services/core/java/android/os/BatteryStatsInternal.java @@ -41,6 +41,7 @@ public abstract class BatteryStatsInternal { public static final int CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER = 3; public static final int CPU_WAKEUP_SUBSYSTEM_SENSOR = 4; public static final int CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA = 5; + public static final int CPU_WAKEUP_SUBSYSTEM_BLUETOOTH = 6; /** @hide */ @IntDef(prefix = {"CPU_WAKEUP_SUBSYSTEM_"}, value = { @@ -50,6 +51,7 @@ public abstract class BatteryStatsInternal { CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER, CPU_WAKEUP_SUBSYSTEM_SENSOR, CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA, + CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, }) @Retention(RetentionPolicy.SOURCE) public @interface CpuWakeupSubsystem { @@ -99,6 +101,14 @@ public abstract class BatteryStatsInternal { public abstract void noteCpuWakingNetworkPacket(Network network, long elapsedMillis, int uid); /** + * Informs battery stats of a sysproxy packet that woke up the CPU + * + * @param uid The uid that received the packet. + * @param elapsedMillis The time of the packet's arrival in elapsed timebase. + */ + public abstract void noteCpuWakingBluetoothProxyPacket(int uid, long elapsedMillis); + + /** * Informs battery stats of binder stats for the given work source UID. */ public abstract void noteBinderCallStats(int workSourceUid, long incrementalBinderCallCount, diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index d86bae19f174..e64a4803b14f 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -219,7 +219,7 @@ class StorageManagerService extends IStorageManager.Stub public static final int FAILED_MOUNT_RESET_TIMEOUT_SECONDS = 10; /** Extended timeout for the system server watchdog. */ - private static final int SLOW_OPERATION_WATCHDOG_TIMEOUT_MS = 20 * 1000; + private static final int SLOW_OPERATION_WATCHDOG_TIMEOUT_MS = 30 * 1000; /** Extended timeout for the system server watchdog for vold#partition operation. */ private static final int PARTITION_OPERATION_WATCHDOG_TIMEOUT_MS = 3 * 60 * 1000; @@ -3251,7 +3251,7 @@ class StorageManagerService extends IStorageManager.Stub if (Binder.getCallingUid() != android.os.Process.SYSTEM_UID) { throw new SecurityException("no permission to commit checkpoint changes"); } - + extendWatchdogTimeout("vold#commitChanges might be slow"); mVold.commitChanges(); } diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 15277cebac6e..ef82c7477558 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -664,6 +664,8 @@ public final class BatteryStatsService extends IBatteryStats.Stub } else if (nc.hasTransport(TRANSPORT_CELLULAR)) { return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA; } + // For TRANSPORT_BLUETOOTH, we have a separate channel to catch Bluetooth wakeups. + // See noteCpuWakingSysproxyPacket method. return CPU_WAKEUP_SUBSYSTEM_UNKNOWN; } @@ -686,6 +688,15 @@ public final class BatteryStatsService extends IBatteryStats.Stub } @Override + public void noteCpuWakingBluetoothProxyPacket(int uid, long elapsedMillis) { + if (uid < 0) { + Slog.e(TAG, "Invalid uid for waking bluetooth proxy packet: " + uid); + return; + } + noteCpuWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, elapsedMillis, uid); + } + + @Override public void noteBinderCallStats(int workSourceUid, long incrementatCallCount, Collection<BinderCallsStats.CallStat> callStats) { synchronized (BatteryStatsService.this.mLock) { diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index a6389f7f5311..e0cf96fbccd0 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -1031,6 +1031,9 @@ public class AppOpsService extends IAppOpsService.Stub { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); + if (action == null) { + return; + } String pkgName = intent.getData().getEncodedSchemeSpecificPart().intern(); int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID); diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 88907e35854f..1f9eb082aaf4 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -3377,10 +3377,18 @@ public final class DisplayManagerService extends SystemService { private void dumpInternal(PrintWriter pw) { pw.println("DISPLAY MANAGER (dumpsys display)"); BrightnessTracker brightnessTrackerLocal; + SparseArray<DisplayPowerController> displayPowerControllersLocal = new SparseArray<>(); + int displayPowerControllerCount; synchronized (mSyncRoot) { brightnessTrackerLocal = mBrightnessTracker; + displayPowerControllerCount = mDisplayPowerControllers.size(); + for (int i = 0; i < displayPowerControllerCount; i++) { + displayPowerControllersLocal.put( + mDisplayPowerControllers.keyAt(i), mDisplayPowerControllers.valueAt(i)); + } + pw.println(" mSafeMode=" + mSafeMode); pw.println(" mPendingTraversal=" + mPendingTraversal); pw.println(" mViewports=" + mViewports); @@ -3451,13 +3459,6 @@ public final class DisplayManagerService extends SystemService { + ", mWifiDisplayScanRequested=" + callback.mWifiDisplayScanRequested); } - final int displayPowerControllerCount = mDisplayPowerControllers.size(); - pw.println(); - pw.println("Display Power Controllers: size=" + displayPowerControllerCount); - for (int i = 0; i < displayPowerControllerCount; i++) { - mDisplayPowerControllers.valueAt(i).dump(pw); - } - pw.println(); mPersistentDataStore.dump(pw); @@ -3470,6 +3471,12 @@ public final class DisplayManagerService extends SystemService { mDisplayWindowPolicyControllers.valueAt(i).second.dump(" ", pw); } } + pw.println(); + pw.println("Display Power Controllers: size=" + displayPowerControllerCount); + for (int i = 0; i < displayPowerControllerCount; i++) { + displayPowerControllersLocal.valueAt(i).dump(pw); + } + if (brightnessTrackerLocal != null) { pw.println(); brightnessTrackerLocal.dump(pw); diff --git a/services/core/java/com/android/server/flags/pinner.aconfig b/services/core/java/com/android/server/flags/pinner.aconfig index 2f817dbb9a7f..345366882d5a 100644 --- a/services/core/java/com/android/server/flags/pinner.aconfig +++ b/services/core/java/com/android/server/flags/pinner.aconfig @@ -9,8 +9,11 @@ flag { } flag { - name: "skip_home_art_pins" - namespace: "system_performance" - description: "Ablation study flag that controls if home app odex/vdex files should be pinned in memory." - bug: "340935152" -}
\ No newline at end of file + name: "pin_global_quota" + namespace: "system_performance" + description: "This flag controls whether pinner will use a global quota or not" + bug: "340935152" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index 48d24f2e14dd..47f579db604f 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -195,10 +195,9 @@ public final class MediaProjectionManagerService extends SystemService == PackageManager.PERMISSION_GRANTED) { return true; } - boolean operationActive = mAppOps.isOperationActive(AppOpsManager.OP_PROJECT_MEDIA, - mProjectionGrant.uid, - mProjectionGrant.packageName); - if (operationActive) { + if (AppOpsManager.MODE_ALLOWED == mAppOps.noteOpNoThrow(AppOpsManager.OP_PROJECT_MEDIA, + mProjectionGrant.uid, mProjectionGrant.packageName, /* attributionTag= */ null, + "recording lockscreen")) { // Some tools use media projection by granting the OP_PROJECT_MEDIA app // op via a shell command. Those tools can be granted keyguard capture return true; diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index c3a714b0eef0..79633f19715b 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -2579,6 +2579,7 @@ public class NotificationManagerService extends SystemService { mNotificationChannelLogger, mAppOps, mUserProfiles, + mUgmInternal, mShowReviewPermissionsNotification, Clock.systemUTC()); mRankingHelper = new RankingHelper(getContext(), mRankingHandler, mPreferencesHelper, @@ -6672,13 +6673,7 @@ public class NotificationManagerService extends SystemService { final Uri originalSoundUri = (originalChannel != null) ? originalChannel.getSound() : null; if (soundUri != null && !Objects.equals(originalSoundUri, soundUri)) { - Binder.withCleanCallingIdentity(() -> { - mUgmInternal.checkGrantUriPermission(sourceUid, null, - ContentProvider.getUriWithoutUserId(soundUri), - Intent.FLAG_GRANT_READ_URI_PERMISSION, - ContentProvider.getUserIdFromUri(soundUri, - UserHandle.getUserId(sourceUid))); - }); + PermissionHelper.grantUriPermission(mUgmInternal, soundUri, sourceUid); } } diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index b9f0968b5864..3ba93845a290 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -1493,14 +1493,23 @@ public final class NotificationRecord { final Notification notification = getNotification(); notification.visitUris((uri) -> { - visitGrantableUri(uri, false, false); + if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) { + visitGrantableUri(uri, false, false); + } else { + oldVisitGrantableUri(uri, false, false); + } }); if (notification.getChannelId() != null) { NotificationChannel channel = getChannel(); if (channel != null) { - visitGrantableUri(channel.getSound(), (channel.getUserLockedFields() - & NotificationChannel.USER_LOCKED_SOUND) != 0, true); + if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) { + visitGrantableUri(channel.getSound(), (channel.getUserLockedFields() + & NotificationChannel.USER_LOCKED_SOUND) != 0, true); + } else { + oldVisitGrantableUri(channel.getSound(), (channel.getUserLockedFields() + & NotificationChannel.USER_LOCKED_SOUND) != 0, true); + } } } } finally { @@ -1516,7 +1525,7 @@ public final class NotificationRecord { * {@link #mGrantableUris}. Otherwise, this will either log or throw * {@link SecurityException} depending on target SDK of enqueuing app. */ - private void visitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) { + private void oldVisitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) { if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return; if (mGrantableUris != null && mGrantableUris.contains(uri)) { @@ -1555,6 +1564,45 @@ public final class NotificationRecord { } } + /** + * Note the presence of a {@link Uri} that should have permission granted to + * whoever will be rendering it. + * <p> + * If the enqueuing app has the ability to grant access, it will be added to + * {@link #mGrantableUris}. Otherwise, this will either log or throw + * {@link SecurityException} depending on target SDK of enqueuing app. + */ + private void visitGrantableUri(Uri uri, boolean userOverriddenUri, + boolean isSound) { + if (mGrantableUris != null && mGrantableUris.contains(uri)) { + return; // already verified this URI + } + + final int sourceUid = getSbn().getUid(); + try { + PermissionHelper.grantUriPermission(mUgmInternal, uri, sourceUid); + + if (mGrantableUris == null) { + mGrantableUris = new ArraySet<>(); + } + mGrantableUris.add(uri); + } catch (SecurityException e) { + if (!userOverriddenUri) { + if (isSound) { + mSound = Settings.System.DEFAULT_NOTIFICATION_URI; + Log.w(TAG, "Replacing " + uri + " from " + sourceUid + ": " + e.getMessage()); + } else { + if (mTargetSdkVersion >= Build.VERSION_CODES.P) { + throw e; + } else { + Log.w(TAG, + "Ignoring " + uri + " from " + sourceUid + ": " + e.getMessage()); + } + } + } + } + } + public LogMaker getLogMaker(long now) { LogMaker lm = getSbn().getLogMaker() .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_CHANNEL_IMPORTANCE, mImportance) diff --git a/services/core/java/com/android/server/notification/PermissionHelper.java b/services/core/java/com/android/server/notification/PermissionHelper.java index b6f48890c528..1464d481311a 100644 --- a/services/core/java/com/android/server/notification/PermissionHelper.java +++ b/services/core/java/com/android/server/notification/PermissionHelper.java @@ -25,19 +25,25 @@ import android.Manifest; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.companion.virtual.VirtualDeviceManager; +import android.content.ContentProvider; +import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; +import android.net.Uri; import android.os.Binder; import android.os.RemoteException; +import android.os.UserHandle; import android.permission.IPermissionManager; import android.util.ArrayMap; import android.util.Pair; import android.util.Slog; import com.android.internal.util.ArrayUtils; +import com.android.server.uri.UriGrantsManagerInternal; import java.util.Collections; import java.util.HashSet; @@ -58,7 +64,7 @@ public final class PermissionHelper { private final IPermissionManager mPermManager; public PermissionHelper(Context context, IPackageManager packageManager, - IPermissionManager permManager) { + IPermissionManager permManager) { mContext = context; mPackageManager = packageManager; mPermManager = permManager; @@ -298,6 +304,19 @@ public final class PermissionHelper { return false; } + static void grantUriPermission(final UriGrantsManagerInternal ugmInternal, Uri uri, + int sourceUid) { + if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return; + + Binder.withCleanCallingIdentity(() -> { + // This will throw a SecurityException if the caller can't grant. + ugmInternal.checkGrantUriPermission(sourceUid, null, + ContentProvider.getUriWithoutUserId(uri), + Intent.FLAG_GRANT_READ_URI_PERMISSION, + ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid))); + }); + } + public static class PackagePermission { public final String packageName; public final @UserIdInt int userId; diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 85c395781d0a..9e70f815dff9 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -94,6 +94,7 @@ import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.notification.PermissionHelper.PackagePermission; +import com.android.server.uri.UriGrantsManagerInternal; import org.json.JSONArray; import org.json.JSONException; @@ -219,6 +220,7 @@ public class PreferencesHelper implements RankingConfig { private final NotificationChannelLogger mNotificationChannelLogger; private final AppOpsManager mAppOps; private final ManagedServices.UserProfiles mUserProfiles; + private final UriGrantsManagerInternal mUgmInternal; private SparseBooleanArray mBadgingEnabled; private SparseBooleanArray mBubblesEnabled; @@ -239,6 +241,7 @@ public class PreferencesHelper implements RankingConfig { ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager, NotificationChannelLogger notificationChannelLogger, AppOpsManager appOpsManager, ManagedServices.UserProfiles userProfiles, + UriGrantsManagerInternal ugmInternal, boolean showReviewPermissionsNotification, Clock clock) { mContext = context; mZenModeHelper = zenHelper; @@ -249,6 +252,7 @@ public class PreferencesHelper implements RankingConfig { mNotificationChannelLogger = notificationChannelLogger; mAppOps = appOpsManager; mUserProfiles = userProfiles; + mUgmInternal = ugmInternal; mShowReviewPermissionsNotification = showReviewPermissionsNotification; mIsMediaNotificationFilteringEnabled = context.getResources() .getBoolean(R.bool.config_quickSettingsShowMediaPlayer); @@ -1169,6 +1173,13 @@ public class PreferencesHelper implements RankingConfig { } clearLockedFieldsLocked(channel); + // Verify that the app has permission to read the sound Uri + // Only check for new channels, as regular apps can only set sound + // before creating. See: {@link NotificationChannel#setSound} + if (Flags.notificationVerifyChannelSoundUri()) { + PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid); + } + channel.setImportanceLockedByCriticalDeviceFunction( r.defaultAppLockedImportance || r.fixedImportance); diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index be3adc142fa4..0b34177d7413 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -144,6 +144,13 @@ flag { } flag { + name: "notification_minimalism" + namespace: "systemui" + description: "Minimize the notifications to show on the lockscreen." + bug: "330387368" +} + +flag { name: "notification_force_group_singletons" namespace: "systemui" description: "This flag enables forced auto-grouping singleton groups" @@ -163,3 +170,13 @@ flag { description: "This flag enables sound uri with vibration source" bug: "358524009" } + +flag { + name: "notification_verify_channel_sound_uri" + namespace: "systemui" + description: "Verify Uri permission for sound when creating a notification channel" + bug: "337775777" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/pinner/PinRangeSource.java b/services/core/java/com/android/server/pinner/PinRangeSource.java new file mode 100644 index 000000000000..5f9641122294 --- /dev/null +++ b/services/core/java/com/android/server/pinner/PinRangeSource.java @@ -0,0 +1,27 @@ +/* + * 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.pinner; + +/* package */ abstract class PinRangeSource { + /** + * Retrieve a range to pin. + * + * @param outPinRange Receives the pin region + * @return True if we filled in outPinRange or false if we're out of pin entries + */ + abstract boolean read(PinnerService.PinRange outPinRange); +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java b/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java new file mode 100644 index 000000000000..d6fc48790883 --- /dev/null +++ b/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java @@ -0,0 +1,37 @@ +/* + * 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.pinner; + +/* package */ class PinRangeSourceStatic extends PinRangeSource { + private final int mPinStart; + private final int mPinLength; + private boolean mDone = false; + + PinRangeSourceStatic(int pinStart, int pinLength) { + mPinStart = pinStart; + mPinLength = pinLength; + } + + @Override + boolean read(PinnerService.PinRange outPinRange) { + outPinRange.start = mPinStart; + outPinRange.length = mPinLength; + boolean done = mDone; + mDone = true; + return !done; + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/pinner/PinRangeSourceStream.java b/services/core/java/com/android/server/pinner/PinRangeSourceStream.java new file mode 100644 index 000000000000..79900b9de463 --- /dev/null +++ b/services/core/java/com/android/server/pinner/PinRangeSourceStream.java @@ -0,0 +1,43 @@ +/* + * 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.pinner; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; + +/* package */ final class PinRangeSourceStream extends PinRangeSource { + private final DataInputStream mStream; + private boolean mDone = false; + + PinRangeSourceStream(InputStream stream) { + mStream = new DataInputStream(stream); + } + + @Override + boolean read(PinnerService.PinRange outPinRange) { + if (!mDone) { + try { + outPinRange.start = mStream.readInt(); + outPinRange.length = mStream.readInt(); + } catch (IOException ex) { + mDone = true; + } + } + return !mDone; + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/pinner/PinnedFile.java b/services/core/java/com/android/server/pinner/PinnedFile.java new file mode 100644 index 000000000000..a8de344d10af --- /dev/null +++ b/services/core/java/com/android/server/pinner/PinnedFile.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.pinner; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; + +@VisibleForTesting +public final class PinnedFile implements AutoCloseable { + private long mAddress; + final long mapSize; + final String fileName; + public final long bytesPinned; + + // Whether this file was pinned using a pinlist + boolean used_pinlist; + + // User defined group name for pinner accounting + String groupName = ""; + ArrayList<PinnedFile> pinnedDeps = new ArrayList<>(); + + public PinnedFile(long address, long mapSize, String fileName, long bytesPinned) { + mAddress = address; + this.mapSize = mapSize; + this.fileName = fileName; + this.bytesPinned = bytesPinned; + } + + @Override + public void close() { + if (mAddress >= 0) { + PinnerUtils.safeMunmap(mAddress, mapSize); + mAddress = -1; + } + for (PinnedFile dep : pinnedDeps) { + if (dep != null) { + dep.close(); + } + } + } + + @Override + public void finalize() { + close(); + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/PinnerService.java b/services/core/java/com/android/server/pinner/PinnerService.java index ef03888d6620..d7ac5203ff53 100644 --- a/services/core/java/com/android/server/PinnerService.java +++ b/services/core/java/com/android/server/pinner/PinnerService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,14 @@ * limitations under the License. */ -package com.android.server; +package com.android.server.pinner; import static android.app.ActivityManager.UID_OBSERVER_ACTIVE; import static android.app.ActivityManager.UID_OBSERVER_GONE; import static android.os.Process.SYSTEM_UID; +import static com.android.server.flags.Flags.pinGlobalQuota; import static com.android.server.flags.Flags.pinWebview; -import static com.android.server.flags.Flags.skipHomeArtPins; import android.annotation.EnforcePermission; import android.annotation.IntDef; @@ -49,6 +49,7 @@ import android.os.Handler; import android.os.HandlerExecutor; import android.os.Looper; import android.os.Message; +import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; @@ -72,13 +73,13 @@ import com.android.internal.app.ResolverActivity; import com.android.internal.os.BackgroundThread; import com.android.internal.util.DumpUtils; import com.android.internal.util.function.pooled.PooledLambda; +import com.android.server.LocalServices; +import com.android.server.SystemService; import com.android.server.wm.ActivityTaskManagerInternal; import dalvik.system.DexFile; import dalvik.system.VMRuntime; -import java.io.Closeable; -import java.io.DataInputStream; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; @@ -110,8 +111,7 @@ public final class PinnerService extends SystemService { private static final String PIN_META_FILENAME = "pinlist.meta"; private static final int PAGE_SIZE = (int) Os.sysconf(OsConstants._SC_PAGESIZE); private static final int MATCH_FLAGS = PackageManager.MATCH_DEFAULT_ONLY - | PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE; + | PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE; private static final int KEY_CAMERA = 0; private static final int KEY_HOME = 1; @@ -126,6 +126,8 @@ public final class PinnerService extends SystemService { public static final String ANON_REGION_STAT_NAME = "[anon]"; + private static final String SYSTEM_GROUP_NAME = "system"; + @IntDef({KEY_CAMERA, KEY_HOME, KEY_ASSISTANT}) @Retention(RetentionPolicy.SOURCE) public @interface AppKey {} @@ -139,7 +141,8 @@ public final class PinnerService extends SystemService { private final UserManager mUserManager; /** The list of the statically pinned files. */ - @GuardedBy("this") private final ArrayMap<String, PinnedFile> mPinnedFiles = new ArrayMap<>(); + @GuardedBy("this") + private final ArrayMap<String, PinnedFile> mPinnedFiles = new ArrayMap<>(); /** The list of the pinned apps. This is a map from {@link AppKey} to a pinned app. */ @GuardedBy("this") @@ -159,8 +162,8 @@ public final class PinnerService extends SystemService { /** * A set of {@link AppKey} that are configured to be pinned. */ - @GuardedBy("this") - private ArraySet<Integer> mPinKeys; + @GuardedBy("this") private + ArraySet<Integer> mPinKeys; // Note that we don't use the `_BOOT` namespace for anonymous pinnings, as we want // them to be responsive to dynamic flag changes for experimentation. @@ -180,14 +183,23 @@ public final class PinnerService extends SystemService { private final boolean mConfiguredToPinAssistant; private final int mConfiguredWebviewPinBytes; + // This is the percentage of total device memory that will be used to set the global quota. + private final int mConfiguredMaxPinnedMemoryPercentage; + + // This is the global pinner quota that can be pinned. + private long mConfiguredMaxPinnedMemory; + + // This is the currently pinned memory. + private long mCurrentPinnedMemory = 0; + private BinderService mBinderService; private PinnerHandler mPinnerHandler = null; private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - // If an app has updated, update pinned files accordingly. - if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) { + // If an app has updated, update pinned files accordingly. + if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) { Uri packageUri = intent.getData(); String packageName = packageUri.getSchemeSpecificPart(); ArraySet<String> updatedPackages = new ArraySet<>(); @@ -210,7 +222,7 @@ public final class PinnerService extends SystemService { /** Utility class for testing. */ @VisibleForTesting - static class Injector { + public static class Injector { protected DeviceConfigInterface getDeviceConfigInterface() { return DeviceConfigInterface.REAL; } @@ -219,9 +231,9 @@ public final class PinnerService extends SystemService { service.publishBinderService("pinner", binderService); } - protected PinnedFile pinFileInternal(String fileToPin, - int maxBytesToPin, boolean attemptPinIntrospection) { - return PinnerService.pinFileInternal(fileToPin, maxBytesToPin, attemptPinIntrospection); + protected PinnedFile pinFileInternal(PinnerService service, String fileToPin, + long maxBytesToPin, boolean attemptPinIntrospection) { + return service.pinFileInternal(fileToPin, maxBytesToPin, attemptPinIntrospection); } } @@ -230,7 +242,7 @@ public final class PinnerService extends SystemService { } @VisibleForTesting - PinnerService(Context context, Injector injector) { + public PinnerService(Context context, Injector injector) { super(context); mContext = context; @@ -244,6 +256,9 @@ public final class PinnerService extends SystemService { com.android.internal.R.bool.config_pinnerAssistantApp); mConfiguredWebviewPinBytes = context.getResources().getInteger( com.android.internal.R.integer.config_pinnerWebviewPinBytes); + mConfiguredMaxPinnedMemoryPercentage = context.getResources().getInteger( + com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage); + mPinKeys = createPinKeys(); mPinnerHandler = new PinnerHandler(BackgroundThread.get().getLooper()); @@ -261,10 +276,8 @@ public final class PinnerService extends SystemService { registerUidListener(); registerUserSetupCompleteListener(); - mDeviceConfigInterface.addOnPropertiesChangedListener( - DEVICE_CONFIG_NAMESPACE_ANON_SIZE, - new HandlerExecutor(mPinnerHandler), - mDeviceConfigAnonSizeListener); + mDeviceConfigInterface.addOnPropertiesChangedListener(DEVICE_CONFIG_NAMESPACE_ANON_SIZE, + new HandlerExecutor(mPinnerHandler), mDeviceConfigAnonSizeListener); } @Override @@ -272,6 +285,10 @@ public final class PinnerService extends SystemService { if (DEBUG) { Slog.i(TAG, "Starting PinnerService"); } + mConfiguredMaxPinnedMemory = + (Process.getTotalMemory() + * Math.clamp(mConfiguredMaxPinnedMemoryPercentage, 0, 100)) + / 100; mBinderService = new BinderService(); mInjector.publishBinderService(this, mBinderService); publishLocalService(PinnerService.class, this); @@ -348,7 +365,7 @@ public final class PinnerService extends SystemService { protected PinnedFileStats(int uid, PinnedFile file) { this.uid = uid; this.filename = file.fileName.substring(file.fileName.lastIndexOf('/') + 1); - this.sizeKb = file.bytesPinned / 1024; + this.sizeKb = (int) file.bytesPinned / 1024; } } @@ -358,20 +375,11 @@ public final class PinnerService extends SystemService { private void handlePinOnStart() { // Files to pin come from the overlay and can be specified per-device config String[] filesToPin = mContext.getResources().getStringArray( - com.android.internal.R.array.config_defaultPinnerServiceFiles); + com.android.internal.R.array.config_defaultPinnerServiceFiles); // Continue trying to pin each file even if we fail to pin some of them for (String fileToPin : filesToPin) { - PinnedFile pf = mInjector.pinFileInternal(fileToPin, Integer.MAX_VALUE, - /*attemptPinIntrospection=*/false); - if (pf == null) { - Slog.e(TAG, "Failed to pin file = " + fileToPin); - continue; - } - synchronized (this) { - mPinnedFiles.put(pf.fileName, pf); - } - pf.groupName = "system"; - pinOptimizedDexDependencies(pf, Integer.MAX_VALUE, null); + pinFile(fileToPin, Integer.MAX_VALUE, /*appInfo=*/null, /*groupName=*/SYSTEM_GROUP_NAME, + true); } refreshPinAnonConfig(); @@ -383,10 +391,9 @@ public final class PinnerService extends SystemService { * regular home app. */ private void registerUserSetupCompleteListener() { - Uri userSetupCompleteUri = Settings.Secure.getUriFor( - Settings.Secure.USER_SETUP_COMPLETE); - mContext.getContentResolver().registerContentObserver(userSetupCompleteUri, - false, new ContentObserver(null) { + Uri userSetupCompleteUri = Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE); + mContext.getContentResolver().registerContentObserver( + userSetupCompleteUri, false, new ContentObserver(null) { @Override public void onChange(boolean selfChange, Uri uri) { if (userSetupCompleteUri.equals(uri)) { @@ -409,7 +416,7 @@ public final class PinnerService extends SystemService { } @Override - public void onUidActive(int uid) { + public void onUidActive(int uid) { mPinnerHandler.sendMessage(PooledLambda.obtainMessage( PinnerService::handleUidActive, PinnerService.this, uid)); } @@ -423,7 +430,6 @@ public final class PinnerService extends SystemService { updateActiveState(uid, false /* active */); int key; synchronized (this) { - // In case we have a pending repin, repin now. See mPendingRepin for more information. key = mPendingRepin.getOrDefault(uid, -1); if (key == -1) { @@ -491,8 +497,8 @@ public final class PinnerService extends SystemService { private ApplicationInfo getCameraInfo(int userHandle) { Intent cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); - ApplicationInfo info = getApplicationInfoForIntent(cameraIntent, userHandle, - false /* defaultToSystemApp */); + ApplicationInfo info = getApplicationInfoForIntent( + cameraIntent, userHandle, false /* defaultToSystemApp */); // If the STILL_IMAGE_CAMERA intent doesn't resolve, try the _SECURE intent. // We don't use _SECURE first because it will never get set on a device @@ -501,16 +507,16 @@ public final class PinnerService extends SystemService { // preference using this intent. if (info == null) { cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE); - info = getApplicationInfoForIntent(cameraIntent, userHandle, - false /* defaultToSystemApp */); + info = getApplicationInfoForIntent( + cameraIntent, userHandle, false /* defaultToSystemApp */); } // If the _SECURE intent doesn't resolve, try the original intent but request // the system app for camera if there was more than one result. if (info == null) { cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); - info = getApplicationInfoForIntent(cameraIntent, userHandle, - true /* defaultToSystemApp */); + info = getApplicationInfoForIntent( + cameraIntent, userHandle, true /* defaultToSystemApp */); } return info; } @@ -525,14 +531,14 @@ public final class PinnerService extends SystemService { return getApplicationInfoForIntent(intent, userHandle, true); } - private ApplicationInfo getApplicationInfoForIntent(Intent intent, int userHandle, - boolean defaultToSystemApp) { + private ApplicationInfo getApplicationInfoForIntent( + Intent intent, int userHandle, boolean defaultToSystemApp) { if (intent == null) { return null; } - ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivityAsUser(intent, - MATCH_FLAGS, userHandle); + ResolveInfo resolveInfo = + mContext.getPackageManager().resolveActivityAsUser(intent, MATCH_FLAGS, userHandle); // If this intent can resolve to only one app, choose that one. // Otherwise, if we've requested to default to the system app, return it; @@ -547,12 +553,11 @@ public final class PinnerService extends SystemService { } if (defaultToSystemApp) { - List<ResolveInfo> infoList = mContext.getPackageManager() - .queryIntentActivitiesAsUser(intent, MATCH_FLAGS, userHandle); + List<ResolveInfo> infoList = mContext.getPackageManager().queryIntentActivitiesAsUser( + intent, MATCH_FLAGS, userHandle); ApplicationInfo systemAppInfo = null; for (ResolveInfo info : infoList) { - if ((info.activityInfo.applicationInfo.flags - & ApplicationInfo.FLAG_SYSTEM) != 0) { + if ((info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { if (systemAppInfo == null) { systemAppInfo = info.activityInfo.applicationInfo; } else { @@ -568,13 +573,13 @@ public final class PinnerService extends SystemService { } private void sendPinAppsMessage(int userHandle) { - mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinApps, this, - userHandle)); + mPinnerHandler.sendMessage( + PooledLambda.obtainMessage(PinnerService::pinApps, this, userHandle)); } private void sendPinAppsWithUpdatedKeysMessage(int userHandle) { - mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinAppsWithUpdatedKeys, - this, userHandle)); + mPinnerHandler.sendMessage(PooledLambda.obtainMessage( + PinnerService::pinAppsWithUpdatedKeys, this, userHandle)); } private void sendUnpinAppsMessage() { mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::unpinApps, this)); @@ -586,8 +591,7 @@ public final class PinnerService extends SystemService { // phenotype property is not set. boolean shouldPinCamera = mConfiguredToPinCamera && mDeviceConfigInterface.getBoolean(DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT, - "pin_camera", - SystemProperties.getBoolean("pinner.pin_camera", true)); + "pin_camera", SystemProperties.getBoolean("pinner.pin_camera", true)); if (shouldPinCamera) { pinKeys.add(KEY_CAMERA); } else if (DEBUG) { @@ -626,8 +630,9 @@ public final class PinnerService extends SystemService { synchronized (this) { // This code path demands preceding unpinApps() call. if (!mPinnedApps.isEmpty()) { - Slog.e(TAG, "Attempted to update a list of apps, " - + "but apps were already pinned. Skipping."); + Slog.e(TAG, + "Attempted to update a list of apps, " + + "but apps were already pinned. Skipping."); return; } @@ -646,8 +651,8 @@ public final class PinnerService extends SystemService { * @see #pinApp(int, int, boolean) */ private void sendPinAppMessage(int key, int userHandle, boolean force) { - mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinApp, this, - key, userHandle, force)); + mPinnerHandler.sendMessage( + PooledLambda.obtainMessage(PinnerService::pinApp, this, key, userHandle, force)); } /** @@ -667,10 +672,10 @@ public final class PinnerService extends SystemService { } return; } - unpinApp(key); ApplicationInfo info = getInfoForKey(key, userHandle); + unpinApp(key); if (info != null) { - pinApp(key, info); + pinAppInternal(key, info); } } @@ -682,9 +687,7 @@ public final class PinnerService extends SystemService { private int getUidForKey(@AppKey int key) { synchronized (this) { PinnedApp existing = mPinnedApps.get(key); - return existing != null && existing.active - ? existing.uid - : -1; + return existing != null && existing.active ? existing.uid : -1; } } @@ -727,11 +730,8 @@ public final class PinnerService extends SystemService { * Handle any changes in the anon region pinner config. */ private void refreshPinAnonConfig() { - long newPinAnonSize = - mDeviceConfigInterface.getLong( - DEVICE_CONFIG_NAMESPACE_ANON_SIZE, - DEVICE_CONFIG_KEY_ANON_SIZE, - DEFAULT_ANON_SIZE); + long newPinAnonSize = mDeviceConfigInterface.getLong( + DEVICE_CONFIG_NAMESPACE_ANON_SIZE, DEVICE_CONFIG_KEY_ANON_SIZE, DEFAULT_ANON_SIZE); newPinAnonSize = Math.max(0, Math.min(newPinAnonSize, MAX_ANON_SIZE)); if (newPinAnonSize != mPinAnonSize) { mPinAnonSize = newPinAnonSize; @@ -765,10 +765,9 @@ public final class PinnerService extends SystemService { try { // Map as SHARED to avoid changing rss.anon for system_server (per /proc/*/status). // The mapping is visible in other rss metrics, and as private dirty in smaps/meminfo. - address = Os.mmap(0, alignedPinSize, - OsConstants.PROT_READ | OsConstants.PROT_WRITE, - OsConstants.MAP_SHARED | OsConstants.MAP_ANONYMOUS, - new FileDescriptor(), /*offset=*/0); + address = Os.mmap(0, alignedPinSize, OsConstants.PROT_READ | OsConstants.PROT_WRITE, + OsConstants.MAP_SHARED | OsConstants.MAP_ANONYMOUS, new FileDescriptor(), + /*offset=*/0); Unsafe tempUnsafe = null; Class<sun.misc.Unsafe> clazz = sun.misc.Unsafe.class; @@ -794,14 +793,14 @@ public final class PinnerService extends SystemService { return; } finally { if (address >= 0) { - safeMunmap(address, alignedPinSize); + PinnerUtils.safeMunmap(address, alignedPinSize); } } } private void unpinAnonRegion() { if (mPinAnonAddress != 0) { - safeMunmap(mPinAnonAddress, mCurrentlyPinnedAnonSize); + PinnerUtils.safeMunmap(mPinAnonAddress, mCurrentlyPinnedAnonSize); } mPinAnonAddress = 0; mCurrentlyPinnedAnonSize = 0; @@ -824,12 +823,20 @@ public final class PinnerService extends SystemService { } /** + * Retrieves remaining quota for pinner service, once it reaches 0 it will no longer + * pin any file. + */ + private long getAvailableGlobalQuota() { + return mConfiguredMaxPinnedMemory - mCurrentPinnedMemory; + } + + /** * Pins an application. * * @param key The key of the app to pin. * @param appInfo The corresponding app info. */ - private void pinApp(@AppKey int key, @Nullable ApplicationInfo appInfo) { + private void pinAppInternal(@AppKey int key, @Nullable ApplicationInfo appInfo) { if (appInfo == null) { return; } @@ -839,7 +846,6 @@ public final class PinnerService extends SystemService { mPinnedApps.put(key, pinnedApp); } - // pin APK final int pinSizeLimit = getSizeLimitForKey(key); List<String> apks = new ArrayList<>(); @@ -851,36 +857,31 @@ public final class PinnerService extends SystemService { } } - int apkPinSizeLimit = pinSizeLimit; - - boolean shouldSkipArtPins = key == KEY_HOME && skipHomeArtPins(); + long apkPinSizeLimit = pinSizeLimit; - for (String apk: apks) { + for (String apk : apks) { if (apkPinSizeLimit <= 0) { Slog.w(TAG, "Reached to the pin size limit. Skipping: " + apk); // Continue instead of break to print all skipped APK names. continue; } - PinnedFile pf = mInjector.pinFileInternal(apk, apkPinSizeLimit, /*attemptPinIntrospection=*/true); + String pinGroup = getNameForKey(key); + boolean shouldPinDeps = apk.equals(appInfo.sourceDir); + PinnedFile pf = pinFile(apk, apkPinSizeLimit, appInfo, pinGroup, shouldPinDeps); if (pf == null) { Slog.e(TAG, "Failed to pin " + apk); continue; } - pf.groupName = getNameForKey(key); if (DEBUG) { Slog.i(TAG, "Pinned " + pf.fileName); } synchronized (this) { pinnedApp.mFiles.add(pf); - mPinnedFiles.put(pf.fileName, pf); } apkPinSizeLimit -= pf.bytesPinned; - if (apk.equals(appInfo.sourceDir) && !shouldSkipArtPins) { - pinOptimizedDexDependencies(pf, Integer.MAX_VALUE, appInfo); - } } } @@ -892,19 +893,23 @@ public final class PinnerService extends SystemService { * that related to the file but not within itself. * * @param fileToPin File to pin - * @param maxBytesToPin maximum quota allowed for pinning - * @return total bytes that were pinned. + * @param bytesRequestedToPin maximum bytes requested to pin for {@code fileToPin}. + * @param pinOptimizedDeps whether optimized dependencies such as odex,vdex, etc be pinned. + * Note: {@code bytesRequestedToPin} limit will not apply to optimized + * dependencies pinned, only global quotas will apply instead. + * @return pinned file */ - public int pinFile(String fileToPin, int maxBytesToPin, @Nullable ApplicationInfo appInfo, - @Nullable String groupName) { + public PinnedFile pinFile(String fileToPin, long bytesRequestedToPin, + @Nullable ApplicationInfo appInfo, @Nullable String groupName, + boolean pinOptimizedDeps) { PinnedFile existingPin; - synchronized(this) { + synchronized (this) { existingPin = mPinnedFiles.get(fileToPin); } if (existingPin != null) { - if (existingPin.bytesPinned == maxBytesToPin) { + if (existingPin.bytesPinned == bytesRequestedToPin) { // Duplicate pin requesting same amount of bytes, lets just bail out. - return 0; + return null; } else { // User decided to pin a different amount of bytes than currently pinned // so this is a valid pin request. Unpin the previous version before repining. @@ -915,26 +920,38 @@ public final class PinnerService extends SystemService { } } + long remainingQuota = getAvailableGlobalQuota(); + + if (pinGlobalQuota()) { + if (remainingQuota <= 0) { + Slog.w(TAG, "Reached pin quota, skipping file: " + fileToPin); + return null; + } + bytesRequestedToPin = Math.min(bytesRequestedToPin, remainingQuota); + } + boolean isApk = fileToPin.endsWith(".apk"); - int bytesPinned = 0; - PinnedFile pf = mInjector.pinFileInternal(fileToPin, maxBytesToPin, + + PinnedFile pf = mInjector.pinFileInternal(this, fileToPin, bytesRequestedToPin, /*attemptPinIntrospection=*/isApk); if (pf == null) { Slog.e(TAG, "Failed to pin file = " + fileToPin); - return 0; + return null; } pf.groupName = groupName != null ? groupName : ""; - bytesPinned += pf.bytesPinned; - maxBytesToPin -= bytesPinned; + mCurrentPinnedMemory += pf.bytesPinned; synchronized (this) { mPinnedFiles.put(pf.fileName, pf); } - if (maxBytesToPin > 0) { - pinOptimizedDexDependencies(pf, maxBytesToPin, appInfo); + + if (pinOptimizedDeps) { + mCurrentPinnedMemory += + pinOptimizedDexDependencies(pf, getAvailableGlobalQuota(), appInfo); } - return bytesPinned; + + return pf; } /** @@ -945,13 +962,13 @@ public final class PinnerService extends SystemService { * to null it will use the default supported ABI by the device. * @return total bytes pinned. */ - private int pinOptimizedDexDependencies( - PinnedFile pinnedFile, int maxBytesToPin, @Nullable ApplicationInfo appInfo) { + private long pinOptimizedDexDependencies( + PinnedFile pinnedFile, long maxBytesToPin, @Nullable ApplicationInfo appInfo) { if (pinnedFile == null) { return 0; } - int bytesPinned = 0; + long bytesPinned = 0; if (pinnedFile.fileName.endsWith(".jar") | pinnedFile.fileName.endsWith(".apk")) { String abi = null; if (appInfo != null) { @@ -974,7 +991,7 @@ public final class PinnerService extends SystemService { // Unpin if it was already pinned prior to re-pinning. unpinFile(file); - PinnedFile df = mInjector.pinFileInternal(file, maxBytesToPin, + PinnedFile df = mInjector.pinFileInternal(this, file, maxBytesToPin, /*attemptPinIntrospection=*/false); if (df == null) { Slog.i(TAG, "Failed to pin ART file = " + file); @@ -992,7 +1009,8 @@ public final class PinnerService extends SystemService { return bytesPinned; } - /** mlock length bytes of fileToPin in memory + /** + * mlock length bytes of fileToPin in memory * * If attemptPinIntrospection is true, then treat the file to pin as a zip file and * look for a "pinlist.meta" file in the archive root directory. The structure of this @@ -1029,8 +1047,8 @@ public final class PinnerService extends SystemService { * zip in order to extract the * @return Pinned memory resource owner thing or null on error */ - private static PinnedFile pinFileInternal( - String fileToPin, int maxBytesToPin, boolean attemptPinIntrospection) { + private PinnedFile pinFileInternal( + String fileToPin, long maxBytesToPin, boolean attemptPinIntrospection) { if (DEBUG) { Slog.d(TAG, "pin file: " + fileToPin + " use-pinlist: " + attemptPinIntrospection); } @@ -1054,8 +1072,8 @@ public final class PinnerService extends SystemService { } return pinnedFile; } finally { - safeClose(pinRangeStream); - safeClose(fileAsZip); // Also closes any streams we've opened + PinnerUtils.safeClose(pinRangeStream); + PinnerUtils.safeClose(fileAsZip); // Also closes any streams we've opened } } @@ -1068,11 +1086,8 @@ public final class PinnerService extends SystemService { try { zip = new ZipFile(fileName); } catch (IOException ex) { - Slog.w(TAG, - String.format( - "could not open \"%s\" as zip: pinning as blob", - fileName), - ex); + Slog.w(TAG, String.format("could not open \"%s\" as zip: pinning as blob", fileName), + ex); } return zip; } @@ -1112,9 +1127,9 @@ public final class PinnerService extends SystemService { pinMetaStream = zipFile.getInputStream(pinMetaEntry); } catch (IOException ex) { Slog.w(TAG, - String.format("error reading pin metadata \"%s\": pinning as blob", - fileName), - ex); + String.format( + "error reading pin metadata \"%s\": pinning as blob", fileName), + ex); } } else { Slog.w(TAG, @@ -1124,57 +1139,6 @@ public final class PinnerService extends SystemService { return pinMetaStream; } - private static abstract class PinRangeSource { - /** Retrive a range to pin. - * - * @param outPinRange Receives the pin region - * @return True if we filled in outPinRange or false if we're out of pin entries - */ - abstract boolean read(PinRange outPinRange); - } - - private static final class PinRangeSourceStatic extends PinRangeSource { - private final int mPinStart; - private final int mPinLength; - private boolean mDone = false; - - PinRangeSourceStatic(int pinStart, int pinLength) { - mPinStart = pinStart; - mPinLength = pinLength; - } - - @Override - boolean read(PinRange outPinRange) { - outPinRange.start = mPinStart; - outPinRange.length = mPinLength; - boolean done = mDone; - mDone = true; - return !done; - } - } - - private static final class PinRangeSourceStream extends PinRangeSource { - private final DataInputStream mStream; - private boolean mDone = false; - - PinRangeSourceStream(InputStream stream) { - mStream = new DataInputStream(stream); - } - - @Override - boolean read(PinRange outPinRange) { - if (!mDone) { - try { - outPinRange.start = mStream.readInt(); - outPinRange.length = mStream.readInt(); - } catch (IOException ex) { - mDone = true; - } - } - return !mDone; - } - } - /** * Helper for pinFile. * @@ -1185,25 +1149,20 @@ public final class PinnerService extends SystemService { * @return PinnedFile or null on error */ private static PinnedFile pinFileRanges( - String fileToPin, - int maxBytesToPin, - PinRangeSource pinRangeSource) - { + String fileToPin, long maxBytesToPin, PinRangeSource pinRangeSource) { FileDescriptor fd = new FileDescriptor(); long address = -1; - int mapSize = 0; + long mapSize = 0; try { int openFlags = (OsConstants.O_RDONLY | OsConstants.O_CLOEXEC); fd = Os.open(fileToPin, openFlags, 0); mapSize = (int) Math.min(Os.fstat(fd).st_size, Integer.MAX_VALUE); - address = Os.mmap(0, mapSize, - OsConstants.PROT_READ, - OsConstants.MAP_SHARED, - fd, /*offset=*/0); + address = Os.mmap( + 0, mapSize, OsConstants.PROT_READ, OsConstants.MAP_SHARED, fd, /*offset=*/0); PinRange pinRange = new PinRange(); - int bytesPinned = 0; + long bytesPinned = 0; // We pin at page granularity, so make sure the limit is page-aligned if (maxBytesToPin % PAGE_SIZE != 0) { @@ -1211,10 +1170,10 @@ public final class PinnerService extends SystemService { } while (bytesPinned < maxBytesToPin && pinRangeSource.read(pinRange)) { - int pinStart = pinRange.start; - int pinLength = pinRange.length; - pinStart = clamp(0, pinStart, mapSize); - pinLength = clamp(0, pinLength, mapSize - pinStart); + long pinStart = pinRange.start; + long pinLength = pinRange.length; + pinStart = PinnerUtils.clamp(0, pinStart, mapSize); + pinLength = PinnerUtils.clamp(0, pinLength, mapSize - pinStart); pinLength = Math.min(maxBytesToPin - bytesPinned, pinLength); // mlock doesn't require the region to be page-aligned, but we snap the @@ -1229,14 +1188,13 @@ public final class PinnerService extends SystemService { if (pinLength % PAGE_SIZE != 0) { pinLength += PAGE_SIZE - pinLength % PAGE_SIZE; } - pinLength = clamp(0, pinLength, maxBytesToPin - bytesPinned); + pinLength = PinnerUtils.clamp(0, pinLength, maxBytesToPin - bytesPinned); if (pinLength > 0) { if (DEBUG) { Slog.d(TAG, - String.format( - "pinning at %s %s bytes of %s", - pinStart, pinLength, fileToPin)); + String.format("pinning at %s %s bytes of %s", pinStart, pinLength, + fileToPin)); } Os.mlock(address + pinStart, pinLength); } @@ -1244,15 +1202,15 @@ public final class PinnerService extends SystemService { } PinnedFile pinnedFile = new PinnedFile(address, mapSize, fileToPin, bytesPinned); - address = -1; // Ownership transferred + address = -1; // Ownership transferred return pinnedFile; } catch (ErrnoException ex) { Slog.e(TAG, "Could not pin file " + fileToPin, ex); return null; } finally { - safeClose(fd); + PinnerUtils.safeClose(fd); if (address >= 0) { - safeMunmap(address, mapSize); + PinnerUtils.safeMunmap(address, mapSize); } } } @@ -1273,81 +1231,50 @@ public final class PinnerService extends SystemService { } } - public void unpinFile(String filename) { + /** + * Unpin a file and its optimized dependencies. + * + * @param filename file to unpin. + * @return number of bytes unpinned, 0 in case of failure or nothing to unpin. + */ + public long unpinFile(String filename) { PinnedFile pinnedFile; synchronized (this) { pinnedFile = mPinnedFiles.get(filename); } if (pinnedFile == null) { // File not pinned, nothing to do. - return; + return 0; } + long unpinnedBytes = pinnedFile.bytesPinned; pinnedFile.close(); synchronized (this) { if (DEBUG) { Slog.d(TAG, "Unpinned file: " + filename); } + mCurrentPinnedMemory -= pinnedFile.bytesPinned; + mPinnedFiles.remove(pinnedFile.fileName); for (PinnedFile dep : pinnedFile.pinnedDeps) { if (dep == null) { continue; } + unpinnedBytes -= dep.bytesPinned; + mCurrentPinnedMemory -= dep.bytesPinned; mPinnedFiles.remove(dep.fileName); if (DEBUG) { Slog.d(TAG, "Unpinned dependency: " + dep.fileName); } } } - } - private static int clamp(int min, int value, int max) { - return Math.max(min, Math.min(value, max)); - } - - private static void safeMunmap(long address, long mapSize) { - try { - Os.munmap(address, mapSize); - } catch (ErrnoException ex) { - Slog.w(TAG, "ignoring error in unmap", ex); - } - } - - /** - * Close FD, swallowing irrelevant errors. - */ - private static void safeClose(@Nullable FileDescriptor fd) { - if (fd != null && fd.valid()) { - try { - Os.close(fd); - } catch (ErrnoException ex) { - // Swallow the exception: non-EBADF errors in close(2) - // indicate deferred paging write errors, which we - // don't care about here. The underlying file - // descriptor is always closed. - if (ex.errno == OsConstants.EBADF) { - throw new AssertionError(ex); - } - } - } - } - - /** - * Close closeable thing, swallowing errors. - */ - private static void safeClose(@Nullable Closeable thing) { - if (thing != null) { - try { - thing.close(); - } catch (IOException ex) { - Slog.w(TAG, "ignoring error closing resource: " + thing, ex); - } - } + return unpinnedBytes; } public List<PinnedFileStat> getPinnerStats() { ArrayList<PinnedFileStat> stats = new ArrayList<>(); Collection<PinnedFile> pinnedFiles; - synchronized(this) { + synchronized (this) { pinnedFiles = mPinnedFiles.values(); } for (PinnedFile pf : pinnedFiles) { @@ -1355,8 +1282,8 @@ public final class PinnerService extends SystemService { stats.add(stat); } if (mCurrentlyPinnedAnonSize > 0) { - stats.add(new PinnedFileStat(ANON_REGION_STAT_NAME, - mCurrentlyPinnedAnonSize, ANON_REGION_STAT_NAME)); + stats.add(new PinnedFileStat( + ANON_REGION_STAT_NAME, mCurrentlyPinnedAnonSize, ANON_REGION_STAT_NAME)); } return stats; } @@ -1364,71 +1291,124 @@ public final class PinnerService extends SystemService { public final class BinderService extends IPinnerService.Stub { @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; + if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) + return; HashSet<PinnedFile> shownPins = new HashSet<>(); - HashSet<String> groups = new HashSet<>(); - final int bytesPerMB = 1024 * 1024; + HashSet<String> shownGroups = new HashSet<>(); + HashSet<String> groupsToPrint = new HashSet<>(); + final double bytesPerMB = 1024 * 1024; + pw.format("Pinner Configs:\n"); + pw.format(" Total Pinner quota: %d%% of total device memory\n", + mConfiguredMaxPinnedMemoryPercentage); + pw.format(" Maximum Pinner quota: %d bytes (%.2f MB)\n", mConfiguredMaxPinnedMemory, + mConfiguredMaxPinnedMemory / bytesPerMB); + pw.format(" Max Home App Pin Bytes (without deps): %d\n", mConfiguredHomePinBytes); + pw.format("\nPinned Files:\n"); synchronized (PinnerService.this) { long totalSize = 0; + + // We print apps separately from regular pins as they contain extra information that + // other pins do not. for (int key : mPinnedApps.keySet()) { PinnedApp app = mPinnedApps.get(key); pw.print(getNameForKey(key)); - pw.print(" uid="); pw.print(app.uid); - pw.print(" active="); pw.print(app.active); + pw.print(" uid="); + pw.print(app.uid); + pw.print(" active="); + pw.print(app.active); + + if (!app.mFiles.isEmpty()) { + shownGroups.add(app.mFiles.getFirst().groupName); + } pw.println(); + long bytesPinnedForApp = 0; + long bytesPinnedForAppDeps = 0; for (PinnedFile pf : mPinnedApps.get(key).mFiles) { pw.print(" "); - pw.format("%s pinned:%d bytes (%d MB) pinlist:%b\n", pf.fileName, + pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b\n", pf.fileName, pf.bytesPinned, pf.bytesPinned / bytesPerMB, pf.used_pinlist); totalSize += pf.bytesPinned; + bytesPinnedForApp += pf.bytesPinned; shownPins.add(pf); for (PinnedFile dep : pf.pinnedDeps) { pw.print(" "); - pw.format("%s pinned:%d bytes (%d MB) pinlist:%b (Dependency)\n", dep.fileName, - dep.bytesPinned, dep.bytesPinned / bytesPerMB, dep.used_pinlist); + pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b (Dependency)\n", + dep.fileName, dep.bytesPinned, dep.bytesPinned / bytesPerMB, + dep.used_pinlist); totalSize += dep.bytesPinned; + bytesPinnedForAppDeps += dep.bytesPinned; shownPins.add(dep); } } + long bytesPinnedForAppAndDeps = bytesPinnedForApp + bytesPinnedForAppDeps; + pw.format("Total Pinned = %d (%.2f MB) [App=%d (%.2f MB), " + + "Dependencies=%d (%.2f MB)]\n\n", + bytesPinnedForAppAndDeps, bytesPinnedForAppAndDeps / bytesPerMB, + bytesPinnedForApp, bytesPinnedForApp / bytesPerMB, + bytesPinnedForAppDeps, bytesPinnedForAppDeps / bytesPerMB); } pw.println(); for (PinnedFile pinnedFile : mPinnedFiles.values()) { - if (!groups.contains(pinnedFile.groupName)) { - groups.add(pinnedFile.groupName); + if (!groupsToPrint.contains(pinnedFile.groupName) + && !shownGroups.contains(pinnedFile.groupName)) { + groupsToPrint.add(pinnedFile.groupName); } } - boolean firstPinInGroup = true; - for (String group : groups) { + + // Print all the non app groups. + for (String group : groupsToPrint) { List<PinnedFile> groupPins = getAllPinsForGroup(group); + pw.print("\nGroup:" + group); + long bytesPinnedForGroupNoDeps = 0; + long bytesPinnedForGroupDeps = 0; + pw.println(); for (PinnedFile pinnedFile : groupPins) { if (shownPins.contains(pinnedFile)) { - // Already showed in the dump and accounted for, skip. + // Already displayed and accounted for, skip. continue; } - if (firstPinInGroup) { - firstPinInGroup = false; - // Ensure we only print when there are pins for groups not yet shown - // in the pinned app section. - pw.print("Group:" + group); - pw.println(); - } - pw.format(" %s pinned:%d bytes (%d MB) pinlist:%b\n", pinnedFile.fileName, - pinnedFile.bytesPinned, pinnedFile.bytesPinned / bytesPerMB, - pinnedFile.used_pinlist); + pw.format(" %s pinned: %d bytes (%.2f MB) pinlist:%b\n", + pinnedFile.fileName, pinnedFile.bytesPinned, + pinnedFile.bytesPinned / bytesPerMB, pinnedFile.used_pinlist); totalSize += pinnedFile.bytesPinned; + bytesPinnedForGroupNoDeps += pinnedFile.bytesPinned; + shownPins.add(pinnedFile); + for (PinnedFile dep : pinnedFile.pinnedDeps) { + if (shownPins.contains(dep)) { + // Already displayed and accounted for, skip. + continue; + } + pw.print(" "); + pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b (Dependency)\n", + dep.fileName, dep.bytesPinned, dep.bytesPinned / bytesPerMB, + dep.used_pinlist); + totalSize += dep.bytesPinned; + bytesPinnedForGroupDeps += dep.bytesPinned; + shownPins.add(dep); + } } + long bytesPinnedForGroup = bytesPinnedForGroupNoDeps + bytesPinnedForGroupDeps; + pw.format("Total Pinned = %d (%.2f MB) [Main=%d (%.2f MB), " + + "Dependencies=%d (%.2f MB)]\n\n", + bytesPinnedForGroup, bytesPinnedForGroup / bytesPerMB, + bytesPinnedForGroupNoDeps, bytesPinnedForGroupNoDeps / bytesPerMB, + bytesPinnedForGroupDeps, bytesPinnedForGroupDeps / bytesPerMB); } pw.println(); if (mPinAnonAddress != 0) { - pw.format("Pinned anon region: %d (%d MB)\n", mCurrentlyPinnedAnonSize, mCurrentlyPinnedAnonSize / bytesPerMB); + pw.format("Pinned anon region: %d (%.2f MB)\n", mCurrentlyPinnedAnonSize, + mCurrentlyPinnedAnonSize / bytesPerMB); totalSize += mCurrentlyPinnedAnonSize; } - pw.format("Total pinned: %s bytes (%s MB)\n", totalSize, totalSize / bytesPerMB); + pw.format("Total pinned: %d bytes (%.2f MB)\n", totalSize, totalSize / bytesPerMB); + pw.format("Available Pinner quota: %d bytes (%.2f MB)\n", getAvailableGlobalQuota(), + getAvailableGlobalQuota() / bytesPerMB); pw.println(); if (!mPendingRepin.isEmpty()) { pw.print("Pending repin: "); for (int key : mPendingRepin.values()) { - pw.print(getNameForKey(key)); pw.print(' '); + pw.print(getNameForKey(key)); + pw.print(' '); } pw.println(); } @@ -1462,8 +1442,9 @@ public final class PinnerService extends SystemService { repin(); break; default: - printError(out, String.format( - "Unknown pinner command: %s. Supported commands: repin", command)); + printError(out, + String.format("Unknown pinner command: %s. Supported commands: repin", + command)); resultReceiver.send(-1, null); return; } @@ -1479,46 +1460,6 @@ public final class PinnerService extends SystemService { } } - @VisibleForTesting - public static final class PinnedFile implements AutoCloseable { - private long mAddress; - final int mapSize; - final String fileName; - final int bytesPinned; - - // Whether this file was pinned using a pinlist - boolean used_pinlist; - - // User defined group name for pinner accounting - String groupName = ""; - ArrayList<PinnedFile> pinnedDeps = new ArrayList<>(); - - PinnedFile(long address, int mapSize, String fileName, int bytesPinned) { - mAddress = address; - this.mapSize = mapSize; - this.fileName = fileName; - this.bytesPinned = bytesPinned; - } - - @Override - public void close() { - if (mAddress >= 0) { - safeMunmap(mAddress, mapSize); - mAddress = -1; - } - for (PinnedFile dep : pinnedDeps) { - if (dep != null) { - dep.close(); - } - } - } - - @Override - public void finalize() { - close(); - } - } - final static class PinRange { int start; int length; @@ -1528,7 +1469,6 @@ public final class PinnerService extends SystemService { * Represents an app that was pinned. */ private final class PinnedApp { - /** * The uid of the package being pinned. This stays constant while the package stays * installed. @@ -1557,11 +1497,9 @@ public final class PinnerService extends SystemService { @Override public void handleMessage(Message msg) { switch (msg.what) { - case PIN_ONSTART_MSG: - { + case PIN_ONSTART_MSG: { handlePinOnStart(); - } - break; + } break; default: super.handleMessage(msg); diff --git a/services/core/java/com/android/server/pinner/PinnerUtils.java b/services/core/java/com/android/server/pinner/PinnerUtils.java new file mode 100644 index 000000000000..a836a83dedab --- /dev/null +++ b/services/core/java/com/android/server/pinner/PinnerUtils.java @@ -0,0 +1,75 @@ +/* + * 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.pinner; + +import android.annotation.Nullable; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.util.Slog; + +import java.io.Closeable; +import java.io.FileDescriptor; +import java.io.IOException; + +/* package */ final class PinnerUtils { + private static final String TAG = "PinnerUtils"; + + public static long clamp(long min, long value, long max) { + return Math.max(min, Math.min(value, max)); + } + + public static void safeMunmap(long address, long mapSize) { + try { + Os.munmap(address, mapSize); + } catch (ErrnoException ex) { + Slog.w(TAG, "ignoring error in unmap", ex); + } + } + + /** + * Close FD, swallowing irrelevant errors. + */ + public static void safeClose(@Nullable FileDescriptor fd) { + if (fd != null && fd.valid()) { + try { + Os.close(fd); + } catch (ErrnoException ex) { + // Swallow the exception: non-EBADF errors in close(2) + // indicate deferred paging write errors, which we + // don't care about here. The underlying file + // descriptor is always closed. + if (ex.errno == OsConstants.EBADF) { + throw new AssertionError(ex); + } + } + } + } + + /** + * Close closeable thing, swallowing errors. + */ + public static void safeClose(@Nullable Closeable thing) { + if (thing != null) { + try { + thing.close(); + } catch (IOException ex) { + Slog.w(TAG, "ignoring error closing resource: " + thing, ex); + } + } + } +} diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java index 9ecc7b9a805d..1569fa0aa8d7 100644 --- a/services/core/java/com/android/server/pm/DexOptHelper.java +++ b/services/core/java/com/android/server/pm/DexOptHelper.java @@ -70,13 +70,13 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.IndentingPrintWriter; import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; -import com.android.server.PinnerService; import com.android.server.art.ArtManagerLocal; import com.android.server.art.DexUseManagerLocal; import com.android.server.art.ReasonMapping; import com.android.server.art.model.ArtFlags; import com.android.server.art.model.DexoptParams; import com.android.server.art.model.DexoptResult; +import com.android.server.pinner.PinnerService; import com.android.server.pm.PackageDexOptimizer.DexOptResult; import com.android.server.pm.dex.DexManager; import com.android.server.pm.dex.DexoptOptions; diff --git a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java index f047f564538d..ab630eef4644 100644 --- a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java +++ b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java @@ -17,6 +17,7 @@ package com.android.server.power.stats.wakeups; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM; +import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_BLUETOOTH; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER; @@ -63,6 +64,7 @@ public class CpuWakeupStats { private static final String SUBSYSTEM_SOUND_TRIGGER_STRING = "Sound_trigger"; private static final String SUBSYSTEM_SENSOR_STRING = "Sensor"; private static final String SUBSYSTEM_CELLULAR_DATA_STRING = "Cellular_data"; + private static final String SUBSYSTEM_BLUETOOTH_STRING = "Bluetooth"; private static final String TRACE_TRACK_WAKEUP_ATTRIBUTION = "wakeup_attribution"; private static final long WAKEUP_WRITE_DELAY_MS = TimeUnit.SECONDS.toMillis(30); @@ -512,6 +514,8 @@ public class CpuWakeupStats { return CPU_WAKEUP_SUBSYSTEM_SENSOR; case SUBSYSTEM_CELLULAR_DATA_STRING: return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA; + case SUBSYSTEM_BLUETOOTH_STRING: + return CPU_WAKEUP_SUBSYSTEM_BLUETOOTH; } return CPU_WAKEUP_SUBSYSTEM_UNKNOWN; } @@ -528,6 +532,8 @@ public class CpuWakeupStats { return SUBSYSTEM_SENSOR_STRING; case CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA: return SUBSYSTEM_CELLULAR_DATA_STRING; + case CPU_WAKEUP_SUBSYSTEM_BLUETOOTH: + return SUBSYSTEM_BLUETOOTH_STRING; case CPU_WAKEUP_SUBSYSTEM_UNKNOWN: return "Unknown"; } diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index b35a0a772ff2..74c1124e1f16 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -216,13 +216,13 @@ import com.android.role.RoleManagerLocal; import com.android.server.BinderCallsStatsService; import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; -import com.android.server.PinnerService; -import com.android.server.PinnerService.PinnedFileStats; import com.android.server.SystemService; import com.android.server.SystemServiceManager; import com.android.server.am.MemoryStatUtil.MemoryStat; import com.android.server.health.HealthServiceWrapper; import com.android.server.notification.NotificationManagerService; +import com.android.server.pinner.PinnerService; +import com.android.server.pinner.PinnerService.PinnedFileStats; import com.android.server.pm.UserManagerInternal; import com.android.server.power.stats.KernelWakelockReader; import com.android.server.power.stats.KernelWakelockStats; diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java index c120fc7d82f5..6aed00e0e32b 100644 --- a/services/core/java/com/android/server/vibrator/VibratorController.java +++ b/services/core/java/com/android/server/vibrator/VibratorController.java @@ -57,8 +57,7 @@ final class VibratorController { // for a snippet of the current known vibrator state/info. private volatile VibratorInfo mVibratorInfo; private volatile boolean mVibratorInfoLoadSuccessful; - private volatile boolean mIsVibrating; - private volatile boolean mIsUnderExternalControl; + private volatile VibratorState mCurrentState; private volatile float mCurrentAmplitude; /** @@ -75,6 +74,11 @@ final class VibratorController { void onComplete(int vibratorId, long vibrationId); } + /** Representation of the vibrator state based on the interactions through this controller. */ + private enum VibratorState { + IDLE, VIBRATING, UNDER_EXTERNAL_CONTROL + } + VibratorController(int vibratorId, OnVibrationCompleteListener listener) { this(vibratorId, listener, new NativeWrapper()); } @@ -87,6 +91,7 @@ final class VibratorController { VibratorInfo.Builder vibratorInfoBuilder = new VibratorInfo.Builder(vibratorId); mVibratorInfoLoadSuccessful = mNativeWrapper.getInfo(vibratorInfoBuilder); mVibratorInfo = vibratorInfoBuilder.build(); + mCurrentState = VibratorState.IDLE; if (!mVibratorInfoLoadSuccessful) { Slog.e(TAG, @@ -106,7 +111,7 @@ final class VibratorController { return false; } // Notify its callback after new client registered. - notifyStateListener(listener, mIsVibrating); + notifyStateListener(listener, isVibrating(mCurrentState)); } return true; } finally { @@ -166,7 +171,7 @@ final class VibratorController { * automatically notified to any registered {@link IVibratorStateListener} on change. */ public boolean isVibrating() { - return mIsVibrating; + return isVibrating(mCurrentState); } /** @@ -184,11 +189,6 @@ final class VibratorController { return mCurrentAmplitude; } - /** Return {@code true} if this vibrator is under external control, false otherwise. */ - public boolean isUnderExternalControl() { - return mIsUnderExternalControl; - } - /** * Check against this vibrator capabilities. * @@ -214,7 +214,7 @@ final class VibratorController { /** * Set the vibrator control to be external or not, based on given flag. * - * <p>This will affect the state of {@link #isUnderExternalControl()}. + * <p>This will affect the state of {@link #isVibrating()}. */ public void setExternalControl(boolean externalControl) { Trace.traceBegin(TRACE_TAG_VIBRATOR, @@ -224,9 +224,11 @@ final class VibratorController { if (!mVibratorInfo.hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) { return; } + VibratorState newState = + externalControl ? VibratorState.UNDER_EXTERNAL_CONTROL : VibratorState.IDLE; synchronized (mLock) { - mIsUnderExternalControl = externalControl; mNativeWrapper.setExternalControl(externalControl); + updateStateAndNotifyListenersLocked(newState); } } finally { Trace.traceEnd(TRACE_TAG_VIBRATOR); @@ -264,7 +266,7 @@ final class VibratorController { if (mVibratorInfo.hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL)) { mNativeWrapper.setAmplitude(amplitude); } - if (mIsVibrating) { + if (mCurrentState == VibratorState.VIBRATING) { mCurrentAmplitude = amplitude; } } @@ -289,7 +291,7 @@ final class VibratorController { long duration = mNativeWrapper.on(milliseconds, vibrationId); if (duration > 0) { mCurrentAmplitude = -1; - notifyListenerOnVibrating(true); + updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); } return duration; } @@ -319,7 +321,7 @@ final class VibratorController { vendorEffect.getAdaptiveScale(), vibrationId); if (duration > 0) { mCurrentAmplitude = -1; - notifyListenerOnVibrating(true); + updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); } return duration; } finally { @@ -346,7 +348,7 @@ final class VibratorController { prebaked.getEffectStrength(), vibrationId); if (duration > 0) { mCurrentAmplitude = -1; - notifyListenerOnVibrating(true); + updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); } return duration; } @@ -374,7 +376,7 @@ final class VibratorController { long duration = mNativeWrapper.compose(primitives, vibrationId); if (duration > 0) { mCurrentAmplitude = -1; - notifyListenerOnVibrating(true); + updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); } return duration; } @@ -402,7 +404,7 @@ final class VibratorController { long duration = mNativeWrapper.composePwle(primitives, braking, vibrationId); if (duration > 0) { mCurrentAmplitude = -1; - notifyListenerOnVibrating(true); + updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); } return duration; } @@ -422,7 +424,7 @@ final class VibratorController { synchronized (mLock) { mNativeWrapper.off(); mCurrentAmplitude = 0; - notifyListenerOnVibrating(false); + updateStateAndNotifyListenersLocked(VibratorState.IDLE); } } finally { Trace.traceEnd(TRACE_TAG_VIBRATOR); @@ -443,9 +445,8 @@ final class VibratorController { return "VibratorController{" + "mVibratorInfo=" + mVibratorInfo + ", mVibratorInfoLoadSuccessful=" + mVibratorInfoLoadSuccessful - + ", mIsVibrating=" + mIsVibrating + + ", mCurrentState=" + mCurrentState.name() + ", mCurrentAmplitude=" + mCurrentAmplitude - + ", mIsUnderExternalControl=" + mIsUnderExternalControl + ", mVibratorStateListeners count=" + mVibratorStateListeners.getRegisteredCallbackCount() + '}'; @@ -454,8 +455,7 @@ final class VibratorController { void dump(IndentingPrintWriter pw) { pw.println("Vibrator (id=" + mVibratorInfo.getId() + "):"); pw.increaseIndent(); - pw.println("isVibrating = " + mIsVibrating); - pw.println("isUnderExternalControl = " + mIsUnderExternalControl); + pw.println("currentState = " + mCurrentState.name()); pw.println("currentAmplitude = " + mCurrentAmplitude); pw.println("vibratorInfoLoadSuccessful = " + mVibratorInfoLoadSuccessful); pw.println("vibratorStateListener size = " @@ -464,14 +464,19 @@ final class VibratorController { pw.decreaseIndent(); } + /** + * Updates current vibrator state and notify listeners if {@link #isVibrating()} result changed. + */ @GuardedBy("mLock") - private void notifyListenerOnVibrating(boolean isVibrating) { - if (mIsVibrating != isVibrating) { - mIsVibrating = isVibrating; + private void updateStateAndNotifyListenersLocked(VibratorState state) { + boolean previousIsVibrating = isVibrating(mCurrentState); + final boolean newIsVibrating = isVibrating(state); + mCurrentState = state; + if (previousIsVibrating != newIsVibrating) { // The broadcast method is safe w.r.t. register/unregister listener methods, but lock // is required here to guarantee delivery order. mVibratorStateListeners.broadcast( - listener -> notifyStateListener(listener, isVibrating)); + listener -> notifyStateListener(listener, newIsVibrating)); } } @@ -483,6 +488,11 @@ final class VibratorController { } } + /** Returns true only if given state is not {@link VibratorState#IDLE}. */ + private static boolean isVibrating(VibratorState state) { + return state != VibratorState.IDLE; + } + /** Wrapper around the static-native methods of {@link VibratorController} for tests. */ @VisibleForTesting public static class NativeWrapper { diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 95c648334327..07473d10b217 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -809,17 +809,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mCurrentExternalVibration.getDebugInfo().dump(proto, VibratorManagerServiceDumpProto.CURRENT_EXTERNAL_VIBRATION); } - - boolean isVibrating = false; - boolean isUnderExternalControl = false; for (int i = 0; i < mVibrators.size(); i++) { proto.write(VibratorManagerServiceDumpProto.VIBRATOR_IDS, mVibrators.keyAt(i)); - isVibrating |= mVibrators.valueAt(i).isVibrating(); - isUnderExternalControl |= mVibrators.valueAt(i).isUnderExternalControl(); } - proto.write(VibratorManagerServiceDumpProto.IS_VIBRATING, isVibrating); - proto.write(VibratorManagerServiceDumpProto.VIBRATOR_UNDER_EXTERNAL_CONTROL, - isUnderExternalControl); } mVibratorManagerRecords.dump(proto); mVibratorControlService.dump(proto); diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java index 67401530763b..ab5316f46d78 100644 --- a/services/core/java/com/android/server/webkit/SystemImpl.java +++ b/services/core/java/com/android/server/webkit/SystemImpl.java @@ -41,7 +41,8 @@ import android.webkit.WebViewZygote; import com.android.internal.util.XmlUtils; import com.android.server.LocalServices; -import com.android.server.PinnerService; +import com.android.server.pinner.PinnedFile; +import com.android.server.pinner.PinnerService; import org.xmlpull.v1.XmlPullParserException; @@ -318,8 +319,9 @@ public class SystemImpl implements SystemInterface { if (webviewPinQuota <= 0) { break; } - int bytesPinned = pinnerService.pinFile(apk, webviewPinQuota, appInfo, PIN_GROUP); - webviewPinQuota -= bytesPinned; + PinnedFile pf = pinnerService.pinFile( + apk, webviewPinQuota, appInfo, PIN_GROUP, /*pinOptimizedDeps=*/true); + webviewPinQuota -= pf.bytesPinned; } } diff --git a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java index 241390c12818..fbf9478b4fd9 100644 --- a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java @@ -170,7 +170,7 @@ class AppCompatCameraOverrides { * </ul> */ boolean shouldApplyFreeformTreatmentForCameraCompat() { - return Flags.cameraCompatForFreeform() && !isChangeEnabled(mActivityRecord, + return Flags.enableCameraCompatForDesktopWindowing() && !isChangeEnabled(mActivityRecord, OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT); } diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java index 67bfd7605128..5338c01666fe 100644 --- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java @@ -48,8 +48,9 @@ class AppCompatCameraPolicy { // without the need to restart the device. final boolean needsDisplayRotationCompatPolicy = wmService.mAppCompatConfiguration.isCameraCompatTreatmentEnabledAtBuildTime(); - final boolean needsCameraCompatFreeformPolicy = Flags.cameraCompatForFreeform() - && DesktopModeHelper.canEnterDesktopMode(wmService.mContext); + final boolean needsCameraCompatFreeformPolicy = + Flags.enableCameraCompatForDesktopWindowing() + && DesktopModeHelper.canEnterDesktopMode(wmService.mContext); if (needsDisplayRotationCompatPolicy || needsCameraCompatFreeformPolicy) { mCameraStateMonitor = new CameraStateMonitor(displayContent, wmService.mH); mActivityRefresher = new ActivityRefresher(wmService, wmService.mH); diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 2259b5a5b08c..515f148ac2ff 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -1120,7 +1120,9 @@ public class BackgroundActivityStartController { @Nullable Task targetTask, int launchFlags, int balCode, int callingUid, int realCallingUid, TaskDisplayArea preferredTaskDisplayArea) { // BAL Exception allowed in all cases - if (balCode == BAL_ALLOW_ALLOWLISTED_UID) { + if (balCode == BAL_ALLOW_ALLOWLISTED_UID + || (android.security.Flags.asmReintroduceGracePeriod() + && balCode == BAL_ALLOW_GRACE_PERIOD)) { return true; } @@ -1173,10 +1175,15 @@ public class BackgroundActivityStartController { ArrayList<Task> visibleTasks = displayArea.getVisibleTasks(); for (int i = 0; i < visibleTasks.size(); i++) { Task task = visibleTasks.get(i); - if (visibleTasks.size() == 1 && task.isActivityTypeHomeOrRecents()) { - bas.optedIn(task.getTopMostActivity()); - } else { + if (android.security.Flags.asmReintroduceGracePeriod()) { bas = checkTopActivityForAsm(task, callingUid, /*sourceRecord*/null, bas); + } else { + if (visibleTasks.size() == 1 && task.isActivityTypeHomeOrRecents()) { + bas.optedIn(task.getTopMostActivity()); + } else { + bas = checkTopActivityForAsm( + task, callingUid, /*sourceRecord*/null, bas); + } } } } diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java index e3232e08749e..d6caa1a248b4 100644 --- a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java +++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java @@ -124,7 +124,7 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa */ @VisibleForTesting boolean shouldApplyFreeformTreatmentForCameraCompat(@NonNull ActivityRecord activity) { - return Flags.cameraCompatForFreeform() && !activity.info.isChangeEnabled( + return Flags.enableCameraCompatForDesktopWindowing() && !activity.info.isChangeEnabled( ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index b8f47cce6005..942634704ff5 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -9007,7 +9007,9 @@ public class WindowManagerService extends IWindowManager.Stub final boolean isInputTargetNotFocused = mFocusedInputTarget != t && mFocusedInputTarget != null; - if (!isInputTargetNotFocused) { + final boolean isTouchOnFocusedDisplay = mFocusedInputTarget != null + && t.getDisplayId() == mFocusedInputTarget.getDisplayId(); + if (!(isInputTargetNotFocused && isTouchOnFocusedDisplay)) { return false; } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 4c4b4f65edf5..0e3ab63aefb9 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1969,6 +1969,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP boolean isReadyForDisplay() { final boolean parentAndClientVisible = !isParentWindowHidden() && mViewVisibility == View.VISIBLE; + // TODO(b/338426357): Remove this once the last target using legacy transitions is moved to + // shell transitions + if (!mTransitionController.isShellTransitionsEnabled()) { + return mHasSurface && isVisibleByPolicy() && !mDestroying + && ((parentAndClientVisible && mToken.isVisible()) + || isAnimating(TRANSITION | PARENTS)); + } return mHasSurface && isVisibleByPolicy() && !mDestroying && mToken.isVisible() && (parentAndClientVisible || isAnimating(TRANSITION | PARENTS)); } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 5cf260adece6..ce6f1ecc9463 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -205,6 +205,7 @@ import com.android.server.os.SchedulingPolicyService; import com.android.server.pdb.PersistentDataBlockService; import com.android.server.people.PeopleService; import com.android.server.permission.access.AccessCheckingService; +import com.android.server.pinner.PinnerService; import com.android.server.pm.ApexManager; import com.android.server.pm.ApexSystemServiceInfo; import com.android.server.pm.BackgroundInstallControlService; diff --git a/services/tests/powerstatstests/res/xml/irq_device_map_3.xml b/services/tests/powerstatstests/res/xml/irq_device_map_3.xml index fd55428c48df..c3df0785bd9b 100644 --- a/services/tests/powerstatstests/res/xml/irq_device_map_3.xml +++ b/services/tests/powerstatstests/res/xml/irq_device_map_3.xml @@ -32,4 +32,7 @@ <device name="test.sensor.device"> <subsystem>Sensor</subsystem> </device> + <device name="test.bluetooth.device"> + <subsystem>Bluetooth</subsystem> + </device> </irq-device-map>
\ No newline at end of file diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java index 0dc836ba0400..fe4d971face5 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java @@ -17,6 +17,7 @@ package com.android.server.power.stats.wakeups; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM; +import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_BLUETOOTH; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER; @@ -52,6 +53,7 @@ public class CpuWakeupStatsTest { private static final String KERNEL_REASON_SOUND_TRIGGER_IRQ = "129 test.sound_trigger.device"; private static final String KERNEL_REASON_SENSOR_IRQ = "15 test.sensor.device"; private static final String KERNEL_REASON_CELLULAR_DATA_IRQ = "18 test.cellular_data.device"; + private static final String KERNEL_REASON_BLUETOOTH_IRQ = "19 test.bluetooth.device"; private static final String KERNEL_REASON_UNKNOWN_IRQ = "140 test.unknown.device"; private static final String KERNEL_REASON_UNKNOWN_FORMAT = "free-form-reason test.alarm.device"; private static final String KERNEL_REASON_ALARM_ABNORMAL = "-1 test.alarm.device"; @@ -62,12 +64,14 @@ public class CpuWakeupStatsTest { private static final int TEST_UID_3 = 92261423; private static final int TEST_UID_4 = 56926423; private static final int TEST_UID_5 = 76421423; + private static final int TEST_UID_6 = 62345353; private static final int TEST_PROC_STATE_1 = 72331; private static final int TEST_PROC_STATE_2 = 792351; private static final int TEST_PROC_STATE_3 = 138831; private static final int TEST_PROC_STATE_4 = 23231; private static final int TEST_PROC_STATE_5 = 42; + private static final int TEST_PROC_STATE_6 = 129942; private static final Context sContext = InstrumentationRegistry.getTargetContext(); private final Handler mHandler = Mockito.mock(Handler.class); @@ -79,6 +83,7 @@ public class CpuWakeupStatsTest { obj.mUidProcStates.put(TEST_UID_3, TEST_PROC_STATE_3); obj.mUidProcStates.put(TEST_UID_4, TEST_PROC_STATE_4); obj.mUidProcStates.put(TEST_UID_5, TEST_PROC_STATE_5); + obj.mUidProcStates.put(TEST_UID_6, TEST_PROC_STATE_6); } @Test @@ -118,6 +123,7 @@ public class CpuWakeupStatsTest { CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER, CPU_WAKEUP_SUBSYSTEM_SENSOR, CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA, + CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, }; final String[] kernelReasons = new String[] { @@ -126,10 +132,11 @@ public class CpuWakeupStatsTest { KERNEL_REASON_SOUND_TRIGGER_IRQ, KERNEL_REASON_SENSOR_IRQ, KERNEL_REASON_CELLULAR_DATA_IRQ, + KERNEL_REASON_BLUETOOTH_IRQ, }; final int[] uids = new int[] { - TEST_UID_2, TEST_UID_3, TEST_UID_4, TEST_UID_1, TEST_UID_5 + TEST_UID_2, TEST_UID_3, TEST_UID_4, TEST_UID_1, TEST_UID_5, TEST_UID_6 }; final int[] procStates = new int[] { @@ -137,7 +144,8 @@ public class CpuWakeupStatsTest { TEST_PROC_STATE_3, TEST_PROC_STATE_4, TEST_PROC_STATE_1, - TEST_PROC_STATE_5 + TEST_PROC_STATE_5, + TEST_PROC_STATE_6 }; final int total = subsystems.length; @@ -285,6 +293,40 @@ public class CpuWakeupStatsTest { } @Test + public void bluetoothIrqAttributionSolo() { + final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); + final long wakeupTime = 1236121; + + populateDefaultProcStates(obj); + + obj.noteWakeupTimeAndReason(wakeupTime, 1, KERNEL_REASON_BLUETOOTH_IRQ); + + // Outside the window, so should be ignored. + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, + wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_1); + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, + wakeupTime + obj.mConfig.WAKEUP_MATCHING_WINDOW_MS + 1, TEST_UID_2); + // Should be attributed + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime + 5, TEST_UID_3, + TEST_UID_5); + + final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime); + assertThat(attribution).isNotNull(); + assertThat(attribution.size()).isEqualTo(1); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)).isTrue(); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey( + TEST_UID_1)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey( + TEST_UID_2)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_3)).isEqualTo( + TEST_PROC_STATE_3); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey( + TEST_UID_4)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_5)).isEqualTo( + TEST_PROC_STATE_5); + } + + @Test public void alarmAndWifiIrqAttribution() { final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); final long wakeupTime = 92123210; @@ -400,6 +442,47 @@ public class CpuWakeupStatsTest { } @Test + public void unknownAndBluetoothAttribution() { + final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); + final long wakeupTime = 92123520; + + populateDefaultProcStates(obj); + + obj.noteWakeupTimeAndReason(wakeupTime, 24, + KERNEL_REASON_UNKNOWN_IRQ + ":" + KERNEL_REASON_BLUETOOTH_IRQ); + + // Bluetooth activity + // Outside the window, so should be ignored. + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, + wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_4); + // Should be attributed + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime + 2, TEST_UID_1); + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime - 1, TEST_UID_3, + TEST_UID_5); + + // Unrelated, should be ignored. + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3); + + final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime); + assertThat(attribution).isNotNull(); + assertThat(attribution.size()).isEqualTo(2); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)).isTrue(); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_1)).isEqualTo( + TEST_PROC_STATE_1); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH) + .indexOfKey(TEST_UID_2)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_3)).isEqualTo( + TEST_PROC_STATE_3); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH) + .indexOfKey(TEST_UID_4)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_5)).isEqualTo( + TEST_PROC_STATE_5); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isTrue(); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isNull(); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_ALARM)).isFalse(); + } + + @Test public void unknownFormatWakeupIgnored() { final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); final long wakeupTime = 72123210; diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index ac1b7c6876f7..cbe6700f4d41 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -52,6 +52,7 @@ android_test { "services.credentials", "services.devicepolicy", "services.flags", + "com.android.server.flags.services-aconfig-java", "services.net", "services.people", "services.supervision", @@ -81,6 +82,7 @@ android_test { // TODO: remove once Android migrates to JUnit 4.12, // which provides assertThrows "testng", + "flag-junit", "junit", "junit-params", "ActivityContext", diff --git a/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java b/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java index ec78bcea7539..c18faef2c028 100644 --- a/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java @@ -31,6 +31,9 @@ import android.content.pm.ResolveInfo; import android.os.Binder; import android.os.Handler; import android.os.Looper; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; import android.provider.DeviceConfigInterface; import android.testing.TestableContext; @@ -43,6 +46,9 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.server.flags.Flags; +import com.android.server.pinner.PinnedFile; +import com.android.server.pinner.PinnerService; import com.android.server.testutils.FakeDeviceConfigInterface; import com.android.server.wm.ActivityTaskManagerInternal; @@ -73,15 +79,18 @@ public class PinnerServiceTest { private static final long WAIT_FOR_PINNER_TIMEOUT = TimeUnit.SECONDS.toMillis(2); + private static final int MEMORY_PERCENTAGE_FOR_QUOTA = 10; + @Rule public TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext(), null); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private final ArraySet<String> mUpdatedPackages = new ArraySet<>(); private ResolveInfo mHomePackageResolveInfo; private FakeDeviceConfigInterface mFakeDeviceConfigInterface; private PinnerService.Injector mInjector; - @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -114,6 +123,8 @@ public class PinnerServiceTest { resources.addOverride(com.android.internal.R.bool.config_pinnerCameraApp, false); resources.addOverride(com.android.internal.R.integer.config_pinnerHomePinBytes, 0); resources.addOverride(com.android.internal.R.bool.config_pinnerAssistantApp, false); + resources.addOverride(com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, + MEMORY_PERCENTAGE_FOR_QUOTA); mFakeDeviceConfigInterface = new FakeDeviceConfigInterface(); setDeviceConfigPinnedAnonSize(0); @@ -138,10 +149,9 @@ public class PinnerServiceTest { } @Override - protected PinnerService.PinnedFile pinFileInternal(String fileToPin, - int maxBytesToPin, boolean attemptPinIntrospection) { - return new PinnerService.PinnedFile(-1, - maxBytesToPin, fileToPin, maxBytesToPin); + protected PinnedFile pinFileInternal(PinnerService service, String fileToPin, + long maxBytesToPin, boolean attemptPinIntrospection) { + return new PinnedFile(-1, maxBytesToPin, fileToPin, maxBytesToPin); } }; } @@ -167,6 +177,12 @@ public class PinnerServiceTest { unpinAnonRegionMethod.invoke(pinnerService); } + private long getGlobalPinQuota(PinnerService service) throws Exception { + Method getQuotaMethod = PinnerService.class.getDeclaredMethod("getAvailableGlobalQuota"); + getQuotaMethod.setAccessible(true); + return (long) getQuotaMethod.invoke(service); + } + private void waitForPinnerService(PinnerService pinnerService) throws NoSuchFieldException, IllegalAccessException { // There's no notification/callback when pinning finished @@ -315,15 +331,121 @@ public class PinnerServiceTest { PinnerService pinnerService = new PinnerService(mContext, mInjector); pinnerService.onStart(); - pinnerService.pinFile("test_file", 4096, null, "my_group"); + pinnerService.pinFile("test_file", 4096, null, "my_group", false); - assertThat(getPinnedSize(pinnerService)).isGreaterThan(0); - assertThat(getTotalPinnedFiles(pinnerService)).isGreaterThan(0); + assertThat(getPinnedSize(pinnerService)).isEqualTo(4096); + assertThat(getTotalPinnedFiles(pinnerService)).isEqualTo(1); + + unpinAll(pinnerService); + } + + @Test + @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA) + public void testPinAllQuota() throws Exception { + PinnerService pinnerService = new PinnerService(mContext, mInjector); + pinnerService.onStart(); + + long quota = getGlobalPinQuota(pinnerService); + + pinnerService.pinFile("test_file", Long.MAX_VALUE, null, "my_group", false); + + assertThat(getPinnedSize(pinnerService)).isEqualTo(quota); unpinAll(pinnerService); } @Test + @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA) + public void testGlobalPinQuotaAsDevicePercentage() throws Exception { + PinnerService pinnerService = new PinnerService(mContext, mInjector); + pinnerService.onStart(); + long origQuota = getGlobalPinQuota(pinnerService); + + long totalMem = android.os.Process.getTotalMemory(); + + // Verify that pin quota is the set percentage of device total memory + assertThat(origQuota).isEqualTo((totalMem * MEMORY_PERCENTAGE_FOR_QUOTA) / 100); + + pinnerService.pinFile("test_file", 4096, null, "my_group", false); + assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(origQuota - 4096); + } + + @Test + @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA) + public void testGlobalPinWhenNoQuota() throws Exception { + TestableResources resources = mContext.getOrCreateTestableResources(); + resources.addOverride( + com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, 0); + + PinnerService pinnerService = new PinnerService(mContext, mInjector); + pinnerService.onStart(); + + // Verify that pin quota is zero + assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(0); + + pinnerService.pinFile("test_file", 4096, null, "my_group", false); + assertThat(getTotalPinnedFiles(pinnerService)).isEqualTo(0); + } + + /** + * This test is temporary, it should be cleaned up when removing the pin_global_quota bugfix + * flag. + */ + @Test + @DisableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA) + public void testGlobalQuotaDisabled() throws Exception { + TestableResources resources = mContext.getOrCreateTestableResources(); + resources.addOverride( + com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, 0); + + PinnerService pinnerService = new PinnerService(mContext, mInjector); + pinnerService.onStart(); + + // The quota parameter exists but it should have no effect on pinning + long quota = getGlobalPinQuota(pinnerService); + + pinnerService.pinFile("test_file", quota + 1, null, "my_group", false); + + // Verify that we can pin past the quota as it is disabled + assertThat(getPinnedSize(pinnerService)).isEqualTo(quota + 1); + } + + @Test + @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA) + public void testUnpinReleasesQuota() throws Exception { + PinnerService pinnerService = new PinnerService(mContext, mInjector); + pinnerService.onStart(); + long origQuota = getGlobalPinQuota(pinnerService); + + // Verify that pin quota exists and is non zero. + assertThat(getGlobalPinQuota(pinnerService)).isGreaterThan(0); + + pinnerService.pinFile("test_file", origQuota, null, "my_group", false); + + // Make sure all the quota was consumed + assertThat(getPinnedSize(pinnerService)).isEqualTo(origQuota); + + // Unpin the file and verify that the quota has been released. + pinnerService.unpinFile("test_file"); + assertThat(getPinnedSize(pinnerService)).isEqualTo(0); + assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(origQuota); + } + + @Test + @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA) + public void testGlobalPinQuotaNegative() throws Exception { + TestableResources resources = mContext.getOrCreateTestableResources(); + resources.addOverride( + com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, -10); + + PinnerService pinnerService = new PinnerService(mContext, mInjector); + pinnerService.onStart(); + + // Verify that pin quota is zero + assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(0); + } + + @Test public void testPinAnonRegion() throws Exception { setDeviceConfigPinnedAnonSize(32768); diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java index abc9ce3fdc36..ee63d5d32ff1 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java @@ -38,6 +38,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; @@ -91,6 +92,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -174,8 +176,8 @@ public class MediaProjectionManagerServiceTest { private PackageManager mPackageManager; @Mock private KeyguardManager mKeyguardManager; - @Mock - AppOpsManager mAppOpsManager; + + private AppOpsManager mAppOpsManager; @Mock private IMediaProjectionWatcherCallback mWatcherCallback; @Mock @@ -193,6 +195,7 @@ public class MediaProjectionManagerServiceTest { LocalServices.removeServiceForTest(WindowManagerInternal.class); LocalServices.addService(WindowManagerInternal.class, mWindowManagerInternal); + mAppOpsManager = mockAppOpsManager(); mContext.addMockSystemService(AppOpsManager.class, mAppOpsManager); mContext.addMockSystemService(KeyguardManager.class, mKeyguardManager); mContext.setMockPackageManager(mPackageManager); @@ -206,6 +209,17 @@ public class MediaProjectionManagerServiceTest { mService = new MediaProjectionManagerService(mContext); } + private static AppOpsManager mockAppOpsManager() { + return mock(AppOpsManager.class, invocationOnMock -> { + if (invocationOnMock.getMethod().getName().startsWith("noteOp")) { + // Mockito will return 0 for non-stubbed method which corresponds to MODE_ALLOWED + // and is not what we want. + return AppOpsManager.MODE_IGNORED; + } + return Answers.RETURNS_DEFAULTS.answer(invocationOnMock); + }); + } + @After public void tearDown() { LocalServices.removeServiceForTest(ActivityManagerInternal.class); @@ -305,8 +319,10 @@ public class MediaProjectionManagerServiceTest { public void testCreateProjection_keyguardLocked_AppOpMediaProjection() throws NameNotFoundException { MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); - doReturn(true).when(mAppOpsManager).isOperationActive(eq(AppOpsManager.OP_PROJECT_MEDIA), - eq(projection.uid), eq(projection.packageName)); + doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager) + .noteOpNoThrow(eq(AppOpsManager.OP_PROJECT_MEDIA), + eq(projection.uid), eq(projection.packageName), nullable(String.class), + nullable(String.class)); doReturn(true).when(mKeyguardManager).isKeyguardLocked(); doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission( @@ -1159,7 +1175,7 @@ public class MediaProjectionManagerServiceTest { doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(), any(ApplicationInfoFlags.class), any(UserHandle.class)); return service.createProjectionInternal(UID, PACKAGE_NAME, - TYPE_MIRRORING, /* isPermanentGrant= */ true, UserHandle.CURRENT); + TYPE_MIRRORING, /* isPermanentGrant= */ false, UserHandle.CURRENT); } // Set up preconditions for starting a projection, with no foreground service requirements. diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 6c9015d72d5a..44770d21892c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -4652,7 +4652,42 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { doThrow(new SecurityException("no access")).when(mUgmInternal) .checkGrantUriPermission(eq(Process.myUid()), any(), eq(soundUri), - anyInt(), eq(Process.myUserHandle().getIdentifier())); + anyInt(), eq(Process.myUserHandle().getIdentifier())); + + mBinderService.updateNotificationChannelFromPrivilegedListener( + null, mPkg, Process.myUserHandle(), updatedNotificationChannel); + + verify(mPreferencesHelper, times(1)).updateNotificationChannel( + anyString(), anyInt(), any(), anyBoolean(), anyInt(), anyBoolean()); + + verify(mListeners, never()).notifyNotificationChannelChanged(eq(mPkg), + eq(Process.myUserHandle()), eq(mTestNotificationChannel), + eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED)); + } + + @Test + public void + testUpdateNotificationChannelFromPrivilegedListener_oldSoundNoUriPerm_newSoundHasUriPerm() + throws Exception { + mService.setPreferencesHelper(mPreferencesHelper); + when(mCompanionMgr.getAssociations(mPkg, mUserId)) + .thenReturn(singletonList(mock(AssociationInfo.class))); + when(mPreferencesHelper.getNotificationChannel(eq(mPkg), anyInt(), + eq(mTestNotificationChannel.getId()), anyBoolean())) + .thenReturn(mTestNotificationChannel); + + // Missing Uri permissions for the old channel sound + final Uri oldSoundUri = Settings.System.DEFAULT_NOTIFICATION_URI; + doThrow(new SecurityException("no access")).when(mUgmInternal) + .checkGrantUriPermission(eq(Process.myUid()), any(), eq(oldSoundUri), + anyInt(), eq(Process.myUserHandle().getIdentifier())); + + // Has Uri permissions for the old channel sound + final Uri newSoundUri = Uri.parse("content://media/test/sound/uri"); + final NotificationChannel updatedNotificationChannel = new NotificationChannel( + TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT); + updatedNotificationChannel.setSound(newSoundUri, + updatedNotificationChannel.getAudioAttributes()); mBinderService.updateNotificationChannelFromPrivilegedListener( null, mPkg, Process.myUserHandle(), updatedNotificationChannel); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index a0c0df8853f9..d64b9e858c64 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -45,11 +45,13 @@ import static android.app.NotificationManager.IMPORTANCE_MAX; import static android.app.NotificationManager.IMPORTANCE_NONE; import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; import static android.app.NotificationManager.VISIBILITY_NO_OVERRIDE; +import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE; +import static android.content.ContentResolver.SCHEME_CONTENT; +import static android.content.ContentResolver.SCHEME_FILE; import static android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION; import static android.media.AudioAttributes.USAGE_NOTIFICATION; import static android.os.UserHandle.USER_ALL; import static android.os.UserHandle.USER_SYSTEM; - import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION; import static android.service.notification.Flags.notificationClassification; @@ -59,6 +61,7 @@ import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_P import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED; import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL; +import static com.android.server.notification.Flags.FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI; import static com.android.server.notification.Flags.FLAG_PERSIST_INCOMPLETE_RESTORE_DATA; import static com.android.server.notification.NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_UPDATED_BY_USER; import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE; @@ -84,6 +87,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -369,10 +373,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - false, mClock); + mUgmInternal, false, mClock); mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - false, mClock); + mUgmInternal, false, mClock); resetZenModeHelper(); mAudioAttributes = new AudioAttributes.Builder() @@ -783,7 +787,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testReadXml_oldXml_migrates() throws Exception { mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - /* showReviewPermissionsNotification= */ true, mClock); + mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); String xml = "<ranking version=\"2\">\n" + "<package name=\"" + PKG_N_MR1 + "\" uid=\"" + UID_N_MR1 @@ -919,7 +923,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception { mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - /* showReviewPermissionsNotification= */ true, mClock); + mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); String xml = "<ranking version=\"3\">\n" + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n" @@ -978,7 +982,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testReadXml_newXml_permissionNotificationOff() throws Exception { mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - /* showReviewPermissionsNotification= */ false, mClock); + mUgmInternal, /* showReviewPermissionsNotification= */ false, mClock); String xml = "<ranking version=\"3\">\n" + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n" @@ -1037,7 +1041,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception { mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - /* showReviewPermissionsNotification= */ true, mClock); + mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); String xml = "<ranking version=\"4\">\n" + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n" @@ -1709,7 +1713,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { // simulate load after reboot mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - false, mClock); + mUgmInternal, false, mClock); loadByteArrayXml(baos.toByteArray(), false, USER_ALL); // Trigger 2nd restore pass @@ -1764,7 +1768,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { // simulate load after reboot mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - false, mClock); + mUgmInternal, false, mClock); loadByteArrayXml(xml.getBytes(), false, USER_ALL); // Trigger 2nd restore pass @@ -1842,10 +1846,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - false, mClock); + mUgmInternal, false, mClock); mXmlHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - false, mClock); + mUgmInternal, false, mClock); NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_LOW); @@ -3049,6 +3053,64 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) + public void testCreateChannel_noSoundUriPermission_contentSchemeVerified() { + final Uri sound = Uri.parse(SCHEME_CONTENT + "://media/test/sound/uri"); + + doThrow(new SecurityException("no access")).when(mUgmInternal) + .checkGrantUriPermission(eq(UID_N_MR1), any(), eq(sound), + anyInt(), eq(Process.myUserHandle().getIdentifier())); + + final NotificationChannel channel = new NotificationChannel("id2", "name2", + NotificationManager.IMPORTANCE_DEFAULT); + channel.setSound(sound, mAudioAttributes); + + assertThrows(SecurityException.class, + () -> mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, + true, false, UID_N_MR1, false)); + assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true)) + .isNull(); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) + public void testCreateChannel_noSoundUriPermission_fileSchemaIgnored() { + final Uri sound = Uri.parse(SCHEME_FILE + "://path/sound"); + + doThrow(new SecurityException("no access")).when(mUgmInternal) + .checkGrantUriPermission(eq(UID_N_MR1), any(), any(), + anyInt(), eq(Process.myUserHandle().getIdentifier())); + + final NotificationChannel channel = new NotificationChannel("id2", "name2", + NotificationManager.IMPORTANCE_DEFAULT); + channel.setSound(sound, mAudioAttributes); + + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true) + .getSound()).isEqualTo(sound); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) + public void testCreateChannel_noSoundUriPermission_resourceSchemaIgnored() { + final Uri sound = Uri.parse(SCHEME_ANDROID_RESOURCE + "://resId/sound"); + + doThrow(new SecurityException("no access")).when(mUgmInternal) + .checkGrantUriPermission(eq(UID_N_MR1), any(), any(), + anyInt(), eq(Process.myUserHandle().getIdentifier())); + + final NotificationChannel channel = new NotificationChannel("id2", "name2", + NotificationManager.IMPORTANCE_DEFAULT); + channel.setSound(sound, mAudioAttributes); + + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true) + .getSound()).isEqualTo(sound); + } + + @Test public void testPermanentlyDeleteChannels() throws Exception { NotificationChannel channel1 = new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java index 0d13be6d5ab2..e8ca8bf8ec63 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java @@ -127,13 +127,13 @@ public class VibratorControllerTest { public void setExternalControl_withCapability_enablesExternalControl() { mockVibratorCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); VibratorController controller = createController(); - assertFalse(controller.isUnderExternalControl()); + assertFalse(controller.isVibrating()); controller.setExternalControl(true); - assertTrue(controller.isUnderExternalControl()); + assertTrue(controller.isVibrating()); controller.setExternalControl(false); - assertFalse(controller.isUnderExternalControl()); + assertFalse(controller.isVibrating()); InOrder inOrderVerifier = inOrder(mNativeWrapperMock); inOrderVerifier.verify(mNativeWrapperMock).setExternalControl(eq(true)); @@ -143,10 +143,10 @@ public class VibratorControllerTest { @Test public void setExternalControl_withNoCapability_ignoresExternalControl() { VibratorController controller = createController(); - assertFalse(controller.isUnderExternalControl()); + assertFalse(controller.isVibrating()); controller.setExternalControl(true); - assertFalse(controller.isUnderExternalControl()); + assertFalse(controller.isVibrating()); verify(mNativeWrapperMock, never()).setExternalControl(anyBoolean()); } @@ -181,6 +181,38 @@ public class VibratorControllerTest { } @Test + public void setAmplitude_vibratorIdle_ignoresAmplitude() { + VibratorController controller = createController(); + assertFalse(controller.isVibrating()); + + controller.setAmplitude(1); + assertEquals(0, controller.getCurrentAmplitude(), /* delta= */ 0); + } + + @Test + public void setAmplitude_vibratorUnderExternalControl_ignoresAmplitude() { + mockVibratorCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); + VibratorController controller = createController(); + controller.setExternalControl(true); + assertTrue(controller.isVibrating()); + + controller.setAmplitude(1); + assertEquals(0, controller.getCurrentAmplitude(), /* delta= */ 0); + } + + @Test + public void setAmplitude_vibratorVibrating_setsAmplitude() { + when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0)); + VibratorController controller = createController(); + controller.on(100, /* vibrationId= */ 1); + assertTrue(controller.isVibrating()); + assertEquals(-1, controller.getCurrentAmplitude(), /* delta= */ 0); + + controller.setAmplitude(1); + assertEquals(1, controller.getCurrentAmplitude(), /* delta= */ 0); + } + + @Test public void on_withDuration_turnsVibratorOn() { when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0)); VibratorController controller = createController(); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java index d99b20c689dd..538c3fc2ddae 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -266,12 +266,13 @@ public class VibratorManagerServiceTest { @After public void tearDown() throws Exception { if (mService != null) { - if (!mPendingVibrations.stream().allMatch(HalVibration::hasEnded)) { - // Cancel any pending vibration from tests. - cancelVibrate(mService); - for (HalVibration vibration : mPendingVibrations) { - vibration.waitForEnd(); - } + // Make sure we have permission to cancel test vibrations, even if the test denied them. + grantPermission(android.Manifest.permission.VIBRATE); + // Cancel any pending vibration from tests, including external vibrations. + cancelVibrate(mService); + // Wait until pending vibrations end asynchronously. + for (HalVibration vibration : mPendingVibrations) { + vibration.waitForEnd(); } // Wait until all vibrators have stopped vibrating, waiting for ramp-down. // Note: if a test is flaky here something is wrong with the vibration finalization. @@ -2242,7 +2243,7 @@ public class VibratorManagerServiceTest { VibratorManagerService service = createSystemReadyService(); VibrationEffect effect = VibrationEffect.createOneShot(10 * TEST_TIMEOUT_MILLIS, 100); - vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS); + HalVibration vibration = vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS); // VibrationThread will start this vibration async, so wait until vibration is triggered. assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); @@ -2255,7 +2256,8 @@ public class VibratorManagerServiceTest { assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); // Vibration is cancelled. - assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + vibration.waitForEnd(); + assertThat(vibration.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED); assertEquals(Arrays.asList(false, true), mVibratorProviders.get(1).getExternalControlStates()); } @@ -2296,7 +2298,7 @@ public class VibratorManagerServiceTest { VibrationEffect repeatingEffect = VibrationEffect.createWaveform( new long[]{100, 200, 300}, new int[]{128, 255, 255}, 1); - vibrate(service, repeatingEffect, ALARM_ATTRS); + HalVibration repeatingVibration = vibrate(service, repeatingEffect, ALARM_ATTRS); // VibrationThread will start this vibration async, so wait until vibration is triggered. assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); @@ -2308,7 +2310,8 @@ public class VibratorManagerServiceTest { assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); // Vibration is cancelled. - assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + repeatingVibration.waitForEnd(); + assertThat(repeatingVibration.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED); assertEquals(Arrays.asList(false, true), mVibratorProviders.get(1).getExternalControlStates()); } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java index d66c21a77fcd..b91a5b7afe26 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java @@ -28,7 +28,7 @@ import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_V import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM; +import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import android.compat.testing.PlatformCompatChangeRule; import android.platform.test.annotations.DisableFlags; @@ -218,7 +218,7 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase { } @Test - @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testShouldApplyCameraCompatFreeformTreatment_flagIsDisabled_returnsFalse() { runTestScenario((robot) -> { robot.activity().createActivityWithComponentInNewTask(); @@ -229,7 +229,7 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase { @Test @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT}) - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() { runTestScenario((robot) -> { robot.activity().createActivityWithComponentInNewTask(); @@ -240,7 +240,7 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase { @Test @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT}) - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testShouldApplyCameraCompatFreeformTreatment_disabledByOverride_returnsFalse() { runTestScenario((robot) -> { robot.activity().createActivityWithComponentInNewTask(); @@ -250,7 +250,7 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase { } @Test - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() { runTestScenario((robot) -> { robot.activity().createActivityWithComponentInNewTask(); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java index d91b38efd40b..41102d6922da 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java @@ -20,7 +20,7 @@ import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM; +import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -85,7 +85,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraCompatFreeformPolicy_presentWhenEnabledAndDW() { runTestScenario((robot) -> { robot.allowEnterDesktopMode(/* isAllowed= */ true); @@ -95,7 +95,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraCompatFreeformPolicy_notPresentWhenNoDW() { runTestScenario((robot) -> { robot.allowEnterDesktopMode(/* isAllowed= */ false); @@ -105,7 +105,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraCompatFreeformPolicy_notPresentWhenNoFlag() { runTestScenario((robot) -> { robot.allowEnterDesktopMode(/* isAllowed= */ true); @@ -115,7 +115,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraCompatFreeformPolicy_notPresentWhenNoFlagAndNoDW() { runTestScenario((robot) -> { robot.allowEnterDesktopMode(/* isAllowed= */ false); @@ -125,7 +125,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraCompatFreeformPolicy_startedWhenEnabledAndDW() { runTestScenario((robot) -> { robot.allowEnterDesktopMode(/* isAllowed= */ true); @@ -136,7 +136,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraStateManager_existsWhenCameraCompatFreeformExists() { runTestScenario((robot) -> { robot.allowEnterDesktopMode(true); @@ -147,7 +147,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraStateManager_startedWhenCameraCompatFreeformExists() { runTestScenario((robot) -> { robot.allowEnterDesktopMode(true); @@ -180,7 +180,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraStateManager_doesNotExistWhenNoPolicyExists() { runTestScenario((robot) -> { robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ false); diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java index a48813d775d1..dbcef10a6be2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java @@ -35,7 +35,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; -import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM; +import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -60,6 +60,7 @@ import android.content.res.Configuration.Orientation; import android.graphics.Rect; import android.hardware.camera2.CameraManager; import android.os.Handler; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.DisplayInfo; import android.view.Surface; @@ -135,7 +136,6 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { }); mActivityRefresher = new ActivityRefresher(mDisplayContent.mWmService, mMockHandler); - mSetFlagsRule.enableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM); CameraStateMonitor cameraStateMonitor = new CameraStateMonitor(mDisplayContent, mMockHandler); mCameraCompatFreeformPolicy = @@ -147,6 +147,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { cameraStateMonitor.startListeningToCameraState(); } + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test public void testFullscreen_doesNotActivateCameraCompatMode() { configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); @@ -157,6 +158,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { assertNotInCameraCompatMode(); } + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test public void testOrientationUnspecified_doesNotActivateCameraCompatMode() { configureActivity(SCREEN_ORIENTATION_UNSPECIFIED); @@ -164,12 +166,14 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { assertNotInCameraCompatMode(); } + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test public void testNoCameraConnection_doesNotActivateCameraCompatMode() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); assertNotInCameraCompatMode(); } + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -210,6 +214,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { assertActivityRefreshRequested(/* refreshRequested */ false); } + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test public void testCameraReconnected_cameraCompatModeAndRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -235,6 +240,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT}) public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -246,6 +252,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -254,6 +261,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -268,6 +276,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception { when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) .thenReturn(false); @@ -281,6 +290,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 85cb1bcc01fb..5c0d424f4f42 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -83,7 +83,7 @@ import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_TOKEN_TRANSFO import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; import static com.android.server.wm.WindowContainer.POSITION_TOP; import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL; -import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM; +import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE; import static com.google.common.truth.Truth.assertThat; @@ -2820,7 +2820,7 @@ public class DisplayContentTests extends WindowTestsBase { verify(mWm.mUmInternal, never()).isUserVisible(userId2, displayId); } - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test public void cameraCompatFreeformFlagEnabled_cameraCompatFreeformPolicyNotNull() { doReturn(true).when(() -> @@ -2829,7 +2829,7 @@ public class DisplayContentTests extends WindowTestsBase { assertTrue(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy()); } - @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test public void cameraCompatFreeformFlagNotEnabled_cameraCompatFreeformPolicyIsNull() { doReturn(true).when(() -> @@ -2838,7 +2838,7 @@ public class DisplayContentTests extends WindowTestsBase { assertFalse(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy()); } - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @Test public void desktopWindowingFlagNotEnabled_cameraCompatFreeformPolicyIsNull() { |