diff options
273 files changed, 8708 insertions, 1283 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index c0b6ab6a6755..9fc350d75bbc 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -19373,6 +19373,7 @@ package android.hardware.camera2 { field @FlaggedApi("com.android.internal.camera.flags.color_temperature") @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.util.Range<java.lang.Integer>> COLOR_CORRECTION_COLOR_TEMPERATURE_RANGE; field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AE_AVAILABLE_ANTIBANDING_MODES; field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AE_AVAILABLE_MODES; + field @FlaggedApi("com.android.internal.camera.flags.ae_priority") @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AE_AVAILABLE_PRIORITY_MODES; field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.util.Range<java.lang.Integer>[]> CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES; field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.util.Range<java.lang.Integer>> CONTROL_AE_COMPENSATION_RANGE; field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.util.Rational> CONTROL_AE_COMPENSATION_STEP; @@ -19690,6 +19691,9 @@ package android.hardware.camera2 { field public static final int CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL = 2; // 0x2 field public static final int CONTROL_AE_PRECAPTURE_TRIGGER_IDLE = 0; // 0x0 field public static final int CONTROL_AE_PRECAPTURE_TRIGGER_START = 1; // 0x1 + field @FlaggedApi("com.android.internal.camera.flags.ae_priority") public static final int CONTROL_AE_PRIORITY_MODE_OFF = 0; // 0x0 + field @FlaggedApi("com.android.internal.camera.flags.ae_priority") public static final int CONTROL_AE_PRIORITY_MODE_SENSOR_EXPOSURE_TIME_PRIORITY = 2; // 0x2 + field @FlaggedApi("com.android.internal.camera.flags.ae_priority") public static final int CONTROL_AE_PRIORITY_MODE_SENSOR_SENSITIVITY_PRIORITY = 1; // 0x1 field public static final int CONTROL_AE_STATE_CONVERGED = 2; // 0x2 field public static final int CONTROL_AE_STATE_FLASH_REQUIRED = 4; // 0x4 field public static final int CONTROL_AE_STATE_INACTIVE = 0; // 0x0 @@ -19783,6 +19787,8 @@ package android.hardware.camera2 { field public static final int CONTROL_VIDEO_STABILIZATION_MODE_OFF = 0; // 0x0 field public static final int CONTROL_VIDEO_STABILIZATION_MODE_ON = 1; // 0x1 field public static final int CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION = 2; // 0x2 + field @FlaggedApi("com.android.internal.camera.flags.zoom_method") public static final int CONTROL_ZOOM_METHOD_AUTO = 0; // 0x0 + field @FlaggedApi("com.android.internal.camera.flags.zoom_method") public static final int CONTROL_ZOOM_METHOD_ZOOM_RATIO = 1; // 0x1 field public static final int DISTORTION_CORRECTION_MODE_FAST = 1; // 0x1 field public static final int DISTORTION_CORRECTION_MODE_HIGH_QUALITY = 2; // 0x2 field public static final int DISTORTION_CORRECTION_MODE_OFF = 0; // 0x0 @@ -19972,6 +19978,7 @@ package android.hardware.camera2 { field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Boolean> CONTROL_AE_LOCK; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AE_MODE; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AE_PRECAPTURE_TRIGGER; + field @FlaggedApi("com.android.internal.camera.flags.ae_priority") @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AE_PRIORITY_MODE; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AE_REGIONS; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<android.util.Range<java.lang.Integer>> CONTROL_AE_TARGET_FPS_RANGE; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AF_MODE; @@ -19990,6 +19997,7 @@ package android.hardware.camera2 { field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_SCENE_MODE; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_SETTINGS_OVERRIDE; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_VIDEO_STABILIZATION_MODE; + field @FlaggedApi("com.android.internal.camera.flags.zoom_method") @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_ZOOM_METHOD; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Float> CONTROL_ZOOM_RATIO; field @NonNull public static final android.os.Parcelable.Creator<android.hardware.camera2.CaptureRequest> CREATOR; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> DISTORTION_CORRECTION_MODE; @@ -20064,6 +20072,7 @@ package android.hardware.camera2 { field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Boolean> CONTROL_AE_LOCK; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AE_MODE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AE_PRECAPTURE_TRIGGER; + field @FlaggedApi("com.android.internal.camera.flags.ae_priority") @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AE_PRIORITY_MODE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AE_REGIONS; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AE_STATE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<android.util.Range<java.lang.Integer>> CONTROL_AE_TARGET_FPS_RANGE; @@ -20088,6 +20097,7 @@ package android.hardware.camera2 { field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_SCENE_MODE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_SETTINGS_OVERRIDE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_VIDEO_STABILIZATION_MODE; + field @FlaggedApi("com.android.internal.camera.flags.zoom_method") @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_ZOOM_METHOD; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Float> CONTROL_ZOOM_RATIO; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> DISTORTION_CORRECTION_MODE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> EDGE_MODE; @@ -20910,6 +20920,8 @@ package android.hardware.usb { method public boolean hasPermission(android.hardware.usb.UsbDevice); method public boolean hasPermission(android.hardware.usb.UsbAccessory); method public android.os.ParcelFileDescriptor openAccessory(android.hardware.usb.UsbAccessory); + method @FlaggedApi("android.hardware.usb.flags.enable_accessory_stream_api") @NonNull public java.io.InputStream openAccessoryInputStream(@NonNull android.hardware.usb.UsbAccessory); + method @FlaggedApi("android.hardware.usb.flags.enable_accessory_stream_api") @NonNull public java.io.OutputStream openAccessoryOutputStream(@NonNull android.hardware.usb.UsbAccessory); method public android.hardware.usb.UsbDeviceConnection openDevice(android.hardware.usb.UsbDevice); method public void requestPermission(android.hardware.usb.UsbDevice, android.app.PendingIntent); method public void requestPermission(android.hardware.usb.UsbAccessory, android.app.PendingIntent); @@ -21012,6 +21024,7 @@ package android.inputmethodservice { method @Deprecated public android.inputmethodservice.AbstractInputMethodService.AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface(); method public android.view.View onCreateInputView(); method protected void onCurrentInputMethodSubtypeChanged(android.view.inputmethod.InputMethodSubtype); + method @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") public void onCustomImeSwitcherButtonRequestedVisible(boolean); method public void onDisplayCompletions(android.view.inputmethod.CompletionInfo[]); method public boolean onEvaluateFullscreenMode(); method @CallSuper public boolean onEvaluateInputViewShown(); @@ -22899,6 +22912,7 @@ package android.media { method public void onCryptoError(@NonNull android.media.MediaCodec, @NonNull android.media.MediaCodec.CryptoException); method public abstract void onError(@NonNull android.media.MediaCodec, @NonNull android.media.MediaCodec.CodecException); method public abstract void onInputBufferAvailable(@NonNull android.media.MediaCodec, int); + method @FlaggedApi("android.media.codec.subsession_metrics") public void onMetricsFlushed(@NonNull android.media.MediaCodec, @NonNull android.os.PersistableBundle); method public abstract void onOutputBufferAvailable(@NonNull android.media.MediaCodec, int, @NonNull android.media.MediaCodec.BufferInfo); method @FlaggedApi("com.android.media.codec.flags.large_audio_frame") public void onOutputBuffersAvailable(@NonNull android.media.MediaCodec, int, @NonNull java.util.ArrayDeque<android.media.MediaCodec.BufferInfo>); method public abstract void onOutputFormatChanged(@NonNull android.media.MediaCodec, @NonNull android.media.MediaFormat); @@ -42159,6 +42173,25 @@ package android.service.settings.preferences { method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setWriteSensitivity(int); } + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public abstract class SettingsPreferenceService extends android.app.Service { + ctor public SettingsPreferenceService(); + method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); + method public abstract void onGetAllPreferenceMetadata(@NonNull android.service.settings.preferences.MetadataRequest, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.MetadataResult,java.lang.Exception>); + method public abstract void onGetPreferenceValue(@NonNull android.service.settings.preferences.GetValueRequest, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.GetValueResult,java.lang.Exception>); + method public abstract void onSetPreferenceValue(@NonNull android.service.settings.preferences.SetValueRequest, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.SetValueResult,java.lang.Exception>); + field public static final String ACTION_PREFERENCE_SERVICE = "android.service.settings.preferences.action.PREFERENCE_SERVICE"; + } + + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public class SettingsPreferenceServiceClient implements java.lang.AutoCloseable { + ctor public SettingsPreferenceServiceClient(@NonNull android.content.Context, @NonNull String); + method public void close(); + method public void getAllPreferenceMetadata(@NonNull android.service.settings.preferences.MetadataRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.MetadataResult,java.lang.Exception>); + method public void getPreferenceValue(@NonNull android.service.settings.preferences.GetValueRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.GetValueResult,java.lang.Exception>); + method public void setPreferenceValue(@NonNull android.service.settings.preferences.SetValueRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.SetValueResult,java.lang.Exception>); + method public void start(); + method public void stop(); + } + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class SettingsPreferenceValue implements android.os.Parcelable { method public int describeContents(); method public boolean getBooleanValue(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 71c431216bf8..0e521c664340 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -1272,13 +1272,21 @@ package android.app { public class WallpaperManager { method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public void clearWallpaper(int, int); + method @FlaggedApi("android.app.customization_packs_apis") @NonNull @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public android.util.SparseArray<android.graphics.Rect> getBitmapCrops(int); + method @FlaggedApi("android.app.customization_packs_apis") public static int getOrientation(@NonNull android.graphics.Point); method @FloatRange(from=0.0f, to=1.0f) @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT) public float getWallpaperDimAmount(); + method @FlaggedApi("android.app.customization_packs_apis") @Nullable public android.os.ParcelFileDescriptor getWallpaperFile(int, boolean); method @FlaggedApi("android.app.live_wallpaper_content_handling") @Nullable @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public android.app.wallpaper.WallpaperInstance getWallpaperInstance(int); method public void setDisplayOffset(android.os.IBinder, int, int); + method @FlaggedApi("com.android.window.flags.multi_crop") @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) public int setStreamWithCrops(@NonNull java.io.InputStream, @NonNull android.util.SparseArray<android.graphics.Rect>, boolean, int) throws java.io.IOException; method @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT) public boolean setWallpaperComponent(android.content.ComponentName); method @FlaggedApi("android.app.live_wallpaper_content_handling") @RequiresPermission(allOf={android.Manifest.permission.SET_WALLPAPER_COMPONENT, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}, conditional=true) public boolean setWallpaperComponentWithDescription(@NonNull android.app.wallpaper.WallpaperDescription, int); method @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT) public boolean setWallpaperComponentWithFlags(@NonNull android.content.ComponentName, int); method @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT) public void setWallpaperDimAmount(@FloatRange(from=0.0f, to=1.0f) float); + field @FlaggedApi("android.app.customization_packs_apis") public static final int ORIENTATION_LANDSCAPE = 1; // 0x1 + field @FlaggedApi("android.app.customization_packs_apis") public static final int ORIENTATION_PORTRAIT = 0; // 0x0 + field @FlaggedApi("android.app.customization_packs_apis") public static final int ORIENTATION_SQUARE_LANDSCAPE = 3; // 0x3 + field @FlaggedApi("android.app.customization_packs_apis") public static final int ORIENTATION_SQUARE_PORTRAIT = 2; // 0x2 } } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 8dd12172fdc5..c8ecfa94ec87 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -535,9 +535,13 @@ package android.app { public class WallpaperManager { method @Nullable public android.graphics.Bitmap getBitmap(); method @Nullable public android.graphics.Bitmap getBitmapAsUser(int, boolean, int); + method @FlaggedApi("com.android.window.flags.multi_crop") @NonNull @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public java.util.List<android.graphics.Rect> getBitmapCrops(@NonNull java.util.List<android.graphics.Point>, int, boolean); + method @FlaggedApi("com.android.window.flags.multi_crop") @NonNull public java.util.List<android.graphics.Rect> getBitmapCrops(@NonNull android.graphics.Point, @NonNull java.util.List<android.graphics.Point>, @Nullable java.util.Map<android.graphics.Point,android.graphics.Rect>); method public boolean isLockscreenLiveWallpaperEnabled(); method @Nullable public android.graphics.Rect peekBitmapDimensions(); method @Nullable public android.graphics.Rect peekBitmapDimensions(int); + method @FlaggedApi("com.android.window.flags.multi_crop") @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) public int setBitmapWithCrops(@Nullable android.graphics.Bitmap, @NonNull java.util.Map<android.graphics.Point,android.graphics.Rect>, boolean, int) throws java.io.IOException; + method @FlaggedApi("com.android.window.flags.multi_crop") @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) public int setStreamWithCrops(@NonNull java.io.InputStream, @NonNull java.util.Map<android.graphics.Point,android.graphics.Rect>, boolean, int) throws java.io.IOException; method public void setWallpaperZoomOut(@NonNull android.os.IBinder, float); method public boolean shouldEnableWideColorGamut(); method public boolean wallpaperSupportsWcg(int); @@ -3247,6 +3251,14 @@ package android.service.quicksettings { } +package android.service.settings.preferences { + + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public class SettingsPreferenceServiceClient implements java.lang.AutoCloseable { + ctor public SettingsPreferenceServiceClient(@NonNull android.content.Context, @NonNull String, boolean, @Nullable android.content.ServiceConnection); + } + +} + package android.service.voice { public class AlwaysOnHotwordDetector implements android.service.voice.HotwordDetector { diff --git a/core/java/android/app/DisabledWallpaperManager.java b/core/java/android/app/DisabledWallpaperManager.java index b06fb9e2f284..233dc75b810f 100644 --- a/core/java/android/app/DisabledWallpaperManager.java +++ b/core/java/android/app/DisabledWallpaperManager.java @@ -177,6 +177,13 @@ final class DisabledWallpaperManager extends WallpaperManager { } @Override + @NonNull + public SparseArray<Rect> getBitmapCrops(int which) { + unsupported(); + return new SparseArray<>(); + } + + @Override public List<Rect> getBitmapCrops(@NonNull Point bitmapSize, @NonNull List<Point> displaySizes, @Nullable Map<Point, Rect> cropHints) { return unsupported(); @@ -358,8 +365,9 @@ final class DisabledWallpaperManager extends WallpaperManager { @Override - public int setStreamWithCrops(InputStream bitmapData, @NonNull SparseArray<Rect> cropHints, - boolean allowBackup, @SetWallpaperFlags int which) throws IOException { + public int setStreamWithCrops(@NonNull InputStream bitmapData, + @NonNull SparseArray<Rect> cropHints, boolean allowBackup, @SetWallpaperFlags int which + ) throws IOException { return unsupportedInt(); } diff --git a/core/java/android/app/IWallpaperManager.aidl b/core/java/android/app/IWallpaperManager.aidl index f693e9ba11ec..6449ea1742a1 100644 --- a/core/java/android/app/IWallpaperManager.aidl +++ b/core/java/android/app/IWallpaperManager.aidl @@ -97,6 +97,16 @@ interface IWallpaperManager { List getBitmapCrops(in List<Point> displaySizes, int which, boolean originalBitmap, int userId); /** + * For a given user, if the wallpaper of the specified which is an ImageWallpaper, return + * a bundle which is a Map<Integer, Rect> containing the custom cropHints that were sent to + * setBitmapWithCrops or setStreamWithCrops. These crops are relative to the original bitmap. + * If the wallpaper isn't an ImageWallpaper, return null. + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL)") + @SuppressWarnings(value={"untyped-collection"}) + Bundle getCurrentBitmapCrops(int which, int userId); + + /** * Return how a bitmap of a given size would be cropped for a given list of display sizes when * set with the given suggested crops. * @hide diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 479f3df9affb..abb2dd465576 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -19,9 +19,10 @@ package android.app; import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE; import static android.Manifest.permission.READ_WALLPAPER_INTERNAL; import static android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT; +import static android.app.Flags.FLAG_CUSTOMIZATION_PACKS_APIS; +import static android.app.Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; -import static android.app.Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING; import static com.android.window.flags.Flags.FLAG_MULTI_CROP; import static com.android.window.flags.Flags.multiCrop; @@ -342,24 +343,32 @@ public class WallpaperManager { * Portrait orientation of most screens * @hide */ + @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS) + @SystemApi public static final int ORIENTATION_PORTRAIT = 0; /** * Landscape orientation of most screens * @hide */ + @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS) + @SystemApi public static final int ORIENTATION_LANDSCAPE = 1; /** * Portrait orientation with similar width and height (e.g. the inner screen of a foldable) * @hide */ + @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS) + @SystemApi public static final int ORIENTATION_SQUARE_PORTRAIT = 2; /** * Landscape orientation with similar width and height (e.g. the inner screen of a foldable) * @hide */ + @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS) + @SystemApi public static final int ORIENTATION_SQUARE_LANDSCAPE = 3; /** @@ -368,7 +377,9 @@ public class WallpaperManager { * @return the corresponding {@link ScreenOrientation}. * @hide */ - public static @ScreenOrientation int getOrientation(Point screenSize) { + @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS) + @SystemApi + public static @ScreenOrientation int getOrientation(@NonNull Point screenSize) { float ratio = ((float) screenSize.x) / screenSize.y; // ratios between 3/4 and 4/3 are considered square return ratio >= 4 / 3f ? ORIENTATION_LANDSCAPE @@ -1623,14 +1634,15 @@ public class WallpaperManager { * If false, return areas relative to the cropped bitmap. * @return A List of Rect where the Rect is within the cropped/original bitmap, and corresponds * to what is displayed. The Rect may have a larger width/height ratio than the screen - * due to parallax. Return {@code null} if the wallpaper is not an ImageWallpaper. - * Also return {@code null} when called with which={@link #FLAG_LOCK} if there is a + * due to parallax. Return an empty list if the wallpaper is not an ImageWallpaper. + * Also return an empty list when called with which={@link #FLAG_LOCK} if there is a * shared home + lock wallpaper. * @hide */ @FlaggedApi(FLAG_MULTI_CROP) + @TestApi @RequiresPermission(READ_WALLPAPER_INTERNAL) - @Nullable + @NonNull public List<Rect> getBitmapCrops(@NonNull List<Point> displaySizes, @SetWallpaperFlags int which, boolean originalBitmap) { checkExactlyOneWallpaperFlagSet(which); @@ -1653,6 +1665,52 @@ public class WallpaperManager { } /** + * For the current user, if the wallpaper of the specified destination is an ImageWallpaper, + * return the custom crops of the wallpaper, that have been provided for example via + * {@link #setStreamWithCrops}. These crops are relative to the original bitmap. + * <p> + * This method helps apps that change wallpapers provide an undo option. Calling + * {@link #setStreamWithCrops(InputStream, SparseArray, boolean, int)} with this SparseArray and + * the current original bitmap file, that can be obtained with {@link #getWallpaperFile(int, + * boolean)} with {@code getCropped=false}, will exactly lead to the current wallpaper state. + * + * @param which wallpaper type. Must be either {@link #FLAG_SYSTEM} or {@link #FLAG_LOCK}. + * @return A map from {{@link #ORIENTATION_PORTRAIT}, {@link #ORIENTATION_LANDSCAPE}, + * {@link #ORIENTATION_SQUARE_PORTRAIT}, {{@link #ORIENTATION_SQUARE_LANDSCAPE}}} to + * Rect, representing the custom cropHints. The map can be empty and will only contains + * entries for screen orientations for which a custom crop was provided. If no custom + * crop is provided for an orientation, the system will infer the crop based on the + * custom crops of the other orientations; or center-align the full image if no custom + * crops are provided at all. + * <p> + * Return an empty map if the wallpaper is not an ImageWallpaper. Also return + * an empty map when called with which={@link #FLAG_LOCK} if there is a shared + * home + lock wallpaper. + * + * @hide + */ + @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS) + @SystemApi + @RequiresPermission(READ_WALLPAPER_INTERNAL) + @NonNull + public SparseArray<Rect> getBitmapCrops(@SetWallpaperFlags int which) { + checkExactlyOneWallpaperFlagSet(which); + try { + Bundle bundle = sGlobals.mService.getCurrentBitmapCrops(which, mContext.getUserId()); + SparseArray<Rect> result = new SparseArray<>(); + if (bundle == null) return result; + for (String key : bundle.keySet()) { + int intKey = Integer.parseInt(key); + Rect rect = bundle.getParcelable(key, Rect.class); + result.put(intKey, rect); + } + return result; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * For preview purposes. * Return how a bitmap of a given size would be cropped for a given list of display sizes, if * it was set as wallpaper via {@link #setBitmapWithCrops(Bitmap, Map, boolean, int)} or @@ -1664,7 +1722,8 @@ public class WallpaperManager { * @hide */ @FlaggedApi(FLAG_MULTI_CROP) - @Nullable + @TestApi + @NonNull public List<Rect> getBitmapCrops(@NonNull Point bitmapSize, @NonNull List<Point> displaySizes, @Nullable Map<Point, Rect> cropHints) { try { @@ -1890,9 +1949,14 @@ public class WallpaperManager { * defined kind of wallpaper, either {@link #FLAG_SYSTEM} or {@link #FLAG_LOCK}. * @param getCropped If true the cropped file will be retrieved, if false the original will * be retrieved. - * + * @return A ParcelFileDescriptor for the wallpaper bitmap of the given destination, if it's an + * ImageWallpaper wallpaper. Return {@code null} if the wallpaper is not an + * ImageWallpaper. Also return {@code null} when called with + * which={@link #FLAG_LOCK} if there is a shared home + lock wallpaper. * @hide */ + @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS) + @SystemApi @Nullable public ParcelFileDescriptor getWallpaperFile(@SetWallpaperFlags int which, boolean getCropped) { return getWallpaperFile(which, mContext.getUserId(), getCropped); @@ -2371,7 +2435,6 @@ public class WallpaperManager { /** * Version of setBitmap that defines how the wallpaper will be positioned for different * display sizes. - * Requires permission {@link android.Manifest.permission#SET_WALLPAPER}. * @param cropHints map from screen dimensions to a sub-region of the image to display for those * dimensions. The {@code Rect} sub-region may have a larger width/height ratio * than the screen dimensions to apply a horizontal parallax effect. If the @@ -2380,6 +2443,7 @@ public class WallpaperManager { * @hide */ @FlaggedApi(FLAG_MULTI_CROP) + @TestApi @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) public int setBitmapWithCrops(@Nullable Bitmap fullImage, @NonNull Map<Point, Rect> cropHints, boolean allowBackup, @SetWallpaperFlags int which) throws IOException { @@ -2562,7 +2626,6 @@ public class WallpaperManager { /** * Version of setStream that defines how the wallpaper will be positioned for different * display sizes. - * Requires permission {@link android.Manifest.permission#SET_WALLPAPER}. * @param cropHints map from screen dimensions to a sub-region of the image to display for those * dimensions. The {@code Rect} sub-region may have a larger width/height ratio * than the screen dimensions to apply a horizontal parallax effect. If the @@ -2571,9 +2634,11 @@ public class WallpaperManager { * @hide */ @FlaggedApi(FLAG_MULTI_CROP) + @TestApi @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) - public int setStreamWithCrops(InputStream bitmapData, @NonNull Map<Point, Rect> cropHints, - boolean allowBackup, @SetWallpaperFlags int which) throws IOException { + public int setStreamWithCrops(@NonNull InputStream bitmapData, + @NonNull Map<Point, Rect> cropHints, boolean allowBackup, @SetWallpaperFlags int which) + throws IOException { SparseArray<Rect> crops = new SparseArray<>(); cropHints.forEach((k, v) -> crops.put(getOrientation(k), v)); return setStreamWithCrops(bitmapData, crops, allowBackup, which); @@ -2583,15 +2648,21 @@ public class WallpaperManager { * Similar to {@link #setStreamWithCrops(InputStream, Map, boolean, int)}, but using * {@link ScreenOrientation} as keys of the cropHints map. Used for backup & restore, since * WallpaperBackupAgent stores orientations rather than the exact display size. - * Requires permission {@link android.Manifest.permission#SET_WALLPAPER}. + * @param bitmapData A stream containing the raw data to install as a wallpaper. This + * data can be in any format handled by {@link BitmapRegionDecoder}. * @param cropHints map from {@link ScreenOrientation} to a sub-region of the image to display * for that screen orientation. + * @param allowBackup {@code true} if the OS is permitted to back up this wallpaper + * image for restore to a future device; {@code false} otherwise. + * @param which Flags indicating which wallpaper(s) to configure with the new imagery. * @hide */ @FlaggedApi(FLAG_MULTI_CROP) + @SystemApi @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) - public int setStreamWithCrops(InputStream bitmapData, @NonNull SparseArray<Rect> cropHints, - boolean allowBackup, @SetWallpaperFlags int which) throws IOException { + public int setStreamWithCrops(@NonNull InputStream bitmapData, + @NonNull SparseArray<Rect> cropHints, boolean allowBackup, @SetWallpaperFlags int which) + throws IOException { if (sGlobals.mService == null) { Log.w(TAG, "WallpaperService not running"); throw new RuntimeException(new DeadSystemException()); diff --git a/core/java/android/app/appfunctions/AppFunctionException.java b/core/java/android/app/appfunctions/AppFunctionException.java index d33b5055f9cc..cbd1d932ab00 100644 --- a/core/java/android/app/appfunctions/AppFunctionException.java +++ b/core/java/android/app/appfunctions/AppFunctionException.java @@ -141,6 +141,7 @@ public final class AppFunctionException extends Exception implements Parcelable */ public AppFunctionException( @ErrorCode int errorCode, @Nullable String errorMessage, @NonNull Bundle extras) { + super(errorMessage); mErrorCode = errorCode; mErrorMessage = errorMessage; mExtras = Objects.requireNonNull(extras); diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index ee93870be055..6934e9883840 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -100,16 +100,6 @@ flag { } flag { - name: "visit_person_uri" - namespace: "systemui" - description: "Guards the security fix that ensures all URIs Person.java are valid" - bug: "281044385" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "notification_expansion_optional" namespace: "systemui" description: "Experiment to restore the pre-S behavior where standard notifications are not expandable unless they have actions." diff --git a/core/java/android/app/supervision/flags.aconfig b/core/java/android/app/supervision/flags.aconfig index bcb5b3636c95..d5e696d49ff4 100644 --- a/core/java/android/app/supervision/flags.aconfig +++ b/core/java/android/app/supervision/flags.aconfig @@ -7,4 +7,12 @@ flag { namespace: "supervision" description: "Flag to enable the SupervisionService" bug: "340351729" -}
\ No newline at end of file +} + +flag { + name: "supervision_api_on_wear" + is_exported: true + namespace: "supervision" + description: "Flag to enable the SupervisionService on Wear devices" + bug: "373358935" +} diff --git a/core/java/android/app/wallpaper.aconfig b/core/java/android/app/wallpaper.aconfig index 4b880d030413..f750a844f4ff 100644 --- a/core/java/android/app/wallpaper.aconfig +++ b/core/java/android/app/wallpaper.aconfig @@ -22,3 +22,21 @@ flag { bug: "347235611" is_exported: true } + +flag { + name: "customization_packs_apis" + is_exported: true + namespace: "systemui" + description: "Move APIs related to bitmap and crops to @SystemApi." + bug: "372344184" +} + +flag { + name: "accurate_wallpaper_downsampling" + namespace: "systemui" + description: "Accurate downsampling of wallpaper bitmap for high resolution images" + bug: "355665230" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java index 34c3f5798bc5..e9e8578af787 100644 --- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java +++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java @@ -38,6 +38,7 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.AttributeSet; +import android.util.EmptyArray; import android.util.Pair; import android.util.Slog; @@ -565,10 +566,7 @@ public class ApkLiteParseUtils { usesSdkLibrariesVersionsMajor, usesSdkLibVersionMajor, /*allowDuplicates=*/ true); - // We allow ":" delimiters in the SHA declaration as this is the format - // emitted by the certtool making it easy for developers to copy/paste. - // TODO(372862145): Add test for this replacement - usesSdkCertDigest = usesSdkCertDigest.replace(":", "").toLowerCase(); + usesSdkCertDigest = normalizeCertDigest(usesSdkCertDigest); if ("".equals(usesSdkCertDigest)) { // Test-only uses-sdk-library empty certificate digest override. @@ -618,18 +616,23 @@ public class ApkLiteParseUtils { usesStaticLibrariesVersions, usesStaticLibVersion, /*allowDuplicates=*/ true); - // We allow ":" delimiters in the SHA declaration as this is the format - // emitted by the certtool making it easy for developers to copy/paste. - // TODO(372862145): Add test for this replacement - usesStaticLibCertDigest = - usesStaticLibCertDigest.replace(":", "").toLowerCase(); + usesStaticLibCertDigest = normalizeCertDigest(usesStaticLibCertDigest); + + ParseResult<String[]> certResult = + parseAdditionalCertificates(input, parser); + if (certResult.isError()) { + return input.error(certResult); + } + String[] additionalCertSha256Digests = certResult.getResult(); + String[] certSha256Digests = + new String[additionalCertSha256Digests.length + 1]; + certSha256Digests[0] = usesStaticLibCertDigest; + System.arraycopy(additionalCertSha256Digests, 0, certSha256Digests, + 1, additionalCertSha256Digests.length); - // TODO(372862145): Add support for multiple signer for app targeting - // O-MR1 usesStaticLibrariesCertDigests = ArrayUtils.appendElement( String[].class, usesStaticLibrariesCertDigests, - new String[]{usesStaticLibCertDigest}, - /*allowDuplicates=*/ true); + certSha256Digests, /*allowDuplicates=*/ true); break; case TAG_SDK_LIBRARY: isSdkLibrary = true; @@ -809,6 +812,43 @@ public class ApkLiteParseUtils { declaredLibraries)); } + private static ParseResult<String[]> parseAdditionalCertificates(ParseInput input, + XmlResourceParser parser) throws XmlPullParserException, IOException { + String[] certSha256Digests = EmptyArray.STRING; + final int depth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > depth)) { + if (type != XmlPullParser.START_TAG) { + continue; + } + final String nodeName = parser.getName(); + if (nodeName.equals("additional-certificate")) { + String certSha256Digest = parser.getAttributeValue( + ANDROID_RES_NAMESPACE, "certDigest"); + if (TextUtils.isEmpty(certSha256Digest)) { + return input.error("Bad additional-certificate declaration with empty" + + " certDigest:" + certSha256Digest); + } + + certSha256Digest = normalizeCertDigest(certSha256Digest); + certSha256Digests = ArrayUtils.appendElement(String.class, + certSha256Digests, certSha256Digest); + } + } + + return input.success(certSha256Digests); + } + + /** + * We allow ":" delimiters in the SHA declaration as this is the format emitted by the + * certtool making it easy for developers to copy/paste. + */ + private static String normalizeCertDigest(String certDigest) { + return certDigest.replace(":", "").toLowerCase(); + } + private static boolean isDeviceAdminReceiver( XmlResourceParser parser, boolean applicationHasBindDeviceAdminPermission) throws XmlPullParserException, IOException { diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index a37648f7e45d..4ca20059f4eb 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -1436,6 +1436,24 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri new Key<android.util.Range<Float>>("android.control.lowLightBoostInfoLuminanceRange", new TypeReference<android.util.Range<Float>>() {{ }}); /** + * <p>List of auto-exposure priority modes for {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} + * that are supported by this camera device.</p> + * <p>This entry lists the valid modes for + * {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} for this camera device. + * If no AE priority modes are available for a device, this will only list OFF.</p> + * <p><b>Range of valid values:</b><br> + * Any value listed in {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode}</p> + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * + * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE + */ + @PublicKey + @NonNull + @FlaggedApi(Flags.FLAG_AE_PRIORITY) + public static final Key<int[]> CONTROL_AE_AVAILABLE_PRIORITY_MODES = + new Key<int[]>("android.control.aeAvailablePriorityModes", int[].class); + + /** * <p>List of edge enhancement modes for {@link CaptureRequest#EDGE_MODE android.edge.mode} that are supported by this camera * device.</p> * <p>Full-capability camera devices must always support OFF; camera devices that support diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index b7856303fc05..75e20582b7b4 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -994,7 +994,7 @@ public final class CameraManager { AttributionSourceState contextAttributionSourceState = contextAttributionSource.asState(); - if (Flags.useContextAttributionSource() && useContextAttributionSource) { + if (Flags.dataDeliveryPermissionChecks() && useContextAttributionSource) { return contextAttributionSourceState; } else { AttributionSourceState clientAttribution = diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java index 987e2ad768b0..8d36fbdc8b10 100644 --- a/core/java/android/hardware/camera2/CameraMetadata.java +++ b/core/java/android/hardware/camera2/CameraMetadata.java @@ -3371,6 +3371,74 @@ public abstract class CameraMetadata<TKey> { public static final int CONTROL_AUTOFRAMING_AUTO = 2; // + // Enumeration values for CaptureRequest#CONTROL_ZOOM_METHOD + // + + /** + * <p>The camera device automatically detects whether the application does zoom with + * {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and in turn decides which + * metadata tag reflects the effective zoom level.</p> + * + * @see CaptureRequest#CONTROL_ZOOM_RATIO + * @see CaptureRequest#SCALER_CROP_REGION + * @see CaptureRequest#CONTROL_ZOOM_METHOD + */ + @FlaggedApi(Flags.FLAG_ZOOM_METHOD) + public static final int CONTROL_ZOOM_METHOD_AUTO = 0; + + /** + * <p>The application intends to control zoom via {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and + * the effective zoom level is reflected by {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in capture results.</p> + * + * @see CaptureRequest#CONTROL_ZOOM_RATIO + * @see CaptureRequest#CONTROL_ZOOM_METHOD + */ + @FlaggedApi(Flags.FLAG_ZOOM_METHOD) + public static final int CONTROL_ZOOM_METHOD_ZOOM_RATIO = 1; + + // + // Enumeration values for CaptureRequest#CONTROL_AE_PRIORITY_MODE + // + + /** + * <p>Disable AE priority mode. This is the default value.</p> + * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE + */ + @FlaggedApi(Flags.FLAG_AE_PRIORITY) + public static final int CONTROL_AE_PRIORITY_MODE_OFF = 0; + + /** + * <p>The camera device's auto-exposure routine is active and + * prioritizes the application-selected ISO ({@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}).</p> + * <p>The application has control over {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity} while + * the application's values for {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime} and + * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} are ignored.</p> + * + * @see CaptureRequest#SENSOR_EXPOSURE_TIME + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @see CaptureRequest#SENSOR_SENSITIVITY + * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE + */ + @FlaggedApi(Flags.FLAG_AE_PRIORITY) + public static final int CONTROL_AE_PRIORITY_MODE_SENSOR_SENSITIVITY_PRIORITY = 1; + + /** + * <p>The camera device's auto-exposure routine is active and + * prioritizes the application-selected exposure time + * ({@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}).</p> + * <p>The application has control over {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime} while + * the application's values for {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity} and + * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} are ignored.</p> + * + * @see CaptureRequest#SENSOR_EXPOSURE_TIME + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @see CaptureRequest#SENSOR_SENSITIVITY + * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE + */ + @FlaggedApi(Flags.FLAG_AE_PRIORITY) + public static final int CONTROL_AE_PRIORITY_MODE_SENSOR_EXPOSURE_TIME_PRIORITY = 2; + + // // Enumeration values for CaptureRequest#EDGE_MODE // diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index 8142bbe9b838..496d316eb028 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -21,7 +21,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.hardware.camera2.impl.CameraMetadataNative; -import android.hardware.camera2.impl.ExtensionKey; import android.hardware.camera2.impl.PublicKey; import android.hardware.camera2.impl.SyntheticKey; import android.hardware.camera2.params.OutputConfiguration; @@ -1407,7 +1406,9 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * application's selected exposure time, sensor sensitivity, * and frame duration ({@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}, * {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}, and - * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}). If one of the FLASH modes + * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}). If {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} is + * enabled, the relevant priority CaptureRequest settings will not be overridden. + * See {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} for more details. If one of the FLASH modes * is selected, the camera device's flash unit controls are * also overridden.</p> * <p>The FLASH modes are only available if the camera device @@ -1441,6 +1442,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * * @see CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE * @see CaptureRequest#CONTROL_MODE * @see CameraCharacteristics#FLASH_INFO_AVAILABLE * @see CaptureRequest#FLASH_MODE @@ -2668,6 +2670,85 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> new Key<Integer>("android.control.autoframing", int.class); /** + * <p>Whether the application uses {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} + * to control zoom levels.</p> + * <p>If set to AUTO, the camera device detects which capture request key the application uses + * to do zoom, {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. If + * the application doesn't set android.scaler.zoomRatio or sets it to 1.0 in the capture + * request, the effective zoom level is reflected in {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} in capture + * results. If {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} is set to values other than 1.0, the effective + * zoom level is reflected in {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. AUTO is the default value + * for this control, and also the behavior of the OS before Android version + * {@link android.os.Build.VERSION_CODES#BAKLAVA BAKLAVA}.</p> + * <p>If set to ZOOM_RATIO, the application explicitly specifies zoom level be controlled + * by {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and the effective zoom level is reflected in + * {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in capture results. This addresses an ambiguity with AUTO, + * with which the camera device cannot know if the application is using cropRegion or + * zoomRatio at 1.0x.</p> + * <p><b>Possible values:</b></p> + * <ul> + * <li>{@link #CONTROL_ZOOM_METHOD_AUTO AUTO}</li> + * <li>{@link #CONTROL_ZOOM_METHOD_ZOOM_RATIO ZOOM_RATIO}</li> + * </ul> + * + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * <p><b>Limited capability</b> - + * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> + * + * @see CaptureRequest#CONTROL_ZOOM_RATIO + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CaptureRequest#SCALER_CROP_REGION + * @see #CONTROL_ZOOM_METHOD_AUTO + * @see #CONTROL_ZOOM_METHOD_ZOOM_RATIO + */ + @PublicKey + @NonNull + @FlaggedApi(Flags.FLAG_ZOOM_METHOD) + public static final Key<Integer> CONTROL_ZOOM_METHOD = + new Key<Integer>("android.control.zoomMethod", int.class); + + /** + * <p>Turn on AE priority mode.</p> + * <p>This control is only effective if {@link CaptureRequest#CONTROL_MODE android.control.mode} is + * AUTO and {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is set to one of its + * ON modes, with the exception of ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY.</p> + * <p>When a priority mode is enabled, the camera device's + * auto-exposure routine will maintain the application's + * selected parameters relevant to the priority mode while overriding + * the remaining exposure parameters + * ({@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}, {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}, and + * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}). For example, if + * SENSOR_SENSITIVITY_PRIORITY mode is enabled, the camera device will + * maintain the application-selected {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity} + * while adjusting {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime} + * and {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}. The overridden fields for a + * given capture will be available in its CaptureResult.</p> + * <p><b>Possible values:</b></p> + * <ul> + * <li>{@link #CONTROL_AE_PRIORITY_MODE_OFF OFF}</li> + * <li>{@link #CONTROL_AE_PRIORITY_MODE_SENSOR_SENSITIVITY_PRIORITY SENSOR_SENSITIVITY_PRIORITY}</li> + * <li>{@link #CONTROL_AE_PRIORITY_MODE_SENSOR_EXPOSURE_TIME_PRIORITY SENSOR_EXPOSURE_TIME_PRIORITY}</li> + * </ul> + * + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * + * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_MODE + * @see CaptureRequest#SENSOR_EXPOSURE_TIME + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @see CaptureRequest#SENSOR_SENSITIVITY + * @see #CONTROL_AE_PRIORITY_MODE_OFF + * @see #CONTROL_AE_PRIORITY_MODE_SENSOR_SENSITIVITY_PRIORITY + * @see #CONTROL_AE_PRIORITY_MODE_SENSOR_EXPOSURE_TIME_PRIORITY + */ + @PublicKey + @NonNull + @FlaggedApi(Flags.FLAG_AE_PRIORITY) + public static final Key<Integer> CONTROL_AE_PRIORITY_MODE = + new Key<Integer>("android.control.aePriorityMode", int.class); + + /** * <p>Operation mode for edge * enhancement.</p> * <p>Edge enhancement improves sharpness and details in the captured image. OFF means @@ -3489,7 +3570,9 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * duration exposed to the nearest possible value (rather than expose longer). * The final exposure time used will be available in the output capture result.</p> * <p>This control is only effective if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} or {@link CaptureRequest#CONTROL_MODE android.control.mode} is set to - * OFF; otherwise the auto-exposure algorithm will override this value.</p> + * OFF; otherwise the auto-exposure algorithm will override this value. However, in the + * case that {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} is set to SENSOR_EXPOSURE_TIME_PRIORITY, this + * control will be effective and not controlled by the auto-exposure algorithm.</p> * <p><b>Units</b>: Nanoseconds</p> * <p><b>Range of valid values:</b><br> * {@link CameraCharacteristics#SENSOR_INFO_EXPOSURE_TIME_RANGE android.sensor.info.exposureTimeRange}</p> @@ -3499,6 +3582,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> * * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE * @see CaptureRequest#CONTROL_MODE * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL * @see CameraCharacteristics#SENSOR_INFO_EXPOSURE_TIME_RANGE @@ -3607,7 +3691,9 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * value. The final sensitivity used will be available in the * output capture result.</p> * <p>This control is only effective if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} or {@link CaptureRequest#CONTROL_MODE android.control.mode} is set to - * OFF; otherwise the auto-exposure algorithm will override this value.</p> + * OFF; otherwise the auto-exposure algorithm will override this value. However, in the + * case that {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} is set to SENSOR_SENSITIVITY_PRIORITY, this + * control will be effective and not controlled by the auto-exposure algorithm.</p> * <p>Note that for devices supporting postRawSensitivityBoost, the total sensitivity applied * to the final processed image is the combination of {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity} and * {@link CaptureRequest#CONTROL_POST_RAW_SENSITIVITY_BOOST android.control.postRawSensitivityBoost}. In case the application uses the sensor @@ -3623,6 +3709,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> * * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE * @see CaptureRequest#CONTROL_MODE * @see CaptureRequest#CONTROL_POST_RAW_SENSITIVITY_BOOST * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index bf3a072ff097..a52be973f564 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -22,7 +22,6 @@ import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.impl.CaptureResultExtras; -import android.hardware.camera2.impl.ExtensionKey; import android.hardware.camera2.impl.PublicKey; import android.hardware.camera2.impl.SyntheticKey; import android.hardware.camera2.utils.TypeReference; @@ -808,7 +807,9 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * application's selected exposure time, sensor sensitivity, * and frame duration ({@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}, * {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}, and - * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}). If one of the FLASH modes + * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}). If {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} is + * enabled, the relevant priority CaptureRequest settings will not be overridden. + * See {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} for more details. If one of the FLASH modes * is selected, the camera device's flash unit controls are * also overridden.</p> * <p>The FLASH modes are only available if the camera device @@ -842,6 +843,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * * @see CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE * @see CaptureRequest#CONTROL_MODE * @see CameraCharacteristics#FLASH_INFO_AVAILABLE * @see CaptureRequest#FLASH_MODE @@ -2915,6 +2917,85 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { new Key<Integer>("android.control.lowLightBoostState", int.class); /** + * <p>Whether the application uses {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} + * to control zoom levels.</p> + * <p>If set to AUTO, the camera device detects which capture request key the application uses + * to do zoom, {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. If + * the application doesn't set android.scaler.zoomRatio or sets it to 1.0 in the capture + * request, the effective zoom level is reflected in {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} in capture + * results. If {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} is set to values other than 1.0, the effective + * zoom level is reflected in {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. AUTO is the default value + * for this control, and also the behavior of the OS before Android version + * {@link android.os.Build.VERSION_CODES#BAKLAVA BAKLAVA}.</p> + * <p>If set to ZOOM_RATIO, the application explicitly specifies zoom level be controlled + * by {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and the effective zoom level is reflected in + * {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in capture results. This addresses an ambiguity with AUTO, + * with which the camera device cannot know if the application is using cropRegion or + * zoomRatio at 1.0x.</p> + * <p><b>Possible values:</b></p> + * <ul> + * <li>{@link #CONTROL_ZOOM_METHOD_AUTO AUTO}</li> + * <li>{@link #CONTROL_ZOOM_METHOD_ZOOM_RATIO ZOOM_RATIO}</li> + * </ul> + * + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * <p><b>Limited capability</b> - + * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> + * + * @see CaptureRequest#CONTROL_ZOOM_RATIO + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CaptureRequest#SCALER_CROP_REGION + * @see #CONTROL_ZOOM_METHOD_AUTO + * @see #CONTROL_ZOOM_METHOD_ZOOM_RATIO + */ + @PublicKey + @NonNull + @FlaggedApi(Flags.FLAG_ZOOM_METHOD) + public static final Key<Integer> CONTROL_ZOOM_METHOD = + new Key<Integer>("android.control.zoomMethod", int.class); + + /** + * <p>Turn on AE priority mode.</p> + * <p>This control is only effective if {@link CaptureRequest#CONTROL_MODE android.control.mode} is + * AUTO and {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is set to one of its + * ON modes, with the exception of ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY.</p> + * <p>When a priority mode is enabled, the camera device's + * auto-exposure routine will maintain the application's + * selected parameters relevant to the priority mode while overriding + * the remaining exposure parameters + * ({@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}, {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}, and + * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}). For example, if + * SENSOR_SENSITIVITY_PRIORITY mode is enabled, the camera device will + * maintain the application-selected {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity} + * while adjusting {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime} + * and {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}. The overridden fields for a + * given capture will be available in its CaptureResult.</p> + * <p><b>Possible values:</b></p> + * <ul> + * <li>{@link #CONTROL_AE_PRIORITY_MODE_OFF OFF}</li> + * <li>{@link #CONTROL_AE_PRIORITY_MODE_SENSOR_SENSITIVITY_PRIORITY SENSOR_SENSITIVITY_PRIORITY}</li> + * <li>{@link #CONTROL_AE_PRIORITY_MODE_SENSOR_EXPOSURE_TIME_PRIORITY SENSOR_EXPOSURE_TIME_PRIORITY}</li> + * </ul> + * + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * + * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_MODE + * @see CaptureRequest#SENSOR_EXPOSURE_TIME + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @see CaptureRequest#SENSOR_SENSITIVITY + * @see #CONTROL_AE_PRIORITY_MODE_OFF + * @see #CONTROL_AE_PRIORITY_MODE_SENSOR_SENSITIVITY_PRIORITY + * @see #CONTROL_AE_PRIORITY_MODE_SENSOR_EXPOSURE_TIME_PRIORITY + */ + @PublicKey + @NonNull + @FlaggedApi(Flags.FLAG_AE_PRIORITY) + public static final Key<Integer> CONTROL_AE_PRIORITY_MODE = + new Key<Integer>("android.control.aePriorityMode", int.class); + + /** * <p>Operation mode for edge * enhancement.</p> * <p>Edge enhancement improves sharpness and details in the captured image. OFF means @@ -4199,7 +4280,9 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * duration exposed to the nearest possible value (rather than expose longer). * The final exposure time used will be available in the output capture result.</p> * <p>This control is only effective if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} or {@link CaptureRequest#CONTROL_MODE android.control.mode} is set to - * OFF; otherwise the auto-exposure algorithm will override this value.</p> + * OFF; otherwise the auto-exposure algorithm will override this value. However, in the + * case that {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} is set to SENSOR_EXPOSURE_TIME_PRIORITY, this + * control will be effective and not controlled by the auto-exposure algorithm.</p> * <p><b>Units</b>: Nanoseconds</p> * <p><b>Range of valid values:</b><br> * {@link CameraCharacteristics#SENSOR_INFO_EXPOSURE_TIME_RANGE android.sensor.info.exposureTimeRange}</p> @@ -4209,6 +4292,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> * * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE * @see CaptureRequest#CONTROL_MODE * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL * @see CameraCharacteristics#SENSOR_INFO_EXPOSURE_TIME_RANGE @@ -4317,7 +4401,9 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * value. The final sensitivity used will be available in the * output capture result.</p> * <p>This control is only effective if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} or {@link CaptureRequest#CONTROL_MODE android.control.mode} is set to - * OFF; otherwise the auto-exposure algorithm will override this value.</p> + * OFF; otherwise the auto-exposure algorithm will override this value. However, in the + * case that {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} is set to SENSOR_SENSITIVITY_PRIORITY, this + * control will be effective and not controlled by the auto-exposure algorithm.</p> * <p>Note that for devices supporting postRawSensitivityBoost, the total sensitivity applied * to the final processed image is the combination of {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity} and * {@link CaptureRequest#CONTROL_POST_RAW_SENSITIVITY_BOOST android.control.postRawSensitivityBoost}. In case the application uses the sensor @@ -4333,6 +4419,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> * * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE * @see CaptureRequest#CONTROL_MODE * @see CaptureRequest#CONTROL_POST_RAW_SENSITIVITY_BOOST * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL diff --git a/core/java/android/hardware/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java index e583627c0960..8b4d0da147bc 100644 --- a/core/java/android/hardware/devicestate/DeviceState.java +++ b/core/java/android/hardware/devicestate/DeviceState.java @@ -172,6 +172,23 @@ public final class DeviceState { */ public static final int PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT = 17; + /** + * Property that indicates that this state corresponds to the device state for rear display + * mode, where both the inner and outer displays are on. In this state, the outer display + * is the default display where the app is shown, and the inner display is used by the system to + * show a UI affordance for exiting the mode. + * + * Note that this value should generally not be used, and may be removed in the future (e.g. + * if or when it becomes the only type of rear display mode when + * {@link android.hardware.devicestate.feature.flags.Flags#deviceStateRdmV2} is removed). + * + * As such, clients should strongly consider relying on {@link #PROPERTY_FEATURE_REAR_DISPLAY} + * instead. + * + * @hide + */ + public static final int PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT = 1001; + /** @hide */ @IntDef(prefix = {"PROPERTY_"}, flag = false, value = { PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED, @@ -190,7 +207,8 @@ public final class DeviceState { PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE, PROPERTY_EXTENDED_DEVICE_STATE_EXTERNAL_DISPLAY, PROPERTY_FEATURE_REAR_DISPLAY, - PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT + PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT, + PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT }) @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) diff --git a/core/java/android/hardware/devicestate/feature/flags.aconfig b/core/java/android/hardware/devicestate/feature/flags.aconfig index 98ba9192044d..6230f4dbf6f4 100644 --- a/core/java/android/hardware/devicestate/feature/flags.aconfig +++ b/core/java/android/hardware/devicestate/feature/flags.aconfig @@ -29,4 +29,13 @@ flag { metadata { purpose: PURPOSE_BUGFIX } +} + +flag { + name: "device_state_rdm_v2" + is_exported: true + namespace: "windowing_sdk" + description: "Enables Rear Display Mode V2, where the inner display shows the user a UI affordance for exiting the state" + bug: "372486634" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index 506a19cce159..24951c4d516e 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -119,6 +119,8 @@ public final class KeyGestureEvent { public static final int KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE = 71; public static final int KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN = 72; public static final int KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT = 73; + public static final int KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION = 74; + public static final int KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK = 75; public static final int FLAG_CANCELLED = 1; @@ -207,6 +209,8 @@ public final class KeyGestureEvent { KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE, KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN, KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT, + KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION, + KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK, }) @Retention(RetentionPolicy.SOURCE) public @interface KeyGestureType { @@ -781,6 +785,10 @@ public final class KeyGestureEvent { return "KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN"; case KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT: return "KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT"; + case KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION: + return "KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION"; + case KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK: + return "KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK"; default: return Integer.toHexString(value); } diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index a8eb11d88aa4..4b2f2c218e5a 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -153,8 +153,8 @@ flag { flag { name: "override_power_key_behavior_in_focused_window" - namespace: "input_native" - description: "Allows privileged focused windows to capture power key events." + namespace: "wallet_integration" + description: "Allows privileged focused windows to override the power key double tap behavior." bug: "357144512" } diff --git a/core/java/android/hardware/usb/UsbManager.java b/core/java/android/hardware/usb/UsbManager.java index 92608d048135..d2e232a94622 100644 --- a/core/java/android/hardware/usb/UsbManager.java +++ b/core/java/android/hardware/usb/UsbManager.java @@ -54,6 +54,11 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -823,6 +828,216 @@ public class UsbManager { } } + /** + * Opens the handle for accessory, marks it as input or output, and adds it to the map + * if it is the first time the accessory has had an I/O stream associated with it. + */ + private AccessoryHandle openHandleForAccessory(UsbAccessory accessory, + boolean openingInputStream) + throws RemoteException { + synchronized (mAccessoryHandleMapLock) { + if (mAccessoryHandleMap == null) { + mAccessoryHandleMap = new ArrayMap<>(); + } + + // If accessory isn't available in map + if (!mAccessoryHandleMap.containsKey(accessory)) { + // open accessory and store associated AccessoryHandle in map + ParcelFileDescriptor pfd = mService.openAccessory(accessory); + AccessoryHandle newHandle = new AccessoryHandle(pfd, openingInputStream, + !openingInputStream); + mAccessoryHandleMap.put(accessory, newHandle); + + return newHandle; + } + + // if accessory is already in map, get modified handle + AccessoryHandle currentHandle = mAccessoryHandleMap.get(accessory); + if (currentHandle == null) { + throw new IllegalStateException("Accessory doesn't have an associated handle yet!"); + } + + AccessoryHandle modifiedHandle = getModifiedHandleForOpeningStream( + openingInputStream, currentHandle); + + mAccessoryHandleMap.put(accessory, modifiedHandle); + + return modifiedHandle; + } + } + + private AccessoryHandle getModifiedHandleForOpeningStream(boolean openingInputStream, + @NonNull AccessoryHandle currentHandle) { + if (currentHandle.isInputStreamOpened() && openingInputStream) { + throw new IllegalStateException("Input stream already open for this accessory! " + + "Please close the existing input stream before opening a new one."); + } + + if (currentHandle.isOutputStreamOpened() && !openingInputStream) { + throw new IllegalStateException("Output stream already open for this accessory! " + + "Please close the existing output stream before opening a new one."); + } + + boolean isInputStreamOpened = openingInputStream || currentHandle.isInputStreamOpened(); + boolean isOutputStreamOpened = !openingInputStream || currentHandle.isOutputStreamOpened(); + + return new AccessoryHandle( + currentHandle.getPfd(), isInputStreamOpened, isOutputStreamOpened); + } + + /** + * Marks the handle for the given accessory closed for input or output, and closes the handle + * and removes it from the map if there are no more I/O streams associated with the handle. + */ + private void closeHandleForAccessory(UsbAccessory accessory, boolean closingInputStream) + throws IOException { + synchronized (mAccessoryHandleMapLock) { + AccessoryHandle currentHandle = mAccessoryHandleMap.get(accessory); + + if (currentHandle == null) { + throw new IllegalStateException( + "No handle has been initialised for this accessory!"); + } + + AccessoryHandle modifiedHandle = getModifiedHandleForClosingStream( + closingInputStream, currentHandle); + if (!modifiedHandle.isOpen()) { + //close handle and remove accessory handle pair from map + modifiedHandle.getPfd().close(); + mAccessoryHandleMap.remove(accessory); + } else { + mAccessoryHandleMap.put(accessory, modifiedHandle); + } + } + } + + private AccessoryHandle getModifiedHandleForClosingStream(boolean closingInputStream, + @NonNull AccessoryHandle currentHandle) { + if (!currentHandle.isInputStreamOpened() && closingInputStream) { + throw new IllegalStateException( + "Attempting to close an input stream that has not been opened " + + "for this accessory!"); + } + + if (!currentHandle.isOutputStreamOpened() && !closingInputStream) { + throw new IllegalStateException( + "Attempting to close an output stream that has not been opened " + + "for this accessory!"); + } + + boolean isInputStreamOpened = !closingInputStream && currentHandle.isInputStreamOpened(); + boolean isOutputStreamOpened = closingInputStream && currentHandle.isOutputStreamOpened(); + + return new AccessoryHandle( + currentHandle.getPfd(), isInputStreamOpened, isOutputStreamOpened); + } + + /** + * An InputStream you can create on a UsbAccessory, which will + * take care of calling {@link ParcelFileDescriptor#close + * ParcelFileDescriptor.close()} for you when the stream is closed. + */ + private class AccessoryAutoCloseInputStream extends FileInputStream { + + private final ParcelFileDescriptor mPfd; + private final UsbAccessory mAccessory; + + AccessoryAutoCloseInputStream(UsbAccessory accessory, ParcelFileDescriptor pfd) { + super(pfd.getFileDescriptor()); + this.mAccessory = accessory; + this.mPfd = pfd; + } + + @Override + public void close() throws IOException { + /* TODO(b/377850642) : Ensure the stream is closed even if client does not + explicitly close the stream to avoid corrupt FDs*/ + super.close(); + closeHandleForAccessory(mAccessory, true); + } + + + @Override + public int read() throws IOException { + final int result = super.read(); + checkError(result); + return result; + } + + @Override + public int read(byte[] b) throws IOException { + final int result = super.read(b); + checkError(result); + return result; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + final int result = super.read(b, off, len); + checkError(result); + return result; + } + + private void checkError(int result) throws IOException { + if (result == -1 && mPfd.canDetectErrors()) { + mPfd.checkError(); + } + } + } + + /** + * An OutputStream you can create on a UsbAccessory, which will + * take care of calling {@link ParcelFileDescriptor#close + * ParcelFileDescriptor.close()} for you when the stream is closed. + */ + private class AccessoryAutoCloseOutputStream extends FileOutputStream { + private final UsbAccessory mAccessory; + + AccessoryAutoCloseOutputStream(UsbAccessory accessory, ParcelFileDescriptor pfd) { + super(pfd.getFileDescriptor()); + mAccessory = accessory; + } + + @Override + public void close() throws IOException { + /* TODO(b/377850642) : Ensure the stream is closed even if client does not + explicitly close the stream to avoid corrupt FDs*/ + super.close(); + closeHandleForAccessory(mAccessory, false); + } + } + + /** + * Holds file descriptor and marks whether input and output streams have been opened for it. + */ + private static class AccessoryHandle { + private final ParcelFileDescriptor mPfd; + private final boolean mInputStreamOpened; + private final boolean mOutputStreamOpened; + AccessoryHandle(ParcelFileDescriptor parcelFileDescriptor, + boolean inputStreamOpened, boolean outputStreamOpened) { + mPfd = parcelFileDescriptor; + mInputStreamOpened = inputStreamOpened; + mOutputStreamOpened = outputStreamOpened; + } + + public ParcelFileDescriptor getPfd() { + return mPfd; + } + + public boolean isInputStreamOpened() { + return mInputStreamOpened; + } + + public boolean isOutputStreamOpened() { + return mOutputStreamOpened; + } + + public boolean isOpen() { + return (mInputStreamOpened || mOutputStreamOpened); + } + } + private final Context mContext; private final IUsbManager mService; private final Object mDisplayPortListenersLock = new Object(); @@ -831,6 +1046,11 @@ public class UsbManager { @GuardedBy("mDisplayPortListenersLock") private DisplayPortAltModeInfoDispatchingListener mDisplayPortServiceListener; + private final Object mAccessoryHandleMapLock = new Object(); + @GuardedBy("mAccessoryHandleMapLock") + private ArrayMap<UsbAccessory, AccessoryHandle> mAccessoryHandleMap; + + /** * @hide */ @@ -922,6 +1142,10 @@ public class UsbManager { * data of a USB transfer should be read at once. If only a partial request is read the rest of * the transfer is dropped. * + * <p>It is strongly recommended to use newer methods instead of this method, + * since this method may provide sub-optimal performance on some devices. + * This method could potentially face interim performance degradation as well. + * * @param accessory the USB accessory to open * @return file descriptor, or null if the accessory could not be opened. */ @@ -935,6 +1159,49 @@ public class UsbManager { } /** + * Opens an input stream for reading from the USB accessory. + * If accessory is not open at this point, accessory will first be opened. + * <p>If data is read from the created {@link java.io.InputStream} all + * data of a USB transfer should be read at once. If only a partial request is read, the rest of + * the transfer is dropped. + * <p>The caller is responsible for ensuring that the returned stream is closed. + * + * @param accessory the USB accessory to open an input stream for + * @return input stream to read from given USB accessory + */ + @FlaggedApi(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API) + @RequiresFeature(PackageManager.FEATURE_USB_ACCESSORY) + public @NonNull InputStream openAccessoryInputStream(@NonNull UsbAccessory accessory) { + try { + return new AccessoryAutoCloseInputStream(accessory, + openHandleForAccessory(accessory, true).getPfd()); + + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Opens an output stream for writing to the USB accessory. + * If accessory is not open at this point, accessory will first be opened. + * <p>The caller is responsible for ensuring that the returned stream is closed. + * + * @param accessory the USB accessory to open an output stream for + * @return output stream to write to given USB accessory + */ + @FlaggedApi(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API) + @RequiresFeature(PackageManager.FEATURE_USB_ACCESSORY) + public @NonNull OutputStream openAccessoryOutputStream(@NonNull UsbAccessory accessory) { + try { + return new AccessoryAutoCloseOutputStream(accessory, + openHandleForAccessory(accessory, false).getPfd()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + } + + /** * Gets the functionfs control file descriptor for the given function, with * the usb descriptors and strings already written. The file descriptor is used * by the function implementation to handle events and control requests. @@ -1293,7 +1560,7 @@ public class UsbManager { * <p> * This function returns the current USB bandwidth through USB Gadget HAL. * It should be used when Android device is in USB peripheral mode and - * connects to a USB host. If USB state is not configued, API will return + * connects to a USB host. If USB state is not configured, API will return * {@value #USB_DATA_TRANSFER_RATE_UNKNOWN}. In addition, the unit of the * return value is Mbps. * </p> diff --git a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig index 3b7a9e95c521..b719a7c6daac 100644 --- a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig +++ b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig @@ -31,3 +31,11 @@ flag { description: "Feature flag to enable exposing usb speed system api" bug: "373653182" } + +flag { + name: "enable_accessory_stream_api" + is_exported: true + namespace: "usb" + description: "Feature flag to enable stream APIs for Accessory mode" + bug: "369356693" +} diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 8c3f0ef08039..ae8366817f2b 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -55,6 +55,7 @@ import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECT import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_OTHER; import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED; import static android.view.inputmethod.Flags.FLAG_CONNECTIONLESS_HANDWRITING; +import static android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API; import static android.view.inputmethod.Flags.ctrlShiftShortcut; import static android.view.inputmethod.Flags.predictiveBackIme; @@ -4392,6 +4393,39 @@ public class InputMethodService extends AbstractInputMethodService { } /** + * Called when the requested visibility of a custom IME Switcher button changes. + * + * <p>When the system provides an IME navigation bar, it may decide to show an IME Switcher + * button inside this bar. However, the IME can request hiding the bar provided by the system + * with {@code getWindowInsetsController().hide(captionBar())} (the IME navigation bar provides + * {@link Type#captionBar() captionBar} insets to the IME window). If the request is successful, + * then it becomes the IME's responsibility to provide a custom IME Switcher button in its + * input view, with equivalent functionality.</p> + * + * <p>This custom button is only requested to be visible when the system provides the IME + * navigation bar, both the bar and the IME Switcher button inside it should be visible, + * but the IME successfully requested to hide the bar. This does not depend on the current + * visibility of the IME. It could be called with {@code true} while the IME is hidden, in + * which case the IME should prepare to show the button as soon as the IME itself is shown.</p> + * + * <p>This is only called when the requested visibility changes. The default value is + * {@code false} and as such, this will not be called initially if the resulting value is + * {@code false}.</p> + * + * <p>This can be called at any time after {@link #onCreate}, even if the IME is not currently + * visible. However, this is not guaranteed to be called before the IME is shown, as it depends + * on when the IME requested hiding the IME navigation bar. If the request is sent during + * the showing flow (e.g. during {@link #onStartInputView}), this will be called shortly after + * {@link #onWindowShown}, but before the first IME frame is drawn.</p> + * + * @param visible whether the button is requested visible or not. + */ + @FlaggedApi(FLAG_IME_SWITCHER_REVAMP_API) + public void onCustomImeSwitcherButtonRequestedVisible(boolean visible) { + // Intentionally empty + } + + /** * Called when the IME switch button was clicked from the client. Depending on the number of * enabled IME subtypes, this will either switch to the next IME/subtype, or show the input * method picker dialog. diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java index b08454dd7f8f..38be8d9f772d 100644 --- a/core/java/android/inputmethodservice/NavigationBarController.java +++ b/core/java/android/inputmethodservice/NavigationBarController.java @@ -41,6 +41,7 @@ import android.view.WindowInsets; import android.view.WindowInsetsController.Appearance; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; +import android.view.inputmethod.Flags; import android.view.inputmethod.InputMethodManager; import android.widget.FrameLayout; @@ -178,6 +179,9 @@ final class NavigationBarController { private boolean mDrawLegacyNavigationBarBackground; + /** Whether a custom IME Switcher button should be visible. */ + private boolean mCustomImeSwitcherVisible; + private final Rect mTempRect = new Rect(); private final int[] mTempPos = new int[2]; @@ -265,6 +269,7 @@ final class NavigationBarController { // IME navigation bar. boolean visible = insets.isVisible(captionBar()); mNavigationBarFrame.setVisibility(visible ? View.VISIBLE : View.GONE); + checkCustomImeSwitcherVisibility(); } return view.onApplyWindowInsets(insets); }); @@ -491,6 +496,8 @@ final class NavigationBarController { mShouldShowImeSwitcherWhenImeIsShown; mShouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherWhenImeIsShown; + checkCustomImeSwitcherVisibility(); + mService.mWindow.getWindow().getDecorView().getWindowInsetsController() .setImeCaptionBarInsetsHeight(getImeCaptionBarHeight(imeDrawsImeNavBar)); @@ -616,12 +623,33 @@ final class NavigationBarController { && mNavigationBarFrame.getVisibility() == View.VISIBLE; } + /** + * Checks if a custom IME Switcher button should be visible, and notifies the IME when this + * state changes. This can only be {@code true} if three conditions are met: + * + * <li>The IME should draw the IME navigation bar.</li> + * <li>The IME Switcher button should be visible when the IME is visible.</li> + * <li>The IME navigation bar should be visible, but was requested hidden by the IME.</li> + */ + private void checkCustomImeSwitcherVisibility() { + if (!Flags.imeSwitcherRevampApi()) { + return; + } + final boolean visible = mImeDrawsImeNavBar && mShouldShowImeSwitcherWhenImeIsShown + && mNavigationBarFrame != null && !isShown(); + if (visible != mCustomImeSwitcherVisible) { + mCustomImeSwitcherVisible = visible; + mService.onCustomImeSwitcherButtonRequestedVisible(mCustomImeSwitcherVisible); + } + } + @Override public String toDebugString() { return "{mImeDrawsImeNavBar=" + mImeDrawsImeNavBar + " mNavigationBarFrame=" + mNavigationBarFrame + " mShouldShowImeSwitcherWhenImeIsShown=" + mShouldShowImeSwitcherWhenImeIsShown + + " mCustomImeSwitcherVisible=" + mCustomImeSwitcherVisible + " mAppearance=0x" + Integer.toHexString(mAppearance) + " mDarkIntensity=" + mDarkIntensity + " mDrawLegacyNavigationBarBackground=" + mDrawLegacyNavigationBarBackground diff --git a/core/java/android/os/WorkSource.java b/core/java/android/os/WorkSource.java index 6d4e28403908..517418a717fb 100644 --- a/core/java/android/os/WorkSource.java +++ b/core/java/android/os/WorkSource.java @@ -1011,13 +1011,7 @@ public class WorkSource implements Parcelable { return mTags.length > 0 ? mTags[0] : null; } - // TODO: The following three trivial getters are purely for testing and will be removed - // once we have higher level logic in place, e.g for serializing this WorkChain to a proto, - // diffing it etc. - - /** @hide */ - @VisibleForTesting public int[] getUids() { int[] uids = new int[mSize]; System.arraycopy(mUids, 0, uids, 0, mSize); @@ -1025,7 +1019,6 @@ public class WorkSource implements Parcelable { } /** @hide */ - @VisibleForTesting public String[] getTags() { String[] tags = new String[mSize]; System.arraycopy(mTags, 0, tags, 0, mSize); @@ -1033,7 +1026,6 @@ public class WorkSource implements Parcelable { } /** @hide */ - @VisibleForTesting public int getSize() { return mSize; } diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index 9c83bc2c88ec..d9db28e0b3c3 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -243,6 +243,15 @@ flag { } flag { + name: "update_engine_api" + namespace: "art_mainline" + description: "Update Engine APIs for ART" + is_exported: true + is_fixed_read_only: true + bug: "377557749" +} + +flag { namespace: "system_performance" name: "perfetto_sdk_tracing" description: "Tracing using Perfetto SDK." diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 60a0ae3f107d..92c5c20a1f82 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -386,3 +386,17 @@ flag { description: "This fixed read-only flag is used to enable new ranging permission for all ranging use cases." bug: "370977414" } + +flag { + name: "system_selection_toolbar_enabled" + namespace: "permissions" + description: "Enables the system selection toolbar feature." + bug: "363318732" +} + +flag { + name: "use_system_selection_toolbar_in_sysui" + namespace: "permissions" + description: "Uses the SysUi process to host the SelectionToolbarRenderService." + bug: "363318732" +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index d19681c86320..ef351719ea70 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -8687,6 +8687,19 @@ public final class Settings { public static final String ACCESSIBILITY_QS_TARGETS = "accessibility_qs_targets"; /** + * Setting specifying the accessibility services, accessibility shortcut targets, + * or features to be toggled via a keyboard shortcut gesture. + * + * <p> This is a colon-separated string list which contains the flattened + * {@link ComponentName} and the class name of a system class implementing a supported + * accessibility feature. + * + * @hide + */ + public static final String ACCESSIBILITY_KEY_GESTURE_TARGETS = + "accessibility_key_gesture_targets"; + + /** * The system class name of magnification controller which is a target to be toggled via * accessibility shortcut or accessibility button. * diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig index 7cb0ffcfcc72..ce901217d700 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -109,7 +109,7 @@ flag { flag { name: "afl_api" - namespace: "platform_security" + namespace: "hardware_backed_security" description: "AFL feature" bug: "365994454" } diff --git a/core/java/android/service/settings/preferences/ISettingsPreferenceService.aidl b/core/java/android/service/settings/preferences/ISettingsPreferenceService.aidl new file mode 100644 index 000000000000..64a8b90fe581 --- /dev/null +++ b/core/java/android/service/settings/preferences/ISettingsPreferenceService.aidl @@ -0,0 +1,18 @@ +package android.service.settings.preferences; + +import android.service.settings.preferences.GetValueRequest; +import android.service.settings.preferences.IGetValueCallback; +import android.service.settings.preferences.IMetadataCallback; +import android.service.settings.preferences.ISetValueCallback; +import android.service.settings.preferences.MetadataRequest; +import android.service.settings.preferences.SetValueRequest; + +/** @hide */ +oneway interface ISettingsPreferenceService { + @EnforcePermission("READ_SYSTEM_PREFERENCES") + void getAllPreferenceMetadata(in MetadataRequest request, IMetadataCallback callback) = 1; + @EnforcePermission("READ_SYSTEM_PREFERENCES") + void getPreferenceValue(in GetValueRequest request, IGetValueCallback callback) = 2; + @EnforcePermission(allOf = {"READ_SYSTEM_PREFERENCES", "WRITE_SYSTEM_PREFERENCES"}) + void setPreferenceValue(in SetValueRequest request, ISetValueCallback callback) = 3; +} diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceService.java b/core/java/android/service/settings/preferences/SettingsPreferenceService.java new file mode 100644 index 000000000000..4a4b5d201f09 --- /dev/null +++ b/core/java/android/service/settings/preferences/SettingsPreferenceService.java @@ -0,0 +1,201 @@ +/* + * 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.service.settings.preferences; + +import android.Manifest; +import android.annotation.EnforcePermission; +import android.annotation.FlaggedApi; +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.IBinder; +import android.os.OutcomeReceiver; +import android.os.PermissionEnforcer; +import android.os.RemoteException; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settingslib.flags.Flags; + +/** + * Base class for a service that exposes its settings preferences to external access. + * <p>This class is to be implemented by apps that contribute to the Android Settings surface. + * Access to this service is permission guarded by + * {@link android.permission.READ_SYSTEM_PREFERENCES} for binding and reading, and guarded by both + * {@link android.permission.READ_SYSTEM_PREFERENCES} and + * {@link android.permission.WRITE_SYSTEM_PREFERENCES} for writing. An additional checks for access + * control are the responsibility of the implementing class. + * + * <p>This implementation must correspond to an exported service declaration in the host app + * AndroidManifest.xml as follows + * <pre class="prettyprint"> + * {@literal + * <service + * android:permission="android.permission.READ_SYSTEM_PREFERENCES" + * android:exported="true"> + * <intent-filter> + * <action android:name="android.service.settings.preferences.action.PREFERENCE_SERVICE" /> + * </intent-filter> + * </service>} + * </pre> + * + * <ul> + * <li>It is recommended to expose the metadata for most, if not all, preferences within a + * settings app, thus implementing {@link #onGetAllPreferenceMetadata}. + * <li>Exposing preferences for read access of their values is up to the implementer, but any + * exposed must be a subset of the preferences exposed in {@link #onGetAllPreferenceMetadata}. + * To expose a preference for read access, the implementation will contain + * {@link #onGetPreferenceValue}. + * <li>Exposing a preference for write access of their values is up to the implementer, but should + * be done so with extra care and consideration, both for security and privacy. These must also + * be a subset of those exposed in {@link #onGetAllPreferenceMetadata}. To expose a preference for + * write access, the implementation will contain {@link #onSetPreferenceValue}. + * </ul> + */ +@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) +public abstract class SettingsPreferenceService extends Service { + + /** + * Intent Action corresponding to a {@link SettingsPreferenceService}. Note that any checks for + * such services must be accompanied by a check to ensure the host is a system application. + * Given an {@link android.content.pm.ApplicationInfo} you can check for + * {@link android.content.pm.ApplicationInfo#FLAG_SYSTEM}, or when querying + * {@link PackageManager#queryIntentServices} you can provide the flag + * {@link PackageManager#MATCH_SYSTEM_ONLY}. + */ + public static final String ACTION_PREFERENCE_SERVICE = + "android.service.settings.preferences.action.PREFERENCE_SERVICE"; + + /** @hide */ + @NonNull + @Override + public final IBinder onBind(@Nullable Intent intent) { + return new ISettingsPreferenceService.Stub( + PermissionEnforcer.fromContext(getApplicationContext())) { + @EnforcePermission(Manifest.permission.READ_SYSTEM_PREFERENCES) + @Override + public void getAllPreferenceMetadata(MetadataRequest request, + IMetadataCallback callback) { + getAllPreferenceMetadata_enforcePermission(); + onGetAllPreferenceMetadata(request, new OutcomeReceiver<>() { + @Override + public void onResult(MetadataResult result) { + try { + callback.onSuccess(result); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + @Override + public void onError(@NonNull Exception error) { + try { + callback.onFailure(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + }); + } + + @EnforcePermission(Manifest.permission.READ_SYSTEM_PREFERENCES) + @Override + public void getPreferenceValue(GetValueRequest request, IGetValueCallback callback) { + getPreferenceValue_enforcePermission(); + onGetPreferenceValue(request, new OutcomeReceiver<>() { + @Override + public void onResult(GetValueResult result) { + try { + callback.onSuccess(result); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + @Override + public void onError(@NonNull Exception error) { + try { + callback.onFailure(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + }); + } + + @EnforcePermission(allOf = { + Manifest.permission.READ_SYSTEM_PREFERENCES, + Manifest.permission.WRITE_SYSTEM_PREFERENCES + }) + @Override + public void setPreferenceValue(SetValueRequest request, ISetValueCallback callback) { + setPreferenceValue_enforcePermission(); + onSetPreferenceValue(request, new OutcomeReceiver<>() { + @Override + public void onResult(SetValueResult result) { + try { + callback.onSuccess(result); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + @Override + public void onError(@NonNull Exception error) { + try { + callback.onFailure(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + }); + } + }; + } + + /** + * Retrieve the metadata for all exposed settings preferences within this application. This + * data should be a snapshot of their state at the time of this method being called. + * @param request object to specify request parameters + * @param callback object to receive result or failure of request + */ + public abstract void onGetAllPreferenceMetadata( + @NonNull MetadataRequest request, + @NonNull OutcomeReceiver<MetadataResult, Exception> callback); + + /** + * Retrieve the current value of the requested settings preference. If this value is not exposed + * or cannot be obtained for some reason, the corresponding result code will be set on the + * result object. + * @param request object to specify request parameters + * @param callback object to receive result or failure of request + */ + public abstract void onGetPreferenceValue( + @NonNull GetValueRequest request, + @NonNull OutcomeReceiver<GetValueResult, Exception> callback); + + /** + * Set the value within the request to the target settings preference. If this value cannot + * be written for some reason, the corresponding result code will be set on the result object. + * @param request object to specify request parameters + * @param callback object to receive result or failure of request + */ + public abstract void onSetPreferenceValue( + @NonNull SetValueRequest request, + @NonNull OutcomeReceiver<SetValueResult, Exception> callback); +} diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceServiceClient.java b/core/java/android/service/settings/preferences/SettingsPreferenceServiceClient.java new file mode 100644 index 000000000000..39995a47fcbe --- /dev/null +++ b/core/java/android/service/settings/preferences/SettingsPreferenceServiceClient.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.settings.preferences; + +import static android.service.settings.preferences.SettingsPreferenceService.ACTION_PREFERENCE_SERVICE; + +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; +import android.annotation.TestApi; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.IBinder; +import android.os.OutcomeReceiver; +import android.os.RemoteException; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settingslib.flags.Flags; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Client class responsible for binding to and interacting with an instance of + * {@link SettingsPreferenceService}. + * <p>This is a convenience class to handle the lifecycle of the service connection. + * <p>This client will only interact with one instance at a time, + * so if the caller requires multiple instances (multiple applications that provide settings), then + * the caller must create multiple client classes, one for each instance required. To find all + * available services, a caller may query {@link android.content.pm.PackageManager} for applications + * that provide the intent action {@link SettingsPreferenceService#ACTION_PREFERENCE_SERVICE} that + * are also system applications ({@link android.content.pm.ApplicationInfo#FLAG_SYSTEM}). + */ +@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) +public class SettingsPreferenceServiceClient implements AutoCloseable { + + private final Context mContext; + private final Intent mServiceIntent; + private final ServiceConnection mServiceConnection; + private final boolean mSystemOnly; + private ISettingsPreferenceService mRemoteService; + + /** + * Construct a client for binding to a {@link SettingsPreferenceService} provided by the + * application corresponding to the provided package name. + * @param packageName - package name for which this client will initiate a service binding + */ + public SettingsPreferenceServiceClient(@NonNull Context context, + @NonNull String packageName) { + this(context, packageName, true, null); + } + + /** + * @hide Only to be called directly by test + */ + @TestApi + public SettingsPreferenceServiceClient(@NonNull Context context, + @NonNull String packageName, + boolean systemOnly, + @Nullable ServiceConnection connectionListener) { + mContext = context.getApplicationContext(); + mServiceIntent = new Intent(ACTION_PREFERENCE_SERVICE).setPackage(packageName); + mSystemOnly = systemOnly; + mServiceConnection = createServiceConnection(connectionListener); + } + + /** + * Initiate binding to service. + * <p>If no service exists for the package provided or the package is not for a system + * application, no binding will occur. + */ + public void start() { + PackageManager pm = mContext.getPackageManager(); + PackageManager.ResolveInfoFlags flags; + if (mSystemOnly) { + flags = PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_SYSTEM_ONLY); + } else { + flags = PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_ALL); + } + List<ResolveInfo> infos = pm.queryIntentServices(mServiceIntent, flags); + if (infos.size() == 1) { + mContext.bindService(mServiceIntent, mServiceConnection, Context.BIND_AUTO_CREATE); + } + } + + /** + * If there is an active service binding, unbind from that service. + */ + public void stop() { + if (mRemoteService != null) { + mRemoteService = null; + mContext.unbindService(mServiceConnection); + } + } + + /** + * Retrieve the metadata for all exposed settings preferences within the application. + * @param request object to specify request parameters + * @param executor {@link Executor} on which to invoke the receiver + * @param receiver callback to receive the result or failure + */ + public void getAllPreferenceMetadata( + @NonNull MetadataRequest request, + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver<MetadataResult, Exception> receiver) { + if (mRemoteService == null) { + executor.execute(() -> + receiver.onError(new IllegalStateException("Service not ready"))); + return; + } + try { + mRemoteService.getAllPreferenceMetadata(request, new IMetadataCallback.Stub() { + @Override + public void onSuccess(MetadataResult result) { + executor.execute(() -> receiver.onResult(result)); + } + + @Override + public void onFailure() { + executor.execute(() -> receiver.onError( + new IllegalStateException("Service call failure"))); + } + }); + } catch (RemoteException | RuntimeException e) { + executor.execute(() -> receiver.onError(e)); + } + } + + /** + * Retrieve the current value of the requested settings preference. + * @param request object to specify request parameters + * @param executor {@link Executor} on which to invoke the receiver + * @param receiver callback to receive the result or failure + */ + public void getPreferenceValue(@NonNull GetValueRequest request, + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver<GetValueResult, Exception> receiver) { + if (mRemoteService == null) { + executor.execute(() -> + receiver.onError(new IllegalStateException("Service not ready"))); + return; + } + try { + mRemoteService.getPreferenceValue(request, new IGetValueCallback.Stub() { + @Override + public void onSuccess(GetValueResult result) { + executor.execute(() -> receiver.onResult(result)); + } + + @Override + public void onFailure() { + executor.execute(() -> receiver.onError( + new IllegalStateException("Service call failure"))); + } + }); + } catch (RemoteException | RuntimeException e) { + executor.execute(() -> receiver.onError(e)); + } + } + + /** + * Set the value on the target settings preference. + * @param request object to specify request parameters + * @param executor {@link Executor} on which to invoke the receiver + * @param receiver callback to receive the result or failure + */ + public void setPreferenceValue(@NonNull SetValueRequest request, + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver<SetValueResult, Exception> receiver) { + if (mRemoteService == null) { + executor.execute(() -> + receiver.onError(new IllegalStateException("Service not ready"))); + return; + } + try { + mRemoteService.setPreferenceValue(request, new ISetValueCallback.Stub() { + @Override + public void onSuccess(SetValueResult result) { + executor.execute(() -> receiver.onResult(result)); + } + + @Override + public void onFailure() { + executor.execute(() -> receiver.onError( + new IllegalStateException("Service call failure"))); + } + }); + } catch (RemoteException | RuntimeException e) { + executor.execute(() -> receiver.onError(e)); + } + } + + @NonNull + private ServiceConnection createServiceConnection(@Nullable ServiceConnection listener) { + return new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mRemoteService = getPreferenceServiceInterface(service); + if (listener != null) { + listener.onServiceConnected(name, service); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mRemoteService = null; + if (listener != null) { + listener.onServiceDisconnected(name); + } + } + }; + } + + @NonNull + private ISettingsPreferenceService getPreferenceServiceInterface(@NonNull IBinder service) { + return ISettingsPreferenceService.Stub.asInterface(service); + } + + /** + * This client handles a resource, thus is it important to appropriately close that resource + * when it is no longer needed. + * <p>This method is provided by {@link AutoCloseable} and calling it + * will unbind any service binding. + */ + @Override + public void close() { + stop(); + } +} diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index 02923eda308e..f43f172d7d5b 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -163,10 +163,12 @@ flag { } flag { - name: "typeface_redesign" + name: "typeface_redesign_readonly" namespace: "text" description: "Decouple variation settings, weight and style information from Typeface class" bug: "361260253" + # This feature does not support runtime flag switch which leads crash in System UI. + is_fixed_read_only: true } flag { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 9a2aa0b8a682..75d2da1b70e4 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -9951,11 +9951,13 @@ public final class ViewRootImpl implements ViewParent, return false; } - if (!mIsDrawing) { - destroyHardwareRenderer(); - } else { - Log.e(mTag, "Attempting to destroy the window while drawing!\n" + - " window=" + this + ", title=" + mWindowAttributes.getTitle()); + if (!com.android.graphics.hwui.flags.Flags.removeVriSketchyDestroy()) { + if (!mIsDrawing) { + destroyHardwareRenderer(); + } else { + Log.e(mTag, "Attempting to destroy the window while drawing!\n" + + " window=" + this + ", title=" + mWindowAttributes.getTitle()); + } } mHandler.sendEmptyMessage(MSG_DIE); return true; @@ -9976,9 +9978,9 @@ public final class ViewRootImpl implements ViewParent, dispatchDetachedFromWindow(); } - if (mAdded && !mFirst) { - destroyHardwareRenderer(); + destroyHardwareRenderer(); + if (mAdded && !mFirst) { if (mView != null) { int viewVisibility = mView.getVisibility(); boolean viewVisibilityChanged = mViewVisibility != viewVisibility; diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java index 0ab51e45a951..905f350ca6c5 100644 --- a/core/java/android/view/autofill/AutofillFeatureFlags.java +++ b/core/java/android/view/autofill/AutofillFeatureFlags.java @@ -316,6 +316,35 @@ public class AutofillFeatureFlags { // END AUTOFILL PCC CLASSIFICATION FLAGS + // START AUTOFILL REMOVE PRE_TRIGGER FLAGS + + /** + * Whether pre-trigger flow is disabled. + * + * @hide + */ + public static final String DEVICE_CONFIG_IMPROVE_FILL_DIALOG_ENABLED = "improve_fill_dialog"; + + /** + * Minimum amount of time (in milliseconds) to wait after IME animation finishes, and before + * starting fill dialog animation. + * + * @hide + */ + public static final String DEVICE_CONFIG_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS = + "fill_dialog_min_wait_after_animation_end_ms"; + + /** + * Sets a value of timeout in milliseconds, measured after animation end, during which fill + * dialog can be shown. If we are at time > animation_end_time + this timeout, fill dialog + * wouldn't be shown. + * + * @hide + */ + public static final String DEVICE_CONFIG_FILL_DIALOG_TIMEOUT_MS = "fill_dialog_timeout_ms"; + + // END AUTOFILL REMOVE PRE_TRIGGER FLAGS + /** * Define the max input length for autofill to show suggesiton UI * @@ -366,6 +395,17 @@ public class AutofillFeatureFlags { DEFAULT_AFAA_SHOULD_INCLUDE_ALL_AUTOFILL_TYPE_NOT_NONE_VIEWS_IN_ASSIST_STRUCTURE = true; // END AUTOFILL FOR ALL APPS DEFAULTS + // START AUTOFILL REMOVE PRE_TRIGGER FLAGS DEFAULTS + // Default for whether the pre trigger removal is enabled. + /** @hide */ + public static final boolean DEFAULT_IMPROVE_FILL_DIALOG_ENABLED = true; + // Default for whether the pre trigger removal is enabled. + /** @hide */ + public static final long DEFAULT_FILL_DIALOG_TIMEOUT_MS = 300; // 300 ms + /** @hide */ + public static final long DEFAULT_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS = 0; // 0 ms + // END AUTOFILL REMOVE PRE_TRIGGER FLAGS DEFAULTS + /** * @hide */ @@ -611,4 +651,48 @@ public class AutofillFeatureFlags { } // END AUTOFILL PCC CLASSIFICATION FUNCTIONS + + + // START AUTOFILL REMOVE PRE_TRIGGER + /** + * Whether Autofill Pre Trigger Removal is enabled. + * + * @hide + */ + public static boolean isImproveFillDialogEnabled() { + // TODO(b/266379948): Add condition for checking whether device has PCC first + + return DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_AUTOFILL, + DEVICE_CONFIG_IMPROVE_FILL_DIALOG_ENABLED, + DEFAULT_IMPROVE_FILL_DIALOG_ENABLED); + } + + /** + * Whether Autofill Pre Trigger Removal is enabled. + * + * @hide + */ + public static long getFillDialogTimeoutMs() { + // TODO(b/266379948): Add condition for checking whether device has PCC first + + return DeviceConfig.getLong( + DeviceConfig.NAMESPACE_AUTOFILL, + DEVICE_CONFIG_FILL_DIALOG_TIMEOUT_MS, + DEFAULT_FILL_DIALOG_TIMEOUT_MS); + } + + /** + * Whether Autofill Pre Trigger Removal is enabled. + * + * @hide + */ + public static long getFillDialogMinWaitAfterImeAnimationtEndMs() { + // TODO(b/266379948): Add condition for checking whether device has PCC first + + return DeviceConfig.getLong( + DeviceConfig.NAMESPACE_AUTOFILL, + DEVICE_CONFIG_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS, + DEFAULT_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS); + } } diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index dae87ddcb1bd..7a01ad340c56 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -54,6 +54,7 @@ public enum DesktopModeFlags { Flags::enableDesktopWindowingWallpaperActivity, true), ENABLE_DESKTOP_WINDOWING_MODALS_POLICY(Flags::enableDesktopWindowingModalsPolicy, true), ENABLE_THEMED_APP_HEADERS(Flags::enableThemedAppHeaders, true), + ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true), ENABLE_DESKTOP_WINDOWING_QUICK_SWITCH(Flags::enableDesktopWindowingQuickSwitch, true), ENABLE_APP_HEADER_WITH_TASK_DENSITY(Flags::enableAppHeaderWithTaskDensity, true), ENABLE_TASK_STACK_OBSERVER_IN_SHELL(Flags::enableTaskStackObserverInShell, true), @@ -75,7 +76,8 @@ public enum DesktopModeFlags { Flags::enableDesktopAppLaunchAlttabTransitions, false), ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS( Flags::enableDesktopAppLaunchTransitions, false), - ENABLE_DESKTOP_WINDOWING_PERSISTENCE(Flags::enableDesktopWindowingPersistence, false); + ENABLE_DESKTOP_WINDOWING_PERSISTENCE(Flags::enableDesktopWindowingPersistence, false), + ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true); private static final String TAG = "DesktopModeFlagsUtil"; // Function called to obtain aconfig flag value. diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index d39ecabbb2d2..f474b34ac390 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -353,6 +353,16 @@ flag { } flag { + name: "enable_desktop_system_dialogs_transitions" + namespace: "lse_desktop_experience" + description: "Enables custom transitions for system dialogs in Desktop Mode." + bug: "335638193" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_move_to_next_display_shortcut" namespace: "lse_desktop_experience" description: "Add new keyboard shortcut of moving a task into next display" diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 68e78fed29c5..d9de38a8bd34 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -268,6 +268,16 @@ flag { } flag { + name: "system_ui_post_animation_end" + namespace: "windowing_frontend" + description: "Run AnimatorListener#onAnimationEnd on next frame for SystemUI" + bug: "300035126" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "system_ui_immersive_confirmation_dialog" namespace: "windowing_frontend" description: "Enable the implementation of the immersive confirmation dialog on system UI side by default" diff --git a/core/java/com/android/internal/accessibility/common/ShortcutConstants.java b/core/java/com/android/internal/accessibility/common/ShortcutConstants.java index 44dceb9b7edb..4a49bb6720ef 100644 --- a/core/java/com/android/internal/accessibility/common/ShortcutConstants.java +++ b/core/java/com/android/internal/accessibility/common/ShortcutConstants.java @@ -63,6 +63,8 @@ public final class ShortcutConstants { * quickly tapping screen 2 times with two fingers as preferred shortcut. * {@code QUICK_SETTINGS} for displaying specifying the accessibility services or features which * choose Quick Settings as preferred shortcut. + * {@code KEY_GESTURE} for shortcuts which are directly from key gestures and should be + * activated always. */ @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -73,6 +75,7 @@ public final class ShortcutConstants { UserShortcutType.TWOFINGER_DOUBLETAP, UserShortcutType.QUICK_SETTINGS, UserShortcutType.GESTURE, + UserShortcutType.KEY_GESTURE, UserShortcutType.ALL }) public @interface UserShortcutType { @@ -84,8 +87,10 @@ public final class ShortcutConstants { int TWOFINGER_DOUBLETAP = 1 << 3; int QUICK_SETTINGS = 1 << 4; int GESTURE = 1 << 5; + int KEY_GESTURE = 1 << 6; // LINT.ThenChange(:shortcut_type_array) - int ALL = SOFTWARE | HARDWARE | TRIPLETAP | TWOFINGER_DOUBLETAP | QUICK_SETTINGS | GESTURE; + int ALL = SOFTWARE | HARDWARE | TRIPLETAP | TWOFINGER_DOUBLETAP | QUICK_SETTINGS | GESTURE + | KEY_GESTURE; } /** @@ -99,7 +104,8 @@ public final class ShortcutConstants { UserShortcutType.TRIPLETAP, UserShortcutType.TWOFINGER_DOUBLETAP, UserShortcutType.QUICK_SETTINGS, - UserShortcutType.GESTURE + UserShortcutType.GESTURE, + UserShortcutType.KEY_GESTURE // LINT.ThenChange(:shortcut_type_intdef) }; diff --git a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java index 2e0ff3db6c50..14ca0f8cae69 100644 --- a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java +++ b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java @@ -27,6 +27,7 @@ import static com.android.internal.accessibility.common.ShortcutConstants.SERVIC import static com.android.internal.accessibility.common.ShortcutConstants.USER_SHORTCUT_TYPES; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP; @@ -187,6 +188,7 @@ public final class ShortcutUtils { case TWOFINGER_DOUBLETAP -> Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED; case QUICK_SETTINGS -> Settings.Secure.ACCESSIBILITY_QS_TARGETS; + case KEY_GESTURE -> Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS; default -> throw new IllegalArgumentException( "Unsupported user shortcut type: " + type); }; @@ -209,6 +211,7 @@ public final class ShortcutUtils { TRIPLETAP; case Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED -> TWOFINGER_DOUBLETAP; + case Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS -> KEY_GESTURE; default -> throw new IllegalArgumentException( "Unsupported user shortcut key: " + key); }; diff --git a/core/jni/android_hardware_Camera.cpp b/core/jni/android_hardware_Camera.cpp index 50252c11ffb1..42406147b2f0 100644 --- a/core/jni/android_hardware_Camera.cpp +++ b/core/jni/android_hardware_Camera.cpp @@ -538,7 +538,7 @@ static bool attributionSourceStateForJavaParcel(JNIEnv *env, jobject jClientAttr return false; } - if (!(useContextAttributionSource && flags::use_context_attribution_source())) { + if (!(useContextAttributionSource && flags::data_delivery_permission_checks())) { clientAttribution.uid = Camera::USE_CALLING_UID; clientAttribution.pid = Camera::USE_CALLING_PID; } diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 606e829c41fa..6af742fb23f4 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -104,6 +104,7 @@ message SecureSettingsProto { optional SettingProto accessibility_single_finger_panning_enabled = 56 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto accessibility_gesture_targets = 57 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto display_daltonizer_saturation_level = 58 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto accessibility_key_gesture_targets = 59 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Accessibility accessibility = 2; diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 6682b858cce9..dc054a4a48ea 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4596,6 +4596,11 @@ exists on the device, the accessibility shortcut will be disabled by default. --> <string name="config_defaultAccessibilityService" translatable="false"></string> + <!-- The component name, flattened to a string, for the default select to speak service to be + enabled by the accessibility keyboard shortcut. If the service with the specified component + name is not preinstalled then this shortcut will do nothing. --> + <string name="config_defaultSelectToSpeakService" translatable="false"></string> + <!-- URI for default Accessibility notification sound when to enable accessibility shortcut. --> <string name="config_defaultAccessibilityNotificationSound" translatable="false"></string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index db81a3be440f..badb98686fb2 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3718,6 +3718,7 @@ <java-symbol type="string" name="color_correction_feature_name" /> <java-symbol type="string" name="reduce_bright_colors_feature_name" /> <java-symbol type="string" name="config_defaultAccessibilityService" /> + <java-symbol type="string" name="config_defaultSelectToSpeakService" /> <java-symbol type="string" name="config_defaultAccessibilityNotificationSound" /> <java-symbol type="string" name="accessibility_shortcut_spoken_feedback" /> <java-symbol type="array" name="config_trustedAccessibilityServices" /> diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index a382d798fb9b..f39508d6de15 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -151,6 +151,7 @@ android_test { ":HelloWorldUsingSdk1And2", ":HelloWorldUsingSdkMalformedNegativeVersion", ":CtsStaticSharedLibConsumerApp1", + ":CtsStaticSharedLibConsumerApp3", ], } diff --git a/core/tests/coretests/AndroidTest.xml b/core/tests/coretests/AndroidTest.xml index 3f7c83a82787..5d8ff87eca24 100644 --- a/core/tests/coretests/AndroidTest.xml +++ b/core/tests/coretests/AndroidTest.xml @@ -41,6 +41,8 @@ value="/data/local/tmp/tests/coretests/pm/HelloWorldSdk1.apk"/> <option name="push-file" key="CtsStaticSharedLibConsumerApp1.apk" value="/data/local/tmp/tests/coretests/pm/CtsStaticSharedLibConsumerApp1.apk"/> + <option name="push-file" key="CtsStaticSharedLibConsumerApp3.apk" + value="/data/local/tmp/tests/coretests/pm/CtsStaticSharedLibConsumerApp3.apk"/> </target_preparer> <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> diff --git a/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java b/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java index d4618d744644..0db49a72c51d 100644 --- a/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java +++ b/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java @@ -72,6 +72,12 @@ public class ApkLiteParseUtilsTest { private static final String TEST_APP_USING_SDK_MALFORMED_VERSION = "HelloWorldUsingSdkMalformedNegativeVersion.apk"; private static final String TEST_APP_USING_STATIC_LIB = "CtsStaticSharedLibConsumerApp1.apk"; + private static final String TEST_APP_USING_STATIC_LIB_TWO_CERTS = + "CtsStaticSharedLibConsumerApp3.apk"; + private static final String STATIC_LIB_CERT_1 = + "70fbd440503ec0bf41f3f21fcc83ffd39880133c27deb0945ed677c6f31d72fb"; + private static final String STATIC_LIB_CERT_2 = + "e49582ff3a0aa4c5589fc5feaac6b7d6e757199dd0c6742df7bf37c2ffef95f5"; private static final String TEST_SDK1 = "HelloWorldSdk1.apk"; private static final String TEST_SDK1_PACKAGE = "com.test.sdk1_1"; private static final String TEST_SDK1_NAME = "com.test.sdk1"; @@ -86,7 +92,7 @@ public class ApkLiteParseUtilsTest { @Before public void setUp() throws IOException { - mTmpDir = mTemporaryFolder.newFolder("DexMetadataHelperTest"); + mTmpDir = mTemporaryFolder.newFolder("ApkLiteParseUtilsTest"); } @After @@ -108,9 +114,8 @@ public class ApkLiteParseUtilsTest { assertThat(baseApk.getUsesSdkLibrariesVersionsMajor()).asList().containsExactly( TEST_SDK1_VERSION, TEST_SDK2_VERSION ); - for (String[] certDigests: baseApk.getUsesSdkLibrariesCertDigests()) { - assertThat(certDigests).asList().containsExactly(""); - } + String[][] expectedCerts = {{""}, {""}}; + assertThat(baseApk.getUsesSdkLibrariesCertDigests()).isEqualTo(expectedCerts); } @SuppressLint("CheckResult") @@ -126,18 +131,13 @@ public class ApkLiteParseUtilsTest { ApkLite baseApk = result.getResult(); String[][] liteCerts = baseApk.getUsesSdkLibrariesCertDigests(); - assertThat(liteCerts).isNotNull(); - for (String[] certDigests: liteCerts) { - assertThat(certDigests).asList().containsExactly(certDigest); - } + String[][] expectedCerts = {{certDigest}, {certDigest}}; + assertThat(liteCerts).isEqualTo(expectedCerts); // Same for package parser AndroidPackage pkg = mPackageParser2.parsePackage(apkFile, 0, true).hideAsFinal(); String[][] pkgCerts = pkg.getUsesSdkLibrariesCertDigests(); - assertThat(pkgCerts).isNotNull(); - for (int i = 0; i < liteCerts.length; i++) { - assertThat(liteCerts[i]).isEqualTo(pkgCerts[i]); - } + assertThat(liteCerts).isEqualTo(pkgCerts); } @@ -160,9 +160,7 @@ public class ApkLiteParseUtilsTest { String[][] liteCerts = baseApk.getUsesSdkLibrariesCertDigests(); String[][] pkgCerts = pkg.getUsesSdkLibrariesCertDigests(); - for (int i = 0; i < liteCerts.length; i++) { - assertThat(liteCerts[i]).isEqualTo(pkgCerts[i]); - } + assertThat(liteCerts).isEqualTo(pkgCerts); } @SuppressLint("CheckResult") @@ -184,9 +182,27 @@ public class ApkLiteParseUtilsTest { String[][] liteCerts = baseApk.getUsesStaticLibrariesCertDigests(); String[][] pkgCerts = pkg.getUsesStaticLibrariesCertDigests(); - for (int i = 0; i < liteCerts.length; i++) { - assertThat(liteCerts[i]).isEqualTo(pkgCerts[i]); - } + assertThat(liteCerts).isEqualTo(pkgCerts); + } + + @Test + public void testParseApkLite_getUsesStaticLibrary_twoCerts() + throws Exception { + File apkFile = copyApkToTmpDir(TEST_APP_USING_STATIC_LIB_TWO_CERTS); + ParseResult<ApkLite> result = ApkLiteParseUtils + .parseApkLite(ParseTypeImpl.forDefaultParsing().reset(), apkFile, 0); + assertThat(result.isError()).isFalse(); + ApkLite baseApk = result.getResult(); + + // There are two certs. + String[][] expectedCerts = {{STATIC_LIB_CERT_1, STATIC_LIB_CERT_2}}; + String[][] liteCerts = baseApk.getUsesStaticLibrariesCertDigests(); + assertThat(liteCerts).isEqualTo(expectedCerts); + + // And they are same as package parser. + AndroidPackage pkg = mPackageParser2.parsePackage(apkFile, 0, true).hideAsFinal(); + String[][] pkgCerts = pkg.getUsesStaticLibrariesCertDigests(); + assertThat(liteCerts).isEqualTo(pkgCerts); } @SuppressLint("CheckResult") diff --git a/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java b/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java index 8bebc62e93f2..1a9af6b55eed 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java @@ -21,6 +21,7 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_MAG import static com.android.internal.accessibility.common.ShortcutConstants.SERVICES_SEPARATOR; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.google.common.truth.Truth.assertThat; @@ -123,6 +124,14 @@ public class ShortcutUtilsTest { } @Test + public void getShortcutTargets_keyGestureShortcutNoService_emptyResult() { + assertThat( + ShortcutUtils.getShortcutTargetsFromSettings( + mContext, KEY_GESTURE, mDefaultUserId) + ).isEmpty(); + } + + @Test public void getShortcutTargets_softwareShortcut1Service_return1Service() { setupShortcutTargets(ONE_COMPONENT, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS); setupShortcutTargets(TWO_COMPONENTS, Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE); diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 7ced809d2a3a..541ca602a386 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -594,6 +594,9 @@ applications that come with the platform <!-- Permission required for CTS test - AdvancedProtectionManagerTest --> <permission name="android.permission.SET_ADVANCED_PROTECTION_MODE" /> <permission name="android.permission.QUERY_ADVANCED_PROTECTION_MODE" /> + <!-- Permissions required for CTS test - SettingsPreferenceServiceClientTest --> + <permission name="android.permission.READ_SYSTEM_PREFERENCES" /> + <permission name="android.permission.WRITE_SYSTEM_PREFERENCES" /> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index 8bb32568ec5a..56bb0f0d12d5 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -2119,7 +2119,7 @@ public class Paint { * @see FontVariationAxis */ public boolean setFontVariationSettings(String fontVariationSettings) { - final boolean useFontVariationStore = Flags.typefaceRedesign() + final boolean useFontVariationStore = Flags.typefaceRedesignReadonly() && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT); if (useFontVariationStore) { FontVariationAxis[] axes = diff --git a/graphics/java/android/graphics/text/PositionedGlyphs.java b/graphics/java/android/graphics/text/PositionedGlyphs.java index ed17fdefcb53..43216ba6e087 100644 --- a/graphics/java/android/graphics/text/PositionedGlyphs.java +++ b/graphics/java/android/graphics/text/PositionedGlyphs.java @@ -133,7 +133,7 @@ public final class PositionedGlyphs { @NonNull public Font getFont(@IntRange(from = 0) int index) { Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index"); - if (Flags.typefaceRedesign()) { + if (Flags.typefaceRedesignReadonly()) { return mFonts.get(nGetFontId(mLayoutPtr, index)); } return mFonts.get(index); @@ -252,7 +252,7 @@ public final class PositionedGlyphs { mXOffset = xOffset; mYOffset = yOffset; - if (Flags.typefaceRedesign()) { + if (Flags.typefaceRedesignReadonly()) { int fontCount = nGetFontCount(layoutPtr); mFonts = new ArrayList<>(fontCount); for (int i = 0; i < fontCount; ++i) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java index 4d7be39ca5a4..76eb207a31c9 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java @@ -19,6 +19,7 @@ package androidx.window.extensions.area; import static android.hardware.devicestate.DeviceState.PROPERTY_EMULATED_ONLY; import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT; import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY; +import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT; import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY; import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; @@ -104,6 +105,30 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, @GuardedBy("mLock") private int mLastReportedRearDisplayPresentationStatus; + @VisibleForTesting + static int getRdmV1Identifier(List<DeviceState> currentSupportedDeviceStates) { + for (int i = 0; i < currentSupportedDeviceStates.size(); i++) { + DeviceState state = currentSupportedDeviceStates.get(i); + if (state.hasProperty(PROPERTY_FEATURE_REAR_DISPLAY) + && !state.hasProperty(PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT)) { + return state.getIdentifier(); + } + } + return INVALID_DEVICE_STATE_IDENTIFIER; + } + + @VisibleForTesting + static int getRdmV2Identifier(List<DeviceState> currentSupportedDeviceStates) { + for (int i = 0; i < currentSupportedDeviceStates.size(); i++) { + DeviceState state = currentSupportedDeviceStates.get(i); + if (state.hasProperties(PROPERTY_FEATURE_REAR_DISPLAY, + PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT)) { + return state.getIdentifier(); + } + } + return INVALID_DEVICE_STATE_IDENTIFIER; + } + public WindowAreaComponentImpl(@NonNull Context context) { mDeviceStateManager = context.getSystemService(DeviceStateManager.class); mDisplayManager = context.getSystemService(DisplayManager.class); @@ -112,12 +137,10 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, mCurrentSupportedDeviceStates = mDeviceStateManager.getSupportedDeviceStates(); if (Flags.deviceStatePropertyMigration()) { - for (int i = 0; i < mCurrentSupportedDeviceStates.size(); i++) { - DeviceState state = mCurrentSupportedDeviceStates.get(i); - if (state.hasProperty(PROPERTY_FEATURE_REAR_DISPLAY)) { - mRearDisplayState = state.getIdentifier(); - break; - } + if (Flags.deviceStateRdmV2()) { + mRearDisplayState = getRdmV2Identifier(mCurrentSupportedDeviceStates); + } else { + mRearDisplayState = getRdmV1Identifier(mCurrentSupportedDeviceStates); } } else { mFoldedDeviceStates = context.getResources().getIntArray( diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java index ccb4ebe9199e..d677fef5c22c 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java @@ -16,8 +16,13 @@ package androidx.window.extensions.area; +import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY; +import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT; +import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; + import static org.junit.Assert.assertEquals; +import android.hardware.devicestate.DeviceState; import android.platform.test.annotations.Presubmit; import android.util.DisplayMetrics; import android.view.Surface; @@ -29,11 +34,34 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + @Presubmit @SmallTest @RunWith(AndroidJUnit4.class) public class WindowAreaComponentImplTests { + private static final DeviceState REAR_DISPLAY_STATE_V1 = new DeviceState( + new DeviceState.Configuration.Builder(1, "STATE_0") + .setSystemProperties( + Set.of(PROPERTY_FEATURE_REAR_DISPLAY)) + .build()); + private static final DeviceState REAR_DISPLAY_STATE_V2 = new DeviceState( + new DeviceState.Configuration.Builder(2, "STATE_0") + .setSystemProperties( + Set.of(PROPERTY_FEATURE_REAR_DISPLAY, + PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT)) + .build()); + // The PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT state must be present together with the + // PROPERTY_FEATURE_REAR_DISPLAY state in order to be a valid state. + private static final DeviceState INVALID_REAR_DISPLAY_STATE = new DeviceState( + new DeviceState.Configuration.Builder(2, "STATE_0") + .setSystemProperties( + Set.of(PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT)) + .build()); + private final DisplayMetrics mTestDisplayMetrics = new DisplayMetrics(); @Before @@ -93,4 +121,37 @@ public class WindowAreaComponentImplTests { Surface.ROTATION_270, Surface.ROTATION_0, mTestDisplayMetrics); assertEquals(expectedMetrics, mTestDisplayMetrics); } + + @Test + public void testRdmV1Identifier() { + final List<DeviceState> supportedStates = new ArrayList<>(); + supportedStates.add(REAR_DISPLAY_STATE_V2); + assertEquals(INVALID_DEVICE_STATE_IDENTIFIER, + WindowAreaComponentImpl.getRdmV1Identifier(supportedStates)); + + supportedStates.add(REAR_DISPLAY_STATE_V1); + assertEquals(REAR_DISPLAY_STATE_V1.getIdentifier(), + WindowAreaComponentImpl.getRdmV1Identifier(supportedStates)); + } + + @Test + public void testRdmV2Identifier_whenStateIsImproperlyConfigured() { + final List<DeviceState> supportedStates = new ArrayList<>(); + supportedStates.add(INVALID_REAR_DISPLAY_STATE); + assertEquals(INVALID_DEVICE_STATE_IDENTIFIER, + WindowAreaComponentImpl.getRdmV2Identifier(supportedStates)); + } + + @Test + public void testRdmV2Identifier_whenStateIsProperlyConfigured() { + final List<DeviceState> supportedStates = new ArrayList<>(); + + supportedStates.add(REAR_DISPLAY_STATE_V1); + assertEquals(INVALID_DEVICE_STATE_IDENTIFIER, + WindowAreaComponentImpl.getRdmV2Identifier(supportedStates)); + + supportedStates.add(REAR_DISPLAY_STATE_V2); + assertEquals(REAR_DISPLAY_STATE_V2.getIdentifier(), + WindowAreaComponentImpl.getRdmV2Identifier(supportedStates)); + } } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt index 0b515f590f98..5f42bb161204 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -475,6 +475,6 @@ class BubbleStackViewTest { override fun hideCurrentInputMethod() {} - override fun updateBubbleBarLocation(location: BubbleBarLocation) {} + override fun updateBubbleBarLocation(location: BubbleBarLocation, source: Int) {} } } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt index 0d742cc6e382..6ac36a3319c9 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt @@ -375,7 +375,7 @@ class BubbleBarExpandedViewTest { override fun hideCurrentInputMethod() { } - override fun updateBubbleBarLocation(location: BubbleBarLocation) { + override fun updateBubbleBarLocation(location: BubbleBarLocation, source: Int) { } } } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt index 00d9a931cebe..0044593ad228 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt @@ -351,7 +351,7 @@ class BubbleBarLayerViewTest { override fun hideCurrentInputMethod() {} - override fun updateBubbleBarLocation(location: BubbleBarLocation) {} + override fun updateBubbleBarLocation(location: BubbleBarLocation, source: Int) {} } } diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index f90e165ffc74..a18a2510f0f7 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -168,7 +168,7 @@ </LinearLayout> <LinearLayout - android:id="@+id/open_in_browser_pill" + android:id="@+id/open_in_app_or_browser_pill" android:layout_width="match_parent" android:layout_height="@dimen/desktop_mode_handle_menu_open_in_browser_pill_height" android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin" @@ -178,7 +178,7 @@ android:background="@drawable/desktop_mode_decor_handle_menu_background"> <Button - android:id="@+id/open_in_browser_button" + android:id="@+id/open_in_app_or_browser_button" android:layout_weight="1" android:contentDescription="@string/open_in_browser_text" android:text="@string/open_in_browser_text" diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 8f1ef6c7e49e..012579a6d40c 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -301,6 +301,8 @@ <string name="screenshot_text">Screenshot</string> <!-- Accessibility text for the handle menu open in browser button [CHAR LIMIT=NONE] --> <string name="open_in_browser_text">Open in browser</string> + <!-- Accessibility text for the handle menu open in app button [CHAR LIMIT=NONE] --> + <string name="open_in_app_text">Open in App</string> <!-- Accessibility text for the handle menu new window button [CHAR LIMIT=NONE] --> <string name="new_window_text">New Window</string> <!-- Accessibility text for the handle menu new window button [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt index 191875d38daf..84a22b873aaf 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt @@ -15,6 +15,7 @@ */ package com.android.wm.shell.shared.bubbles +import android.annotation.IntDef import android.os.Parcel import android.os.Parcelable @@ -60,4 +61,36 @@ enum class BubbleBarLocation : Parcelable { override fun newArray(size: Int) = arrayOfNulls<BubbleBarLocation>(size) } } + + /** Define set of constants that allow to determine why location changed. */ + @IntDef( + UpdateSource.DRAG_BAR, + UpdateSource.DRAG_BUBBLE, + UpdateSource.DRAG_EXP_VIEW, + UpdateSource.A11Y_ACTION_BAR, + UpdateSource.A11Y_ACTION_BUBBLE, + UpdateSource.A11Y_ACTION_EXP_VIEW, + ) + @Retention(AnnotationRetention.SOURCE) + annotation class UpdateSource { + companion object { + /** Location changed from dragging the bar */ + const val DRAG_BAR = 1 + + /** Location changed from dragging the bubble */ + const val DRAG_BUBBLE = 2 + + /** Location changed from dragging the expanded view */ + const val DRAG_EXP_VIEW = 3 + + /** Location changed via a11y action on the bar */ + const val A11Y_ACTION_BAR = 4 + + /** Location changed via a11y action on the bubble */ + const val A11Y_ACTION_BUBBLE = 5 + + /** Location changed via a11y action on the expanded view */ + const val A11Y_ACTION_EXP_VIEW = 6 + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt index 65132fe89063..7243ea36b137 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt @@ -20,7 +20,9 @@ package com.android.wm.shell.apptoweb import android.content.Context import android.content.Intent +import android.content.Intent.ACTION_VIEW import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.content.Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER import android.content.pm.PackageManager import android.content.pm.verify.domain.DomainVerificationManager import android.content.pm.verify.domain.DomainVerificationUserState @@ -31,7 +33,7 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup private const val TAG = "AppToWebUtils" private val GenericBrowserIntent = Intent() - .setAction(Intent.ACTION_VIEW) + .setAction(ACTION_VIEW) .addCategory(Intent.CATEGORY_BROWSABLE) .setData(Uri.parse("http:")) @@ -67,6 +69,20 @@ fun getBrowserIntent(uri: Uri, packageManager: PackageManager): Intent? { } /** + * Returns intent if there is a non-browser application available to handle the uri. Otherwise, + * returns null. + */ +fun getAppIntent(uri: Uri, packageManager: PackageManager): Intent? { + val intent = Intent(ACTION_VIEW, uri).apply { + flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_REQUIRE_NON_BROWSER + } + // If there is no application available to handle intent, return null + val component = intent.resolveActivity(packageManager) ?: return null + intent.setComponent(component) + return intent +} + +/** * Returns the [DomainVerificationUserState] of the user associated with the given * [DomainVerificationManager] and the given package. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index c92a2786e49b..ce7a97703f44 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -1122,7 +1122,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont final BackMotionEvent backFinish = mCurrentTracker .createProgressEvent(); dispatchOnBackProgressed(mActiveCallback, backFinish); - if (!mBackGestureStarted) { + if (mCurrentTracker.isFinished()) { // if the down -> up gesture happened before animation // start, we have to trigger the uninterruptible transition // to finish the back animation. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 14f8cc74bfc5..0fd98ed7eaf1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -740,8 +740,10 @@ public class BubbleController implements ConfigurationChangeListener, /** * Update bubble bar location and trigger and update to listeners */ - public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { + public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation, + @BubbleBarLocation.UpdateSource int source) { if (canShowAsBubbleBar()) { + BubbleBarLocation previousLocation = mBubblePositioner.getBubbleBarLocation(); mBubblePositioner.setBubbleBarLocation(bubbleBarLocation); if (mLayerView != null && !mLayerView.isExpandedViewDragged()) { mLayerView.updateExpandedView(); @@ -749,13 +751,47 @@ public class BubbleController implements ConfigurationChangeListener, BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation; mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate); + + logBubbleBarLocationIfChanged(bubbleBarLocation, previousLocation, source); + } + } + + private void logBubbleBarLocationIfChanged(BubbleBarLocation location, + BubbleBarLocation previous, + @BubbleBarLocation.UpdateSource int source) { + if (mLayerView == null) { + return; + } + boolean isRtl = mLayerView.isLayoutRtl(); + boolean wasLeft = previous.isOnLeft(isRtl); + boolean onLeft = location.isOnLeft(isRtl); + if (wasLeft == onLeft) { + // No changes, skip logging + return; + } + switch (source) { + case BubbleBarLocation.UpdateSource.DRAG_BAR: + case BubbleBarLocation.UpdateSource.A11Y_ACTION_BAR: + mLogger.log(onLeft ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_BAR + : BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_DRAG_BAR); + break; + case BubbleBarLocation.UpdateSource.DRAG_BUBBLE: + case BubbleBarLocation.UpdateSource.A11Y_ACTION_BUBBLE: + mLogger.log(onLeft ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_BUBBLE + : BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_DRAG_BUBBLE); + break; + case BubbleBarLocation.UpdateSource.DRAG_EXP_VIEW: + case BubbleBarLocation.UpdateSource.A11Y_ACTION_EXP_VIEW: + // TODO(b/349845968): move logging from BubbleBarLayerView to here + break; } } /** * Animate bubble bar to the given location. The location change is transient. It does not * update the state of the bubble bar. - * To update bubble bar pinned location, use {@link #setBubbleBarLocation(BubbleBarLocation)}. + * To update bubble bar pinned location, use + * {@link #setBubbleBarLocation(BubbleBarLocation, int)}. */ public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { if (canShowAsBubbleBar()) { @@ -2568,9 +2604,10 @@ public class BubbleController implements ConfigurationChangeListener, } @Override - public void setBubbleBarLocation(BubbleBarLocation location) { + public void setBubbleBarLocation(BubbleBarLocation location, + @BubbleBarLocation.UpdateSource int source) { mMainExecutor.execute(() -> - mController.setBubbleBarLocation(location)); + mController.setBubbleBarLocation(location, source)); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt index ec4854b47aff..6423eed59165 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt @@ -32,7 +32,10 @@ interface BubbleExpandedViewManager { fun isStackExpanded(): Boolean fun isShowingAsBubbleBar(): Boolean fun hideCurrentInputMethod() - fun updateBubbleBarLocation(location: BubbleBarLocation) + fun updateBubbleBarLocation( + location: BubbleBarLocation, + @BubbleBarLocation.UpdateSource source: Int, + ) companion object { /** @@ -82,8 +85,11 @@ interface BubbleExpandedViewManager { controller.hideCurrentInputMethod() } - override fun updateBubbleBarLocation(location: BubbleBarLocation) { - controller.bubbleBarLocation = location + override fun updateBubbleBarLocation( + location: BubbleBarLocation, + @BubbleBarLocation.UpdateSource source: Int, + ) { + controller.setBubbleBarLocation(location, source) } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl index 1855b938f48e..9c2d35431554 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -44,7 +44,7 @@ interface IBubbles { oneway void showUserEducation(in int positionX, in int positionY) = 8; - oneway void setBubbleBarLocation(in BubbleBarLocation location) = 9; + oneway void setBubbleBarLocation(in BubbleBarLocation location, in int source) = 9; oneway void updateBubbleBarTopOnScreen(in int topOnScreen) = 10; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 272dfecb0bf9..3764bcd42ac6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -637,11 +637,13 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView return true; } if (action == R.id.action_move_bubble_bar_left) { - mManager.updateBubbleBarLocation(BubbleBarLocation.LEFT); + mManager.updateBubbleBarLocation(BubbleBarLocation.LEFT, + BubbleBarLocation.UpdateSource.A11Y_ACTION_EXP_VIEW); return true; } if (action == R.id.action_move_bubble_bar_right) { - mManager.updateBubbleBarLocation(BubbleBarLocation.RIGHT); + mManager.updateBubbleBarLocation(BubbleBarLocation.RIGHT, + BubbleBarLocation.UpdateSource.A11Y_ACTION_EXP_VIEW); return true; } return false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 1f77abe54c8d..0c05e3c5115c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -441,7 +441,8 @@ public class BubbleBarLayerView extends FrameLayout @Override public void onRelease(@NonNull BubbleBarLocation location) { - mBubbleController.setBubbleBarLocation(location); + mBubbleController.setBubbleBarLocation(location, + BubbleBarLocation.UpdateSource.DRAG_EXP_VIEW); if (location != mInitialLocation) { BubbleLogger.Event event = location.isOnLeft(isLayoutRtl()) ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_EXP_VIEW diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 44fce81fa059..601cf70b93ed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -67,6 +67,7 @@ import com.android.wm.shell.dagger.pip.PipModule; import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; +import com.android.wm.shell.desktopmode.DesktopBackNavigationTransitionHandler; import com.android.wm.shell.desktopmode.DesktopDisplayEventHandler; import com.android.wm.shell.desktopmode.DesktopImmersiveController; import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler; @@ -915,6 +916,16 @@ public abstract class WMShellModule { @WMSingleton @Provides + static DesktopBackNavigationTransitionHandler provideDesktopBackNavigationTransitionHandler( + @ShellMainThread ShellExecutor mainExecutor, + @ShellAnimationThread ShellExecutor animExecutor, + DisplayController displayController) { + return new DesktopBackNavigationTransitionHandler(mainExecutor, animExecutor, + displayController); + } + + @WMSingleton + @Provides static DesktopModeDragAndDropTransitionHandler provideDesktopModeDragAndDropTransitionHandler( Transitions transitions) { return new DesktopModeDragAndDropTransitionHandler(transitions); @@ -964,6 +975,7 @@ public abstract class WMShellModule { Optional<DesktopRepository> desktopRepository, Transitions transitions, ShellTaskOrganizer shellTaskOrganizer, + Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler, ShellInit shellInit) { return desktopRepository.flatMap( repository -> @@ -973,6 +985,7 @@ public abstract class WMShellModule { repository, transitions, shellTaskOrganizer, + desktopMixedTransitionHandler.get(), shellInit))); } @@ -985,6 +998,7 @@ public abstract class WMShellModule { FreeformTaskTransitionHandler freeformTaskTransitionHandler, CloseDesktopTaskTransitionHandler closeDesktopTaskTransitionHandler, Optional<DesktopImmersiveController> desktopImmersiveController, + DesktopBackNavigationTransitionHandler desktopBackNavigationTransitionHandler, InteractionJankMonitor interactionJankMonitor, @ShellMainThread Handler handler, ShellInit shellInit, @@ -1001,6 +1015,7 @@ public abstract class WMShellModule { freeformTaskTransitionHandler, closeDesktopTaskTransitionHandler, desktopImmersiveController.get(), + desktopBackNavigationTransitionHandler, interactionJankMonitor, handler, shellInit, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandler.kt new file mode 100644 index 000000000000..83b0f8413a28 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandler.kt @@ -0,0 +1,97 @@ +/* + * 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.animation.Animator +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.os.IBinder +import android.util.DisplayMetrics +import android.view.SurfaceControl.Transaction +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import android.window.WindowContainerTransaction +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.TransitionUtil +import com.android.wm.shell.shared.animation.MinimizeAnimator.create +import com.android.wm.shell.transition.Transitions + +/** + * The [Transitions.TransitionHandler] that handles transitions for tasks that are closing or going + * to back as part of back navigation. This handler is used only for animating transitions. + */ +class DesktopBackNavigationTransitionHandler( + private val mainExecutor: ShellExecutor, + private val animExecutor: ShellExecutor, + private val displayController: DisplayController, +) : Transitions.TransitionHandler { + + /** Shouldn't handle anything */ + override fun handleRequest( + transition: IBinder, + request: TransitionRequestInfo, + ): WindowContainerTransaction? = null + + /** Animates a transition with minimizing tasks */ + override fun startAnimation( + transition: IBinder, + info: TransitionInfo, + startTransaction: Transaction, + finishTransaction: Transaction, + finishCallback: Transitions.TransitionFinishCallback, + ): Boolean { + if (!TransitionUtil.isClosingType(info.type)) return false + + val animations = mutableListOf<Animator>() + val onAnimFinish: (Animator) -> Unit = { animator -> + mainExecutor.execute { + // Animation completed + animations.remove(animator) + if (animations.isEmpty()) { + // All animations completed, finish the transition + finishCallback.onTransitionFinished(/* wct= */ null) + } + } + } + + animations += + info.changes + .filter { + it.mode == info.type && + it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM + } + .mapNotNull { createMinimizeAnimation(it, finishTransaction, onAnimFinish) } + if (animations.isEmpty()) return false + animExecutor.execute { animations.forEach(Animator::start) } + return true + } + + private fun createMinimizeAnimation( + change: TransitionInfo.Change, + finishTransaction: Transaction, + onAnimFinish: (Animator) -> Unit + ): Animator? { + val t = Transaction() + val sc = change.leash + finishTransaction.hide(sc) + val displayMetrics: DisplayMetrics? = + change.taskInfo?.let { + displayController.getDisplayContext(it.displayId)?.getResources()?.displayMetrics + } + return displayMetrics?.let { create(it, change, t, onAnimFinish) } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt index 01c680dc8325..2001f9743094 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt @@ -52,6 +52,7 @@ class DesktopMixedTransitionHandler( private val freeformTaskTransitionHandler: FreeformTaskTransitionHandler, private val closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler, private val desktopImmersiveController: DesktopImmersiveController, + private val desktopBackNavigationTransitionHandler: DesktopBackNavigationTransitionHandler, private val interactionJankMonitor: InteractionJankMonitor, @ShellMainThread private val handler: Handler, shellInit: ShellInit, @@ -161,6 +162,14 @@ class DesktopMixedTransitionHandler( finishTransaction, finishCallback ) + is PendingMixedTransition.Minimize -> animateMinimizeTransition( + pending, + transition, + info, + startTransaction, + finishTransaction, + finishCallback + ) } } @@ -272,6 +281,42 @@ class DesktopMixedTransitionHandler( ) } + private fun animateMinimizeTransition( + pending: PendingMixedTransition.Minimize, + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: TransitionFinishCallback, + ): Boolean { + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue) return false + + val minimizeChange = findDesktopTaskChange(info, pending.minimizingTask) + if (minimizeChange == null) { + logW("Should have minimizing desktop task") + return false + } + if (pending.isLastTask) { + // Dispatch close desktop task animation to the default transition handlers. + return dispatchToLeftoverHandler( + transition, + info, + startTransaction, + finishTransaction, + finishCallback + ) + } + + // Animate minimizing desktop task transition with [DesktopBackNavigationTransitionHandler]. + return desktopBackNavigationTransitionHandler.startAnimation( + transition, + info, + startTransaction, + finishTransaction, + finishCallback, + ) + } + override fun onTransitionConsumed( transition: IBinder, aborted: Boolean, @@ -400,6 +445,14 @@ class DesktopMixedTransitionHandler( val minimizingTask: Int?, val exitingImmersiveTask: Int?, ) : PendingMixedTransition() + + /** A task is minimizing. This should be used for task going to back and some closing cases + * with back navigation. */ + data class Minimize( + override val transition: IBinder, + val minimizingTask: Int, + val isLastTask: Boolean, + ) : PendingMixedTransition() } private fun logV(msg: String, vararg arguments: Any?) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt index bed484c7a532..39586e39fdd4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt @@ -594,6 +594,10 @@ class DesktopModeEventLogger { FrameworkStatsLog .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__MAXIMIZE_MENU_RESIZE_TRIGGER ), + DRAG_TO_TOP_RESIZE_TRIGGER( + FrameworkStatsLog + .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__DRAG_TO_TOP_RESIZE_TRIGGER + ), } /** 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 927fd88fb4ff..223038f84418 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 @@ -871,11 +871,10 @@ class DesktopTasksController( return } - // TODO(b/375356605): Introduce a new ResizeTrigger for drag-to-top. desktopModeEventLogger.logTaskResizingStarted( - ResizeTrigger.UNKNOWN_RESIZE_TRIGGER, motionEvent, taskInfo, displayController + ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, motionEvent, taskInfo, displayController ) - toggleDesktopTaskSize(taskInfo, ResizeTrigger.UNKNOWN_RESIZE_TRIGGER, motionEvent) + toggleDesktopTaskSize(taskInfo, ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, motionEvent) } private fun getMaximizeBounds(taskInfo: RunningTaskInfo, stableBounds: Rect): Rect { @@ -1291,7 +1290,11 @@ 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, + transition, + 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 @@ -1621,7 +1624,7 @@ 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, transition: IBinder, requestType: Int): WindowContainerTransaction? { logV("handleTaskClosing") if (!isDesktopModeShowing(task.displayId)) return null @@ -1637,8 +1640,15 @@ class DesktopTasksController( if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { taskRepository.addClosingTask(task.displayId, task.taskId) desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId) + } else if (requestType == TRANSIT_CLOSE) { + // Handle closing tasks, tasks that are going to back are handled in + // [DesktopTasksTransitionObserver]. + desktopMixedTransitionHandler.addPendingMixedTransition( + DesktopMixedTransitionHandler.PendingMixedTransition.Minimize( + transition, task.taskId, taskRepository.getVisibleTaskCount(task.displayId) == 1 + ) + ) } - taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( doesAnyTaskRequireTaskbarRounding( task.displayId, 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 d1534da9a078..c39c715e685c 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 @@ -46,6 +46,7 @@ class DesktopTasksTransitionObserver( private val desktopRepository: DesktopRepository, private val transitions: Transitions, private val shellTaskOrganizer: ShellTaskOrganizer, + private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler, shellInit: ShellInit ) : Transitions.TransitionObserver { @@ -71,7 +72,7 @@ class DesktopTasksTransitionObserver( // TODO: b/332682201 Update repository state updateWallpaperToken(info) if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { - handleBackNavigation(info) + handleBackNavigation(transition, info) removeTaskIfNeeded(info) } removeWallpaperOnLastTaskClosingIfNeeded(transition, info) @@ -95,7 +96,7 @@ class DesktopTasksTransitionObserver( } } - private fun handleBackNavigation(info: TransitionInfo) { + private fun handleBackNavigation(transition: IBinder, 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) { @@ -105,10 +106,14 @@ class DesktopTasksTransitionObserver( continue } - if (desktopRepository.getVisibleTaskCount(taskInfo.displayId) > 0 && + val visibleTaskCount = desktopRepository.getVisibleTaskCount(taskInfo.displayId) + if (visibleTaskCount > 0 && change.mode == TRANSIT_TO_BACK && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) { desktopRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId) + desktopMixedTransitionHandler.addPendingMixedTransition( + DesktopMixedTransitionHandler.PendingMixedTransition.Minimize( + transition, taskInfo.taskId, visibleTaskCount == 1)) } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index 4aeecbec7dfb..5276d9d6a4df 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -27,6 +27,7 @@ import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.annotation.IntDef; import android.annotation.NonNull; +import android.app.AppCompatTaskInfo; import android.app.TaskInfo; import android.content.Context; import android.content.pm.ActivityInfo; @@ -176,12 +177,12 @@ public class PipAnimationController { public PipTransitionAnimator getAnimator(TaskInfo taskInfo, SurfaceControl leash, Rect baseBounds, Rect startBounds, Rect endBounds, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, float startingAngle, - @Surface.Rotation int rotationDelta) { + @Surface.Rotation int rotationDelta, boolean alwaysAnimateTaskBounds) { if (mCurrentAnimator == null) { mCurrentAnimator = setupPipTransitionAnimator( PipTransitionAnimator.ofBounds(taskInfo, leash, startBounds, startBounds, endBounds, sourceHintRect, direction, 0 /* startingAngle */, - rotationDelta)); + rotationDelta, alwaysAnimateTaskBounds)); } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA && mCurrentAnimator.isRunning()) { // If we are still animating the fade into pip, then just move the surface and ensure @@ -197,7 +198,8 @@ public class PipAnimationController { mCurrentAnimator.cancel(); mCurrentAnimator = setupPipTransitionAnimator( PipTransitionAnimator.ofBounds(taskInfo, leash, baseBounds, startBounds, - endBounds, sourceHintRect, direction, startingAngle, rotationDelta)); + endBounds, sourceHintRect, direction, startingAngle, rotationDelta, + alwaysAnimateTaskBounds)); } return mCurrentAnimator; } @@ -585,28 +587,32 @@ public class PipAnimationController { static PipTransitionAnimator<Rect> ofBounds(TaskInfo taskInfo, SurfaceControl leash, Rect baseValue, Rect startValue, Rect endValue, Rect sourceRectHint, @PipAnimationController.TransitionDirection int direction, float startingAngle, - @Surface.Rotation int rotationDelta) { + @Surface.Rotation int rotationDelta, boolean alwaysAnimateTaskBounds) { final boolean isOutPipDirection = isOutPipDirection(direction); final boolean isInPipDirection = isInPipDirection(direction); // Just for simplicity we'll interpolate between the source rect hint insets and empty // insets to calculate the window crop final Rect initialSourceValue; final Rect mainWindowFrame = taskInfo.topActivityMainWindowFrame; - final boolean hasNonMatchFrame = mainWindowFrame != null; + final AppCompatTaskInfo compatInfo = taskInfo.appCompatTaskInfo; + final boolean isSizeCompatOrLetterboxed = compatInfo.isTopActivityInSizeCompat() + || compatInfo.isTopActivityLetterboxed(); + // For the animation to swipe PIP to home or restore a PIP task from home, we don't + // override to the main window frame since we should animate the whole task. + final boolean shouldUseMainWindowFrame = mainWindowFrame != null + && !alwaysAnimateTaskBounds && !isSizeCompatOrLetterboxed; final boolean changeOrientation = rotationDelta == ROTATION_90 || rotationDelta == ROTATION_270; final Rect baseBounds = new Rect(baseValue); final Rect startBounds = new Rect(startValue); final Rect endBounds = new Rect(endValue); if (isOutPipDirection) { - // TODO(b/356277166): handle rotation change with activity that provides main window - // frame. - if (hasNonMatchFrame && !changeOrientation) { + if (shouldUseMainWindowFrame && !changeOrientation) { endBounds.set(mainWindowFrame); } initialSourceValue = new Rect(endBounds); } else if (isInPipDirection) { - if (hasNonMatchFrame) { + if (shouldUseMainWindowFrame) { baseBounds.set(mainWindowFrame); if (startValue.equals(baseValue)) { // If the start value is at initial state as in PIP animation, also override @@ -635,9 +641,19 @@ public class PipAnimationController { if (changeOrientation) { lastEndRect = new Rect(endBounds); rotatedEndRect = new Rect(endBounds); - // Rotate the end bounds according to the rotation delta because the display will - // be rotated to the same orientation. - rotateBounds(rotatedEndRect, initialSourceValue, rotationDelta); + // TODO(b/375977163): polish the animation to restoring the PIP task back from + // swipe-pip-to-home. Ideally we should send the transitionInfo after reparenting + // the PIP activity back to the original task. + if (shouldUseMainWindowFrame) { + // If we should animate the main window frame, set it to the rotatedRect + // instead. The end bounds reported by transitionInfo is the bounds before + // rotation, while main window frame is calculated after the rotation. + rotatedEndRect.set(mainWindowFrame); + } else { + // Rotate the end bounds according to the rotation delta because the display + // will be rotated to the same orientation. + rotateBounds(rotatedEndRect, initialSourceValue, rotationDelta); + } // Use the rect that has the same orientation as the hint rect. initialContainerRect = isOutPipDirection ? rotatedEndRect : initialSourceValue; } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 86c826a680f6..30f1948efa2d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -1880,9 +1880,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, ? mPipBoundsState.getBounds() : currentBounds; final boolean existingAnimatorRunning = mPipAnimationController.getCurrentAnimator() != null && mPipAnimationController.getCurrentAnimator().isRunning(); + // For resize animation, we always animate the whole PIP task bounds. final PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseBounds, currentBounds, destinationBounds, - sourceHintRect, direction, startingAngle, rotationDelta); + sourceHintRect, direction, startingAngle, rotationDelta, + true /* alwaysAnimateTaskBounds */); animator.setTransitionDirection(direction) .setPipTransactionHandler(mPipTransactionHandler) .setDuration(durationMs); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 8220ea5ea575..f7aed4401247 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -891,7 +891,8 @@ public class PipTransition extends PipTransitionController { final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getAnimator(taskInfo, pipChange.getLeash(), startBounds, startBounds, endBounds, null, TRANSITION_DIRECTION_LEAVE_PIP, - 0 /* startingAngle */, pipRotateDelta); + 0 /* startingAngle */, pipRotateDelta, + false /* alwaysAnimateTaskBounds */); animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(mEnterExitAnimationDuration) @@ -906,7 +907,7 @@ public class PipTransition extends PipTransitionController { final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getAnimator(taskInfo, leash, baseBounds, startBounds, endBounds, sourceHintRect, TRANSITION_DIRECTION_LEAVE_PIP, - 0 /* startingAngle */, rotationDelta); + 0 /* startingAngle */, rotationDelta, false /* alwaysAnimateTaskBounds */); animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP) .setDuration(mEnterExitAnimationDuration); if (startTransaction != null) { @@ -1102,8 +1103,6 @@ public class PipTransition extends PipTransitionController { if (taskInfo.pictureInPictureParams != null && taskInfo.pictureInPictureParams.isAutoEnterEnabled() && mPipTransitionState.getInSwipePipToHomeTransition()) { - // TODO(b/356277166): add support to swipe PIP to home with - // non-match parent activity. handleSwipePipToHomeTransition(startTransaction, finishTransaction, leash, sourceHintRect, destinationBounds, taskInfo); return; @@ -1125,7 +1124,7 @@ public class PipTransition extends PipTransitionController { if (enterAnimationType == ANIM_TYPE_BOUNDS) { animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds, currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, - 0 /* startingAngle */, rotationDelta); + 0 /* startingAngle */, rotationDelta, false /* alwaysAnimateTaskBounds */); if (sourceHintRect == null) { // We use content overlay when there is no source rect hint to enter PiP use bounds // animation. We also temporarily disallow app icon overlay and use color overlay @@ -1248,10 +1247,14 @@ public class PipTransition extends PipTransitionController { // to avoid flicker. final Rect savedDisplayCutoutInsets = new Rect(pipTaskInfo.displayCutoutInsets); pipTaskInfo.displayCutoutInsets.setEmpty(); + // Always use the task bounds even if the PIP activity doesn't match parent because the app + // and the whole task will move behind. We should animate the whole task bounds in this + // case. final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getAnimator(pipTaskInfo, leash, sourceBounds, sourceBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, - 0 /* startingAngle */, ROTATION_0 /* rotationDelta */) + 0 /* startingAngle */, ROTATION_0 /* rotationDelta */, + true /* alwaysAnimateTaskBounds */) .setPipTransactionHandler(mTransactionConsumer) .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP); // The start state is the end state for swipe-auto-pip. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 40065b9287a6..9016c45e8197 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -34,6 +34,8 @@ import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION; import static com.android.wm.shell.shared.split.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS; +import static com.android.wm.shell.transition.Transitions.TRANSIT_END_RECENTS_TRANSITION; +import static com.android.wm.shell.transition.Transitions.TRANSIT_START_RECENTS_TRANSITION; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -67,6 +69,7 @@ import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.IResultReceiver; import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipUtils; @@ -216,8 +219,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, break; } } - final IBinder transition = mTransitions.startTransition(TRANSIT_TO_FRONT, wct, - mixer == null ? this : mixer); + final int transitionType = Flags.enableShellTopTaskTracking() + ? TRANSIT_START_RECENTS_TRANSITION + : TRANSIT_TO_FRONT; + final IBinder transition = mTransitions.startTransition(transitionType, + wct, mixer == null ? this : mixer); if (mixer != null) { setTransitionForMixer.accept(transition); } @@ -300,7 +306,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, "RecentsTransitionHandler.mergeAnimation: no controller found"); return; } - controller.merge(info, t, finishCallback); + controller.merge(info, t, mergeTarget, finishCallback); } @Override @@ -367,6 +373,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, private boolean mPausingSeparateHome = false; private ArrayMap<SurfaceControl, SurfaceControl> mLeashMap = null; private PictureInPictureSurfaceTransaction mPipTransaction = null; + // This is the transition that backs the entire recents transition, and the one that the + // pending finish transition below will be merged into private IBinder mTransition = null; private boolean mKeyguardLocked = false; private boolean mWillFinishToHome = false; @@ -386,6 +394,10 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, // next called. private Pair<int[], TaskSnapshot[]> mPendingPauseSnapshotsForCancel; + // Used to track a pending finish transition + private IBinder mPendingFinishTransition; + private IResultReceiver mPendingRunnerFinishCb; + RecentsController(IRecentsAnimationRunner listener) { mInstanceId = System.identityHashCode(this); mListener = listener; @@ -523,6 +535,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mInfo = null; mTransition = null; mPendingPauseSnapshotsForCancel = null; + mPipTaskId = -1; + mPipTask = null; + mPipTransaction = null; + mPendingRunnerFinishCb = null; + mPendingFinishTransition = null; mControllers.remove(this); for (int i = 0; i < mStateListeners.size(); i++) { mStateListeners.get(i).onAnimationStateChanged(false); @@ -734,6 +751,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, // the pausing apps. t.setLayer(target.leash, layer); } else if (taskInfo != null && taskInfo.topActivityType == ACTIVITY_TYPE_HOME) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + " not handling home taskId=%d", taskInfo.taskId); // do nothing } else if (TransitionUtil.isOpeningType(change.getMode())) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, @@ -872,16 +891,35 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, } } + /** + * Note: because we use a book-end transition to finish the recents transition, we must + * either always merge the incoming transition, or always cancel the recents transition + * if we don't handle the incoming transition to ensure that the end transition is queued + * before any unhandled transitions. + */ @SuppressLint("NewApi") - void merge(TransitionInfo info, SurfaceControl.Transaction t, + void merge(TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { if (mFinishCB == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: skip, no finish callback", mInstanceId); - // This was no-op'd (likely a repeated start) and we've already sent finish. + // This was no-op'd (likely a repeated start) and we've already completed finish. + return; + } + + if (Flags.enableShellTopTaskTracking() + && info.getType() == TRANSIT_END_RECENTS_TRANSITION + && mergeTarget == mTransition) { + // This is a pending finish, so merge the end transition to trigger completing the + // cleanup of the recents transition + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.merge: TRANSIT_END_RECENTS_TRANSITION", + mInstanceId); + finishCallback.onTransitionFinished(null /* wct */); return; } + if (info.getType() == TRANSIT_SLEEP) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: transit_sleep", mInstanceId); @@ -1245,7 +1283,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, return; } - if (mFinishCB == null) { + if (mFinishCB == null + || (Flags.enableShellTopTaskTracking() && mPendingFinishTransition != null)) { Slog.e(TAG, "Duplicate call to finish"); return; } @@ -1254,19 +1293,22 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, && !mWillFinishToHome && mPausingTasks != null && mState == STATE_NORMAL; - if (returningToApp && allAppsAreTranslucent(mPausingTasks)) { - mHomeTransitionObserver.notifyHomeVisibilityChanged(true); - } else if (!toHome) { - // For some transitions, we may have notified home activity that it became visible. - // We need to notify the observer that we are no longer going home. - mHomeTransitionObserver.notifyHomeVisibilityChanged(false); + if (!Flags.enableShellTopTaskTracking()) { + // This is only necessary when the recents transition is finished using a finishWCT, + // otherwise a new transition will notify the relevant observers + if (returningToApp && allAppsAreTranslucent(mPausingTasks)) { + mHomeTransitionObserver.notifyHomeVisibilityChanged(true); + } else if (!toHome) { + // For some transitions, we may have notified home activity that it became + // visible. We need to notify the observer that we are no longer going home. + mHomeTransitionObserver.notifyHomeVisibilityChanged(false); + } } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.finishInner: toHome=%b userLeave=%b " - + "willFinishToHome=%b state=%d", - mInstanceId, toHome, sendUserLeaveHint, mWillFinishToHome, mState); - final Transitions.TransitionFinishCallback finishCB = mFinishCB; - mFinishCB = null; + + "willFinishToHome=%b state=%d reason=%s", + mInstanceId, toHome, sendUserLeaveHint, mWillFinishToHome, mState, reason); final SurfaceControl.Transaction t = mFinishTransaction; final WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -1328,6 +1370,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, for (int i = 0; i < mClosingTasks.size(); ++i) { cleanUpPausingOrClosingTask(mClosingTasks.get(i), wct, t, sendUserLeaveHint); } + if (mPipTransaction != null && sendUserLeaveHint) { SurfaceControl pipLeash = null; TransitionInfo.Change pipChange = null; @@ -1379,15 +1422,50 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mTransitions.startTransition(TRANSIT_PIP, wct, null /* handler */); // We need to clear the WCT to send finishWCT=null for Recents. wct.clear(); + + if (Flags.enableShellTopTaskTracking()) { + // In this case, we've already started the PIP transition, so we can + // clean up immediately + mPendingRunnerFinishCb = runnerFinishCb; + onFinishInner(null); + return; + } } } - mPipTaskId = -1; - mPipTask = null; - mPipTransaction = null; } } + + if (Flags.enableShellTopTaskTracking()) { + if (!wct.isEmpty()) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.finishInner: " + + "Queuing TRANSIT_END_RECENTS_TRANSITION", mInstanceId); + mPendingRunnerFinishCb = runnerFinishCb; + mPendingFinishTransition = mTransitions.startTransition( + TRANSIT_END_RECENTS_TRANSITION, wct, + new PendingFinishTransitionHandler()); + } else { + // If there's no work to do, just go ahead and clean up + mPendingRunnerFinishCb = runnerFinishCb; + onFinishInner(null /* wct */); + } + } else { + mPendingRunnerFinishCb = runnerFinishCb; + onFinishInner(wct); + } + } + + /** + * Runs the actual logic to finish the recents transition. + */ + private void onFinishInner(@Nullable WindowContainerTransaction wct) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.finishInner: Completing finish", mInstanceId); + final Transitions.TransitionFinishCallback finishCb = mFinishCB; + final IResultReceiver runnerFinishCb = mPendingRunnerFinishCb; + cleanUp(); - finishCB.onTransitionFinished(wct.isEmpty() ? null : wct); + finishCb.onTransitionFinished(wct); if (runnerFinishCb != null) { try { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, @@ -1472,6 +1550,40 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, } }); } + + /** + * A temporary transition handler used with the pending finish transition, which runs the + * cleanup/finish logic once the pending transition is merged/handled. + * This is only initialized if Flags.enableShellTopTaskTracking() is enabled. + */ + private class PendingFinishTransitionHandler implements Transitions.TransitionHandler { + @Override + public boolean startAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + return false; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + return null; + } + + @Override + public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @Nullable SurfaceControl.Transaction finishTransaction) { + // Once we have merged (or not if the WCT didn't result in any changes), then we can + // run the pending finish logic + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.onTransitionConsumed: " + + "Consumed pending finish transition", mInstanceId); + onFinishInner(null /* wct */); + } + }; }; /** Utility class to track the state of a task as-seen by recents. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 19a73f3631f2..cc0e1df115c2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -55,6 +55,7 @@ import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_LAUNCHER; +import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP; @@ -1098,11 +1099,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, void setSideStagePosition(@SplitPosition int sideStagePosition, @Nullable WindowContainerTransaction wct) { + setSideStagePosition(sideStagePosition, true /* updateBounds */, wct); + } + + private void setSideStagePosition(@SplitPosition int sideStagePosition, boolean updateBounds, + @Nullable WindowContainerTransaction wct) { if (mSideStagePosition == sideStagePosition) return; mSideStagePosition = sideStagePosition; sendOnStagePositionChanged(); - if (mSideStage.mVisible) { + if (mSideStage.mVisible && updateBounds) { if (wct == null) { // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds. onLayoutSizeChanged(mSplitLayout); @@ -1193,7 +1199,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!isSplitActive()) return; final WindowContainerTransaction wct = new WindowContainerTransaction(); - setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, wct); applyExitSplitScreen(childrenToTop, wct, exitReason); } @@ -1593,13 +1598,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } if (present) { updateRecentTasksSplitPair(); - } else if (mMainStage.getChildCount() == 0 && mSideStage.getChildCount() == 0) { - mRecentTasks.ifPresent(recentTasks -> { - // remove the split pair mapping from recentTasks, and disable further updates - // to splits in the recents until we enter split again. - recentTasks.removeSplitPair(taskId); - }); - exitSplitScreen(mMainStage, EXIT_REASON_ROOT_TASK_VANISHED); } for (int i = mListeners.size() - 1; i >= 0; --i) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 29e4b5bca5cc..9fcf98b9efc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -353,7 +353,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { boolean isSeamlessDisplayChange = false; if (mode == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) { - if (info.getType() == TRANSIT_CHANGE) { + if (info.getType() == TRANSIT_CHANGE || isOnlyTranslucent) { final int anim = getRotationAnimationHint(change, info, mDisplayController); isSeamlessDisplayChange = anim == ROTATION_ANIMATION_SEAMLESS; if (!(isSeamlessDisplayChange || anim == ROTATION_ANIMATION_JUMPCUT)) { 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 1d456aed5f4d..3f191497e1ed 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 @@ -203,6 +203,12 @@ public class Transitions implements RemoteCallable<Transitions>, /** Transition type to minimize a task. */ public static final int TRANSIT_MINIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 20; + /** Transition to start the recents transition */ + public static final int TRANSIT_START_RECENTS_TRANSITION = TRANSIT_FIRST_CUSTOM + 21; + + /** Transition to end the recents transition */ + public static final int TRANSIT_END_RECENTS_TRANSITION = TRANSIT_FIRST_CUSTOM + 22; + /** Transition type for desktop mode transitions. */ public static final int TRANSIT_DESKTOP_MODE_TYPES = WindowManager.TRANSIT_FIRST_CUSTOM + 100; @@ -1875,6 +1881,8 @@ public class Transitions implements RemoteCallable<Transitions>, case TRANSIT_SPLIT_PASSTHROUGH -> "SPLIT_PASSTHROUGH"; case TRANSIT_CLEANUP_PIP_EXIT -> "CLEANUP_PIP_EXIT"; case TRANSIT_MINIMIZE -> "MINIMIZE"; + case TRANSIT_START_RECENTS_TRANSITION -> "START_RECENTS_TRANSITION"; + case TRANSIT_END_RECENTS_TRANSITION -> "END_RECENTS_TRANSITION"; default -> ""; }; return typeStr + "(FIRST_CUSTOM+" + (transitType - TRANSIT_FIRST_CUSTOM) + ")"; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 7265fb8f8027..c9f2d2e8c0e2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -26,6 +26,7 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions; +import android.annotation.NonNull; import android.app.ActivityManager.RunningTaskInfo; import android.content.ContentResolver; import android.content.Context; @@ -110,6 +111,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT } mMainExecutor.execute(() -> { mExclusionRegion.set(systemGestureExclusion); + onExclusionRegionChanged(displayId, mExclusionRegion); }); } }; @@ -163,7 +165,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT boolean isFocusedGlobally) { final WindowDecoration decor = mWindowDecorByTaskId.get(taskId); if (decor != null) { - decor.relayout(decor.mTaskInfo, isFocusedGlobally); + decor.relayout(decor.mTaskInfo, isFocusedGlobally, decor.mExclusionRegion); } } @@ -199,9 +201,9 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT if (enableDisplayFocusInShellTransitions()) { // Pass the current global focus status to avoid updates outside of a ShellTransition. - decoration.relayout(taskInfo, decoration.mHasGlobalFocus); + decoration.relayout(taskInfo, decoration.mHasGlobalFocus, decoration.mExclusionRegion); } else { - decoration.relayout(taskInfo, taskInfo.isFocused); + decoration.relayout(taskInfo, taskInfo.isFocused, decoration.mExclusionRegion); } } @@ -240,7 +242,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT } else { decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */, - mFocusTransitionObserver.hasGlobalFocus(taskInfo)); + mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion); } } @@ -254,7 +256,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */, - mFocusTransitionObserver.hasGlobalFocus(taskInfo)); + mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion); } @Override @@ -266,6 +268,15 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT decoration.close(); } + private void onExclusionRegionChanged(int displayId, @NonNull Region exclusionRegion) { + final int decorCount = mWindowDecorByTaskId.size(); + for (int i = 0; i < decorCount; i++) { + final CaptionWindowDecoration decoration = mWindowDecorByTaskId.valueAt(i); + if (decoration.mTaskInfo.displayId != displayId) continue; + decoration.onExclusionRegionChanged(exclusionRegion); + } + } + private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { return true; @@ -333,7 +344,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT windowDecoration.setTaskDragResizer(taskPositioner); windowDecoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */, - mFocusTransitionObserver.hasGlobalFocus(taskInfo)); + mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion); } private class CaptionTouchEventListener implements diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index c9546731a193..982fda0ddf36 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -36,6 +36,7 @@ import android.graphics.Color; import android.graphics.Insets; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.Region; import android.graphics.drawable.GradientDrawable; import android.os.Handler; import android.util.Size; @@ -174,7 +175,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL } @Override - void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus) { + void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus, + @NonNull Region displayExclusionRegion) { final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); // The crop and position of the task should only be set when a task is fluid resizing. In // all other cases, it is expected that the transition handler positions and crops the task @@ -186,7 +188,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL // synced with the buffer transaction (that draws the View). Both will be shown on screen // at the same, whereas applying them independently causes flickering. See b/270202228. relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */, - shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion); } @VisibleForTesting @@ -198,7 +200,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL boolean isStatusBarVisible, boolean isKeyguardVisibleAndOccluded, InsetsState displayInsetsState, - boolean hasGlobalFocus) { + boolean hasGlobalFocus, + @NonNull Region globalExclusionRegion) { relayoutParams.reset(); relayoutParams.mRunningTaskInfo = taskInfo; relayoutParams.mLayoutResId = R.layout.caption_window_decor; @@ -210,6 +213,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL relayoutParams.mSetTaskVisibilityPositionAndCrop = shouldSetTaskVisibilityPositionAndCrop; relayoutParams.mIsCaptionVisible = taskInfo.isFreeform() || (isStatusBarVisible && !isKeyguardVisibleAndOccluded); + relayoutParams.mDisplayExclusionRegion.set(globalExclusionRegion); if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { // If the app is requesting to customize the caption bar, allow input to fall @@ -236,7 +240,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL void relayout(RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, - boolean hasGlobalFocus) { + boolean hasGlobalFocus, + @NonNull Region globalExclusionRegion) { final boolean isFreeform = taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; final boolean isDragResizeable = ENABLE_WINDOWING_SCALED_RESIZING.isTrue() @@ -249,7 +254,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, - mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus); + mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus, + globalExclusionRegion); relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index f2d8a782de34..d71e61a4c4de 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -220,6 +220,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } mMainExecutor.execute(() -> { mExclusionRegion.set(systemGestureExclusion); + onExclusionRegionChanged(displayId, mExclusionRegion); }); } }; @@ -432,7 +433,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, boolean isFocusedGlobally) { final WindowDecoration decor = mWindowDecorByTaskId.get(taskId); if (decor != null) { - decor.relayout(decor.mTaskInfo, isFocusedGlobally); + decor.relayout(decor.mTaskInfo, isFocusedGlobally, decor.mExclusionRegion); } } @@ -465,15 +466,15 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, final RunningTaskInfo oldTaskInfo = decoration.mTaskInfo; if (taskInfo.displayId != oldTaskInfo.displayId - && !Flags.enableHandleInputFix()) { + && !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { removeTaskFromEventReceiver(oldTaskInfo.displayId); incrementEventReceiverTasks(taskInfo.displayId); } if (enableDisplayFocusInShellTransitions()) { // Pass the current global focus status to avoid updates outside of a ShellTransition. - decoration.relayout(taskInfo, decoration.mHasGlobalFocus); + decoration.relayout(taskInfo, decoration.mHasGlobalFocus, decoration.mExclusionRegion); } else { - decoration.relayout(taskInfo, taskInfo.isFocused); + decoration.relayout(taskInfo, taskInfo.isFocused, decoration.mExclusionRegion); } mActivityOrientationChangeHandler.ifPresent(handler -> handler.handleActivityOrientationChange(oldTaskInfo, taskInfo)); @@ -514,7 +515,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } else { decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */, - mFocusTransitionObserver.hasGlobalFocus(taskInfo)); + mFocusTransitionObserver.hasGlobalFocus(taskInfo), + mExclusionRegion); } } @@ -528,7 +530,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */, - mFocusTransitionObserver.hasGlobalFocus(taskInfo)); + mFocusTransitionObserver.hasGlobalFocus(taskInfo), + mExclusionRegion); } @Override @@ -539,7 +542,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, decoration.close(); final int displayId = taskInfo.displayId; if (mEventReceiversByDisplay.contains(displayId) - && !Flags.enableHandleInputFix()) { + && !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { removeTaskFromEventReceiver(displayId); } // Remove the decoration from the cache last because WindowDecoration#close could still @@ -548,6 +551,15 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mWindowDecorByTaskId.remove(taskInfo.taskId); } + private void onExclusionRegionChanged(int displayId, @NonNull Region exclusionRegion) { + final int decorCount = mWindowDecorByTaskId.size(); + for (int i = 0; i < decorCount; i++) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.valueAt(i); + if (decoration.mTaskInfo.displayId != displayId) continue; + decoration.onExclusionRegionChanged(exclusionRegion); + } + } + private void openHandleMenu(int taskId) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); decoration.createHandleMenu(checkNumberOfOtherInstances(decoration.mTaskInfo) @@ -750,10 +762,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, /** * Whether to pilfer the next motion event to send cancellations to the windows below. - * Useful when the caption window is spy and the gesture should be handle by the system + * Useful when the caption window is spy and the gesture should be handled by the system * instead of by the app for their custom header content. + * Should not have any effect when {@link Flags#enableAccessibleCustomHeaders()}, because + * a spy window is not used then. */ - private boolean mShouldPilferCaptionEvents; + private boolean mIsCustomHeaderGesture; + private boolean mIsResizeGesture; private boolean mIsDragging; private boolean mTouchscreenInUse; private boolean mHasLongClicked; @@ -767,7 +782,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mTaskToken = taskInfo.token; mDragPositioningCallback = dragPositioningCallback; final int touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); - final long appHandleHoldToDragDuration = Flags.enableHoldToDragAppHandle() + final long appHandleHoldToDragDuration = + DesktopModeFlags.ENABLE_HOLD_TO_DRAG_APP_HANDLE.isTrue() ? APP_HANDLE_HOLD_TO_DRAG_DURATION_MS : 0; mHandleDragDetector = new DragDetector(this, appHandleHoldToDragDuration, touchSlop); @@ -867,7 +883,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, // to offset position relative to caption as a whole. int[] viewLocation = new int[2]; v.getLocationInWindow(viewLocation); - final boolean isResizeEvent = decoration.shouldResizeListenerHandleEvent(e, + mIsResizeGesture = decoration.shouldResizeListenerHandleEvent(e, new Point(viewLocation[0], viewLocation[1])); // The caption window may be a spy window when the caption background is // transparent, which means events will fall through to the app window. Make @@ -875,21 +891,23 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, // customizable region and what the app reported as exclusion areas, because // the drag-move or other caption gestures should take priority outside those // regions. - mShouldPilferCaptionEvents = !(downInCustomizableCaptionRegion - && downInExclusionRegion && isTransparentCaption) && !isResizeEvent; + mIsCustomHeaderGesture = downInCustomizableCaptionRegion + && downInExclusionRegion && isTransparentCaption; } - if (!mShouldPilferCaptionEvents) { - // The event will be handled by a window below or pilfered by resize handler. + if (mIsCustomHeaderGesture || mIsResizeGesture) { + // The event will be handled by the custom window below or pilfered by resize + // handler. return false; } - // Otherwise pilfer so that windows below receive cancellations for this gesture, and - // continue normal handling as a caption gesture. - if (mInputManager != null) { + if (mInputManager != null + && !Flags.enableAccessibleCustomHeaders()) { + // Pilfer so that windows below receive cancellations for this gesture. mInputManager.pilferPointers(v.getViewRootImpl().getInputToken()); } if (isUpOrCancel) { // Gesture is finished, reset state. - mShouldPilferCaptionEvents = false; + mIsCustomHeaderGesture = false; + mIsResizeGesture = false; } if (isAppHandle) { return mHandleDragDetector.onMotionEvent(v, e); @@ -1234,7 +1252,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, relevantDecor.updateHoverAndPressStatus(ev); final int action = ev.getActionMasked(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { - if (!mTransitionDragActive && !Flags.enableHandleInputFix()) { + if (!mTransitionDragActive && !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { relevantDecor.closeHandleMenuIfNeeded(ev); } } @@ -1277,7 +1295,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } final boolean shouldStartTransitionDrag = relevantDecor.checkTouchEventInFocusedCaptionHandle(ev) - || Flags.enableHandleInputFix(); + || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue(); if (dragFromStatusBarAllowed && shouldStartTransitionDrag) { mTransitionDragActive = true; } @@ -1592,8 +1610,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, windowDecoration.setDragPositioningCallback(taskPositioner); windowDecoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */, - mFocusTransitionObserver.hasGlobalFocus(taskInfo)); - if (!Flags.enableHandleInputFix()) { + mFocusTransitionObserver.hasGlobalFocus(taskInfo), + mExclusionRegion); + if (!DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { incrementEventReceiverTasks(taskInfo.displayId); } } @@ -1618,6 +1637,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, pw.println(innerPrefix + "mTransitionDragActive=" + mTransitionDragActive); pw.println(innerPrefix + "mEventReceiversByDisplay=" + mEventReceiversByDisplay); pw.println(innerPrefix + "mWindowDecorByTaskId=" + mWindowDecorByTaskId); + pw.println(innerPrefix + "mExclusionRegion=" + mExclusionRegion); } private class DesktopModeOnTaskRepositionAnimationListener @@ -1754,7 +1774,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, && Flags.enableDesktopWindowingImmersiveHandleHiding()) { decor.onInsetsStateChanged(insetsState); } - if (!Flags.enableHandleInputFix()) { + if (!DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { // If status bar inset is visible, top task is not in immersive mode. // This value is only needed when the App Handle input is being handled // through the global input monitor (hence the flag check) to ignore gestures diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index d97632a9428c..cdcf14e0cbf3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -394,7 +394,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } @Override - void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus) { + void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus, + @NonNull Region displayExclusionRegion) { final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); // The visibility, crop and position of the task should only be set when a task is // fluid resizing. In all other cases, it is expected that the transition handler sets @@ -415,7 +416,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // causes flickering. See b/270202228. final boolean applyTransactionOnDraw = taskInfo.isFreeform(); relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, - hasGlobalFocus); + hasGlobalFocus, displayExclusionRegion); if (!applyTransactionOnDraw) { t.apply(); } @@ -442,18 +443,18 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin void relayout(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, - boolean hasGlobalFocus) { + boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) { Trace.beginSection("DesktopModeWindowDecoration#relayout"); if (taskInfo.isFreeform()) { // The Task is in Freeform mode -> show its header in sync since it's an integral part // of the window itself - a delayed header might cause bad UX. relayoutInSync(taskInfo, startT, finishT, applyStartTransactionOnDraw, - shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion); } else { // The Task is outside Freeform mode -> allow the handle view to be delayed since the // handle is just a small addition to the window. relayoutWithDelayedViewHost(taskInfo, startT, finishT, applyStartTransactionOnDraw, - shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion); } Trace.endSection(); } @@ -462,11 +463,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private void relayoutInSync(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, - boolean hasGlobalFocus) { + boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) { // Clear the current ViewHost runnable as we will update the ViewHost here clearCurrentViewHostRunnable(); updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, applyStartTransactionOnDraw, - shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion); if (mResult.mRootView != null) { updateViewHost(mRelayoutParams, startT, mResult); } @@ -489,7 +490,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private void relayoutWithDelayedViewHost(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, - boolean hasGlobalFocus) { + boolean hasGlobalFocus, + @NonNull Region displayExclusionRegion) { if (applyStartTransactionOnDraw) { throw new IllegalArgumentException( "We cannot both sync viewhost ondraw and delay viewhost creation."); @@ -498,7 +500,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin clearCurrentViewHostRunnable(); updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, shouldSetTaskVisibilityPositionAndCrop, - hasGlobalFocus); + hasGlobalFocus, displayExclusionRegion); if (mResult.mRootView == null) { // This means something blocks the window decor from showing, e.g. the task is hidden. // Nothing is set up in this case including the decoration surface. @@ -513,7 +515,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, - boolean hasGlobalFocus) { + boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) { Trace.beginSection("DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces"); if (Flags.enableDesktopWindowingAppToWeb()) { setCapturedLink(taskInfo.capturedLink, taskInfo.capturedLinkTimestamp); @@ -538,7 +540,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, inFullImmersive, - mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus); + mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus, + displayExclusionRegion); final WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; @@ -628,13 +631,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @Nullable private Intent getBrowserLink() { - // Do not show browser link in browser applications - final ComponentName baseActivity = mTaskInfo.baseActivity; - if (baseActivity != null && AppToWebUtils.isBrowserApp(mContext, - baseActivity.getPackageName(), mUserContext.getUserId())) { - return null; - } - final Uri browserLink; // If the captured link is available and has not expired, return the captured link. // Otherwise, return the generic link which is set to null if a generic link is unavailable. @@ -651,6 +647,18 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } + @Nullable + private Intent getAppLink() { + return mWebUri == null ? null + : AppToWebUtils.getAppIntent(mWebUri, mContext.getPackageManager()); + } + + private boolean isBrowserApp() { + final ComponentName baseActivity = mTaskInfo.baseActivity; + return baseActivity != null && AppToWebUtils.isBrowserApp(mContext, + baseActivity.getPackageName(), mUserContext.getUserId()); + } + UserHandle getUser() { return mUserContext.getUser(); } @@ -807,7 +815,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ void disposeStatusBarInputLayer() { if (!isAppHandle(mWindowDecorViewHolder) - || !Flags.enableHandleInputFix()) { + || !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { return; } asAppHandle(mWindowDecorViewHolder).disposeStatusBarInputLayer(); @@ -874,7 +882,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin boolean isKeyguardVisibleAndOccluded, boolean inFullImmersiveMode, @NonNull InsetsState displayInsetsState, - boolean hasGlobalFocus) { + boolean hasGlobalFocus, + @NonNull Region displayExclusionRegion) { final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode()); final boolean isAppHeader = captionLayoutId == R.layout.desktop_mode_app_header; @@ -885,6 +894,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode()); relayoutParams.mCaptionWidthId = getCaptionWidthId(relayoutParams.mLayoutResId); relayoutParams.mHasGlobalFocus = hasGlobalFocus; + relayoutParams.mDisplayExclusionRegion.set(displayExclusionRegion); final boolean showCaption; if (Flags.enableFullyImmersiveInDesktop()) { @@ -910,10 +920,20 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin relayoutParams.mIsInsetSource = isAppHeader && !inFullImmersiveMode; if (isAppHeader) { if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { - // If the app is requesting to customize the caption bar, allow input to fall - // through to the windows below so that the app can respond to input events on - // their custom content. - relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY; + // The app is requesting to customize the caption bar, which means input on + // customizable/exclusion regions must go to the app instead of to the system. + // This may be accomplished with spy windows or custom touchable regions: + if (Flags.enableAccessibleCustomHeaders()) { + // Set the touchable region of the caption to only the areas where input should + // be handled by the system (i.e. non custom-excluded areas). The region will + // be calculated based on occluding caption elements and exclusion areas + // reported by the app. + relayoutParams.mLimitTouchRegionToSystemAreas = true; + } else { + // Allow input to fall through to the windows below so that the app can respond + // to input events on their custom content. + relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY; + } } else { if (ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isTrue()) { // Force-consume the caption bar insets when the app tries to hide the caption. @@ -951,7 +971,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } controlsElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.END; relayoutParams.mOccludingCaptionElements.add(controlsElement); - } else if (isAppHandle && !Flags.enableHandleInputFix()) { + } else if (isAppHandle && !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { // The focused decor (fullscreen/split) does not need to handle input because input in // the App Handle is handled by the InputMonitor in DesktopModeWindowDecorViewModel. // Note: This does not apply with the above flag enabled as the status bar input layer @@ -1368,6 +1388,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin .shouldShowChangeAspectRatioButton(mTaskInfo); final boolean inDesktopImmersive = mDesktopRepository .isTaskInFullImmersiveState(mTaskInfo.taskId); + final boolean isBrowserApp = isBrowserApp(); mHandleMenu = mHandleMenuFactory.create( this, mWindowManagerWrapper, @@ -1379,7 +1400,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin supportsMultiInstance, shouldShowManageWindowsButton, shouldShowChangeAspectRatioButton, - getBrowserLink(), + isBrowserApp, + isBrowserApp ? getAppLink() : getBrowserLink(), mResult.mCaptionWidth, mResult.mCaptionHeight, mResult.mCaptionX, @@ -1560,13 +1582,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ boolean checkTouchEventInFocusedCaptionHandle(MotionEvent ev) { if (isHandleMenuActive() || !isAppHandle(mWindowDecorViewHolder) - || Flags.enableHandleInputFix()) { + || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { return false; } // The status bar input layer can only receive input in handle coordinates to begin with, // so checking coordinates is unnecessary as input is always within handle bounds. if (isAppHandle(mWindowDecorViewHolder) - && Flags.enableHandleInputFix() + && DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue() && isCaptionVisible()) { return true; } @@ -1603,7 +1625,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * @param ev the MotionEvent to compare */ void checkTouchEvent(MotionEvent ev) { - if (mResult.mRootView == null || Flags.enableHandleInputFix()) return; + if (mResult.mRootView == null || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) return; final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); final View handle = caption.findViewById(R.id.caption_handle); final boolean inHandle = !isHandleMenuActive() @@ -1616,7 +1638,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // If the whole handle menu can be touched directly, rely on FLAG_WATCH_OUTSIDE_TOUCH. // This is for the case that some of the handle menu is underneath the status bar. if (isAppHandle(mWindowDecorViewHolder) - && !Flags.enableHandleInputFix()) { + && !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { mHandleMenu.checkMotionEvent(ev); closeHandleMenuIfNeeded(ev); } @@ -1630,7 +1652,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * @param ev the MotionEvent to compare against. */ void updateHoverAndPressStatus(MotionEvent ev) { - if (mResult.mRootView == null || Flags.enableHandleInputFix()) return; + if (mResult.mRootView == null || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) return; final View handle = mResult.mRootView.findViewById(R.id.caption_handle); final boolean inHandle = !isHandleMenuActive() && checkTouchEventInFocusedCaptionHandle(ev); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index 2edc380756ac..54c247bff984 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -39,12 +39,14 @@ import android.widget.Button import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView +import android.window.DesktopModeFlags import android.window.SurfaceSyncGroup +import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import androidx.compose.ui.graphics.toArgb import androidx.core.view.isGone -import com.android.window.flags.Flags import com.android.wm.shell.R +import com.android.wm.shell.apptoweb.isBrowserApp import com.android.wm.shell.shared.split.SplitScreenConstants import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer @@ -73,7 +75,8 @@ class HandleMenu( private val shouldShowNewWindowButton: Boolean, private val shouldShowManageWindowsButton: Boolean, private val shouldShowChangeAspectRatioButton: Boolean, - private val openInBrowserIntent: Intent?, + private val isBrowserApp: Boolean, + private val openInAppOrBrowserIntent: Intent?, private val captionWidth: Int, private val captionHeight: Int, captionX: Int, @@ -83,7 +86,7 @@ class HandleMenu( private val taskInfo: RunningTaskInfo = parentDecor.mTaskInfo private val isViewAboveStatusBar: Boolean - get() = (Flags.enableHandleInputFix() && !taskInfo.isFreeform) + get() = (DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue() && !taskInfo.isFreeform) private val pillElevation: Int = loadDimensionPixelSize( R.dimen.desktop_mode_handle_menu_pill_elevation) @@ -111,7 +114,7 @@ class HandleMenu( private val globalMenuPosition: Point = Point() private val shouldShowBrowserPill: Boolean - get() = openInBrowserIntent != null + get() = openInAppOrBrowserIntent != null private val shouldShowMoreActionsPill: Boolean get() = SHOULD_SHOW_SCREENSHOT_BUTTON || shouldShowNewWindowButton || @@ -128,7 +131,7 @@ class HandleMenu( onNewWindowClickListener: () -> Unit, onManageWindowsClickListener: () -> Unit, onChangeAspectRatioClickListener: () -> Unit, - openInBrowserClickListener: (Intent) -> Unit, + openInAppOrBrowserClickListener: (Intent) -> Unit, onOpenByDefaultClickListener: () -> Unit, onCloseMenuClickListener: () -> Unit, onOutsideTouchListener: () -> Unit, @@ -146,7 +149,7 @@ class HandleMenu( onNewWindowClickListener = onNewWindowClickListener, onManageWindowsClickListener = onManageWindowsClickListener, onChangeAspectRatioClickListener = onChangeAspectRatioClickListener, - openInBrowserClickListener = openInBrowserClickListener, + openInAppOrBrowserClickListener = openInAppOrBrowserClickListener, onOpenByDefaultClickListener = onOpenByDefaultClickListener, onCloseMenuClickListener = onCloseMenuClickListener, onOutsideTouchListener = onOutsideTouchListener, @@ -167,7 +170,7 @@ class HandleMenu( onNewWindowClickListener: () -> Unit, onManageWindowsClickListener: () -> Unit, onChangeAspectRatioClickListener: () -> Unit, - openInBrowserClickListener: (Intent) -> Unit, + openInAppOrBrowserClickListener: (Intent) -> Unit, onOpenByDefaultClickListener: () -> Unit, onCloseMenuClickListener: () -> Unit, onOutsideTouchListener: () -> Unit, @@ -181,7 +184,8 @@ class HandleMenu( shouldShowBrowserPill = shouldShowBrowserPill, shouldShowNewWindowButton = shouldShowNewWindowButton, shouldShowManageWindowsButton = shouldShowManageWindowsButton, - shouldShowChangeAspectRatioButton = shouldShowChangeAspectRatioButton + shouldShowChangeAspectRatioButton = shouldShowChangeAspectRatioButton, + isBrowserApp = isBrowserApp ).apply { bind(taskInfo, appIconBitmap, appName, shouldShowMoreActionsPill) this.onToDesktopClickListener = onToDesktopClickListener @@ -190,8 +194,8 @@ class HandleMenu( this.onNewWindowClickListener = onNewWindowClickListener this.onManageWindowsClickListener = onManageWindowsClickListener this.onChangeAspectRatioClickListener = onChangeAspectRatioClickListener - this.onOpenInBrowserClickListener = { - openInBrowserClickListener.invoke(openInBrowserIntent!!) + this.onOpenInAppOrBrowserClickListener = { + openInAppOrBrowserClickListener.invoke(openInAppOrBrowserIntent!!) } this.onOpenByDefaultClickListener = onOpenByDefaultClickListener this.onCloseMenuClickListener = onCloseMenuClickListener @@ -201,7 +205,8 @@ class HandleMenu( val x = handleMenuPosition.x.toInt() val y = handleMenuPosition.y.toInt() handleMenuViewContainer = - if ((!taskInfo.isFreeform && Flags.enableHandleInputFix()) || forceShowSystemBars) { + if ((!taskInfo.isFreeform && DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) + || forceShowSystemBars) { AdditionalSystemViewContainer( windowManagerWrapper = windowManagerWrapper, taskId = taskInfo.taskId, @@ -237,7 +242,7 @@ class HandleMenu( menuX = marginMenuStart menuY = captionY + marginMenuTop } else { - if (Flags.enableHandleInputFix()) { + if (DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { // In a focused decor, we use global coordinates for handle menu. Therefore we // need to account for other factors like split stage and menu/handle width to // center the menu. @@ -435,14 +440,15 @@ class HandleMenu( /** The view within the Handle Menu, with options to change the windowing mode and more. */ @SuppressLint("ClickableViewAccessibility") class HandleMenuView( - context: Context, + private val context: Context, menuWidth: Int, captionHeight: Int, private val shouldShowWindowingPill: Boolean, private val shouldShowBrowserPill: Boolean, private val shouldShowNewWindowButton: Boolean, private val shouldShowManageWindowsButton: Boolean, - private val shouldShowChangeAspectRatioButton: Boolean + private val shouldShowChangeAspectRatioButton: Boolean, + private val isBrowserApp: Boolean ) { val rootView = LayoutInflater.from(context) .inflate(R.layout.desktop_mode_window_decor_handle_menu, null /* root */) as View @@ -472,11 +478,12 @@ class HandleMenu( private val changeAspectRatioBtn = moreActionsPill .requireViewById<Button>(R.id.change_aspect_ratio_button) - // Open in Browser Pill. - private val openInBrowserPill = rootView.requireViewById<View>(R.id.open_in_browser_pill) - private val browserBtn = openInBrowserPill.requireViewById<Button>( - R.id.open_in_browser_button) - private val openByDefaultBtn = openInBrowserPill.requireViewById<ImageButton>( + // Open in Browser/App Pill. + private val openInAppOrBrowserPill = rootView.requireViewById<View>( + R.id.open_in_app_or_browser_pill) + private val openInAppOrBrowserBtn = openInAppOrBrowserPill.requireViewById<Button>( + R.id.open_in_app_or_browser_button) + private val openByDefaultBtn = openInAppOrBrowserPill.requireViewById<ImageButton>( R.id.open_by_default_button) private val decorThemeUtil = DecorThemeUtil(context) private val animator = HandleMenuAnimator(rootView, menuWidth, captionHeight.toFloat()) @@ -490,7 +497,7 @@ class HandleMenu( var onNewWindowClickListener: (() -> Unit)? = null var onManageWindowsClickListener: (() -> Unit)? = null var onChangeAspectRatioClickListener: (() -> Unit)? = null - var onOpenInBrowserClickListener: (() -> Unit)? = null + var onOpenInAppOrBrowserClickListener: (() -> Unit)? = null var onOpenByDefaultClickListener: (() -> Unit)? = null var onCloseMenuClickListener: (() -> Unit)? = null var onOutsideTouchListener: (() -> Unit)? = null @@ -499,7 +506,7 @@ class HandleMenu( fullscreenBtn.setOnClickListener { onToFullscreenClickListener?.invoke() } splitscreenBtn.setOnClickListener { onToSplitScreenClickListener?.invoke() } desktopBtn.setOnClickListener { onToDesktopClickListener?.invoke() } - browserBtn.setOnClickListener { onOpenInBrowserClickListener?.invoke() } + openInAppOrBrowserBtn.setOnClickListener { onOpenInAppOrBrowserClickListener?.invoke() } openByDefaultBtn.setOnClickListener { onOpenByDefaultClickListener?.invoke() } @@ -535,10 +542,10 @@ class HandleMenu( if (shouldShowMoreActionsPill) { bindMoreActionsPill(style) } - bindOpenInBrowserPill(style) + bindOpenInAppOrBrowserPill(style) } - /** Animates the menu opening. */ + /** Animates the menu openInAppOrBrowserg. */ fun animateOpenMenu() { if (taskInfo.isFullscreen || taskInfo.isMultiWindow) { animator.animateCaptionHandleExpandToOpen() @@ -660,13 +667,20 @@ class HandleMenu( } } - private fun bindOpenInBrowserPill(style: MenuStyle) { - openInBrowserPill.apply { + private fun bindOpenInAppOrBrowserPill(style: MenuStyle) { + openInAppOrBrowserPill.apply { isGone = !shouldShowBrowserPill background.setTint(style.backgroundColor) } - browserBtn.apply { + val btnText = if (isBrowserApp) { + getString(R.string.open_in_app_text) + } else { + getString(R.string.open_in_browser_text) + } + openInAppOrBrowserBtn.apply { + text = btnText + contentDescription = btnText setTextColor(style.textColor) compoundDrawableTintList = ColorStateList.valueOf(style.textColor) } @@ -674,6 +688,8 @@ class HandleMenu( openByDefaultBtn.imageTintList = ColorStateList.valueOf(style.textColor) } + private fun getString(@StringRes resId: Int): String = context.resources.getString(resId) + private data class MenuStyle( @ColorInt val backgroundColor: Int, @ColorInt val textColor: Int, @@ -708,7 +724,8 @@ interface HandleMenuFactory { shouldShowNewWindowButton: Boolean, shouldShowManageWindowsButton: Boolean, shouldShowChangeAspectRatioButton: Boolean, - openInBrowserIntent: Intent?, + isBrowserApp: Boolean, + openInAppOrBrowserIntent: Intent?, captionWidth: Int, captionHeight: Int, captionX: Int, @@ -729,7 +746,8 @@ object DefaultHandleMenuFactory : HandleMenuFactory { shouldShowNewWindowButton: Boolean, shouldShowManageWindowsButton: Boolean, shouldShowChangeAspectRatioButton: Boolean, - openInBrowserIntent: Intent?, + isBrowserApp: Boolean, + openInAppOrBrowserIntent: Intent?, captionWidth: Int, captionHeight: Int, captionX: Int, @@ -746,7 +764,8 @@ object DefaultHandleMenuFactory : HandleMenuFactory { shouldShowNewWindowButton, shouldShowManageWindowsButton, shouldShowChangeAspectRatioButton, - openInBrowserIntent, + isBrowserApp, + openInAppOrBrowserIntent, captionWidth, captionHeight, captionX, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt index 0c475f12f53b..470e5a1d88b4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt @@ -74,7 +74,8 @@ class HandleMenuAnimator( private val appInfoPill: ViewGroup = handleMenu.requireViewById(R.id.app_info_pill) private val windowingPill: ViewGroup = handleMenu.requireViewById(R.id.windowing_pill) private val moreActionsPill: ViewGroup = handleMenu.requireViewById(R.id.more_actions_pill) - private val openInBrowserPill: ViewGroup = handleMenu.requireViewById(R.id.open_in_browser_pill) + private val openInAppOrBrowserPill: ViewGroup = + handleMenu.requireViewById(R.id.open_in_app_or_browser_pill) /** Animates the opening of the handle menu. */ fun animateOpen() { @@ -83,7 +84,7 @@ class HandleMenuAnimator( animateAppInfoPillOpen() animateWindowingPillOpen() animateMoreActionsPillOpen() - animateOpenInBrowserPill() + animateOpenInAppOrBrowserPill() runAnimations { appInfoPill.post { appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent( @@ -103,7 +104,7 @@ class HandleMenuAnimator( animateAppInfoPillOpen() animateWindowingPillOpen() animateMoreActionsPillOpen() - animateOpenInBrowserPill() + animateOpenInAppOrBrowserPill() runAnimations { appInfoPill.post { appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent( @@ -124,7 +125,7 @@ class HandleMenuAnimator( animateAppInfoPillFadeOut() windowingPillClose() moreActionsPillClose() - openInBrowserPillClose() + openInAppOrBrowserPillClose() runAnimations(after) } @@ -141,7 +142,7 @@ class HandleMenuAnimator( animateAppInfoPillFadeOut() windowingPillClose() moreActionsPillClose() - openInBrowserPillClose() + openInAppOrBrowserPillClose() runAnimations(after) } @@ -154,7 +155,7 @@ class HandleMenuAnimator( appInfoPill.children.forEach { it.alpha = 0f } windowingPill.alpha = 0f moreActionsPill.alpha = 0f - openInBrowserPill.alpha = 0f + openInAppOrBrowserPill.alpha = 0f // Setup pivots. handleMenu.pivotX = menuWidth / 2f @@ -166,8 +167,8 @@ class HandleMenuAnimator( moreActionsPill.pivotX = menuWidth / 2f moreActionsPill.pivotY = appInfoPill.measuredHeight.toFloat() - openInBrowserPill.pivotX = menuWidth / 2f - openInBrowserPill.pivotY = appInfoPill.measuredHeight.toFloat() + openInAppOrBrowserPill.pivotX = menuWidth / 2f + openInAppOrBrowserPill.pivotY = appInfoPill.measuredHeight.toFloat() } private fun animateAppInfoPillOpen() { @@ -297,36 +298,36 @@ class HandleMenuAnimator( } } - private fun animateOpenInBrowserPill() { + private fun animateOpenInAppOrBrowserPill() { // Open in Browser X & Y Scaling Animation animators += - ObjectAnimator.ofFloat(openInBrowserPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply { startDelay = BODY_SCALE_OPEN_DELAY duration = BODY_SCALE_OPEN_DURATION } animators += - ObjectAnimator.ofFloat(openInBrowserPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply { startDelay = BODY_SCALE_OPEN_DELAY duration = BODY_SCALE_OPEN_DURATION } // Open in Browser Opacity Animation animators += - ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 1f).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 1f).apply { startDelay = BODY_ALPHA_OPEN_DELAY duration = BODY_ALPHA_OPEN_DURATION } // Open in Browser Elevation Animation animators += - ObjectAnimator.ofFloat(openInBrowserPill, TRANSLATION_Z, 1f).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, TRANSLATION_Z, 1f).apply { startDelay = ELEVATION_OPEN_DELAY duration = BODY_ELEVATION_OPEN_DURATION } // Open in Browser Button Opacity Animation - val button = openInBrowserPill.requireViewById<Button>(R.id.open_in_browser_button) + val button = openInAppOrBrowserPill.requireViewById<Button>(R.id.open_in_app_or_browser_button) animators += ObjectAnimator.ofFloat(button, ALPHA, 1f).apply { startDelay = BODY_ALPHA_OPEN_DELAY @@ -438,33 +439,33 @@ class HandleMenuAnimator( } } - private fun openInBrowserPillClose() { + private fun openInAppOrBrowserPillClose() { // Open in Browser X & Y Scaling Animation animators += - ObjectAnimator.ofFloat(openInBrowserPill, SCALE_X, HALF_INITIAL_SCALE).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_X, HALF_INITIAL_SCALE).apply { duration = BODY_CLOSE_DURATION } animators += - ObjectAnimator.ofFloat(openInBrowserPill, SCALE_Y, HALF_INITIAL_SCALE).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_Y, HALF_INITIAL_SCALE).apply { duration = BODY_CLOSE_DURATION } // Open in Browser Opacity Animation animators += - ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 0f).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 0f).apply { duration = BODY_CLOSE_DURATION } animators += - ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 0f).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 0f).apply { duration = BODY_CLOSE_DURATION } // Upward Open in Browser y-translation Animation val yStart: Float = -captionHeight / 2 animators += - ObjectAnimator.ofFloat(openInBrowserPill, TRANSLATION_Y, yStart).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, TRANSLATION_Y, yStart).apply { duration = BODY_CLOSE_DURATION } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt index cf82bb4f9919..8bc56e0807a8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt @@ -16,13 +16,12 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager.RunningTaskInfo -import com.android.window.flags.Flags -import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer - import android.content.Context import android.util.AttributeSet import android.view.MotionEvent import android.widget.ImageButton +import android.window.DesktopModeFlags +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer /** * A custom [ImageButton] for buttons inside handle menu that intentionally doesn't handle hovers. @@ -39,7 +38,7 @@ class HandleMenuImageButton( lateinit var taskInfo: RunningTaskInfo override fun onHoverEvent(motionEvent: MotionEvent): Boolean { - if (Flags.enableHandleInputFix() || taskInfo.isFreeform) { + if (DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue() || taskInfo.isFreeform) { return super.onHoverEvent(motionEvent) } else { return false diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index b016c755e323..a3c75bf33cde 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -127,7 +127,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } mDisplayController.removeDisplayWindowListener(this); - relayout(mTaskInfo, mHasGlobalFocus); + relayout(mTaskInfo, mHasGlobalFocus, mExclusionRegion); } }; @@ -143,7 +143,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> SurfaceControl mDecorationContainerSurface; SurfaceControl mCaptionContainerSurface; - private WindowlessWindowManager mCaptionWindowManager; + private CaptionWindowlessWindowManager mCaptionWindowManager; private SurfaceControlViewHost mViewHost; private Configuration mWindowDecorConfig; TaskDragResizer mTaskDragResizer; @@ -152,6 +152,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> boolean mIsStatusBarVisible; boolean mIsKeyguardVisibleAndOccluded; boolean mHasGlobalFocus; + final Region mExclusionRegion = Region.obtain(); /** The most recent set of insets applied to this window decoration. */ private WindowDecorationInsets mWindowDecorationInsets; @@ -218,7 +219,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> * constructor. * @param hasGlobalFocus Whether the task is focused */ - abstract void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus); + abstract void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus, + @NonNull Region displayExclusionRegion); /** * Used by the {@link DragPositioningCallback} associated with the implementing class to @@ -244,6 +246,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mTaskInfo = params.mRunningTaskInfo; } mHasGlobalFocus = params.mHasGlobalFocus; + mExclusionRegion.set(params.mDisplayExclusionRegion); final int oldLayoutResId = mLayoutResId; mLayoutResId = params.mLayoutResId; @@ -402,7 +405,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> final int elementWidthPx = resources.getDimensionPixelSize(element.mWidthResId); boundingRects[i] = - calculateBoundingRect(element, elementWidthPx, captionInsetsRect); + calculateBoundingRectLocal(element, elementWidthPx, captionInsetsRect); // Subtract the regions used by the caption elements, the rest is // customizable. if (params.hasInputFeatureSpy()) { @@ -477,9 +480,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> if (mCaptionWindowManager == null) { // Put caption under a container surface because ViewRootImpl sets the destination frame // of windowless window layers and BLASTBufferQueue#update() doesn't support offset. - mCaptionWindowManager = new WindowlessWindowManager( - mTaskInfo.getConfiguration(), mCaptionContainerSurface, - null /* hostInputToken */); + mCaptionWindowManager = new CaptionWindowlessWindowManager( + mTaskInfo.getConfiguration(), mCaptionContainerSurface); } mCaptionWindowManager.setConfiguration(mTaskInfo.getConfiguration()); final WindowManager.LayoutParams lp = @@ -492,6 +494,14 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> lp.setTitle("Caption of Task=" + mTaskInfo.taskId); lp.setTrustedOverlay(); lp.inputFeatures = params.mInputFeatures; + final Rect localCaptionBounds = new Rect( + outResult.mCaptionX, + outResult.mCaptionY, + outResult.mCaptionX + outResult.mCaptionWidth, + outResult.mCaptionY + outResult.mCaptionHeight); + final Region touchableRegion = params.mLimitTouchRegionToSystemAreas + ? calculateLimitedTouchableRegion(params, localCaptionBounds) + : null; if (mViewHost == null) { Trace.beginSection("CaptionViewHostLayout-new"); mViewHost = mSurfaceControlViewHostFactory.create(mDecorWindowContext, mDisplay, @@ -503,6 +513,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction); } outResult.mRootView.setPadding(0, params.mCaptionTopPadding, 0, 0); + if (params.mLimitTouchRegionToSystemAreas) { + mCaptionWindowManager.setTouchRegion(mViewHost, touchableRegion); + } mViewHost.setView(outResult.mRootView, lp); Trace.endSection(); } else { @@ -514,13 +527,71 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction); } outResult.mRootView.setPadding(0, params.mCaptionTopPadding, 0, 0); + if (params.mLimitTouchRegionToSystemAreas) { + mCaptionWindowManager.setTouchRegion(mViewHost, touchableRegion); + } mViewHost.relayout(lp); Trace.endSection(); } + if (touchableRegion != null) { + touchableRegion.recycle(); + } Trace.endSection(); // CaptionViewHostLayout } - private Rect calculateBoundingRect(@NonNull OccludingCaptionElement element, + @NonNull + private Region calculateLimitedTouchableRegion( + RelayoutParams params, + @NonNull Rect localCaptionBounds) { + // Make caption bounds relative to display to align with exclusion region. + final Point positionInParent = params.mRunningTaskInfo.positionInParent; + final Rect captionBoundsInDisplay = new Rect(localCaptionBounds); + captionBoundsInDisplay.offsetTo(positionInParent.x, positionInParent.y); + + final Region boundingRects = calculateBoundingRectsRegion(params, captionBoundsInDisplay); + + final Region customizedRegion = Region.obtain(); + customizedRegion.set(captionBoundsInDisplay); + customizedRegion.op(boundingRects, Region.Op.DIFFERENCE); + customizedRegion.op(params.mDisplayExclusionRegion, Region.Op.INTERSECT); + + final Region touchableRegion = Region.obtain(); + touchableRegion.set(captionBoundsInDisplay); + touchableRegion.op(customizedRegion, Region.Op.DIFFERENCE); + // Return resulting region back to window coordinates. + touchableRegion.translate(-positionInParent.x, -positionInParent.y); + + boundingRects.recycle(); + customizedRegion.recycle(); + return touchableRegion; + } + + @NonNull + private Region calculateBoundingRectsRegion( + @NonNull RelayoutParams params, + @NonNull Rect captionBoundsInDisplay) { + final int numOfElements = params.mOccludingCaptionElements.size(); + final Region region = Region.obtain(); + if (numOfElements == 0) { + // The entire caption is a bounding rect. + region.set(captionBoundsInDisplay); + return region; + } + final Resources resources = mDecorWindowContext.getResources(); + for (int i = 0; i < numOfElements; i++) { + final OccludingCaptionElement element = params.mOccludingCaptionElements.get(i); + final int elementWidthPx = resources.getDimensionPixelSize(element.mWidthResId); + final Rect boundingRect = calculateBoundingRectLocal(element, elementWidthPx, + captionBoundsInDisplay); + // Bounding rect is initially calculated relative to the caption, so offset it to make + // it relative to the display. + boundingRect.offset(captionBoundsInDisplay.left, captionBoundsInDisplay.top); + region.union(boundingRect); + } + return region; + } + + private Rect calculateBoundingRectLocal(@NonNull OccludingCaptionElement element, int elementWidthPx, @NonNull Rect captionRect) { switch (element.mAlignment) { case START -> { @@ -539,7 +610,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mIsKeyguardVisibleAndOccluded = visible && occluded; final boolean changed = prevVisAndOccluded != mIsKeyguardVisibleAndOccluded; if (changed) { - relayout(mTaskInfo, mHasGlobalFocus); + relayout(mTaskInfo, mHasGlobalFocus, mExclusionRegion); } } @@ -549,10 +620,14 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> final boolean changed = prevStatusBarVisibility != mIsStatusBarVisible; if (changed) { - relayout(mTaskInfo, mHasGlobalFocus); + relayout(mTaskInfo, mHasGlobalFocus, mExclusionRegion); } } + void onExclusionRegionChanged(@NonNull Region exclusionRegion) { + relayout(mTaskInfo, mHasGlobalFocus, exclusionRegion); + } + /** * Update caption visibility state and views. */ @@ -751,9 +826,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> int mCaptionHeightId; int mCaptionWidthId; final List<OccludingCaptionElement> mOccludingCaptionElements = new ArrayList<>(); + boolean mLimitTouchRegionToSystemAreas; int mInputFeatures; boolean mIsInsetSource = true; @InsetsSource.Flags int mInsetSourceFlags; + final Region mDisplayExclusionRegion = Region.obtain(); int mShadowRadiusId; int mCornerRadius; @@ -772,9 +849,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mCaptionHeightId = Resources.ID_NULL; mCaptionWidthId = Resources.ID_NULL; mOccludingCaptionElements.clear(); + mLimitTouchRegionToSystemAreas = false; mInputFeatures = 0; mIsInsetSource = true; mInsetSourceFlags = 0; + mDisplayExclusionRegion.setEmpty(); mShadowRadiusId = Resources.ID_NULL; mCornerRadius = 0; @@ -830,6 +909,19 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } } + private static class CaptionWindowlessWindowManager extends WindowlessWindowManager { + CaptionWindowlessWindowManager( + @NonNull Configuration configuration, + @NonNull SurfaceControl rootSurface) { + super(configuration, rootSurface, /* hostInputToken= */ null); + } + + /** Set the view host's touchable region. */ + void setTouchRegion(@NonNull SurfaceControlViewHost viewHost, @NonNull Region region) { + setTouchRegion(viewHost.getWindowToken().asBinder(), region); + } + } + @VisibleForTesting public interface SurfaceControlViewHostFactory { default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt index b5700ffb046b..503ad92d4d71 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt @@ -34,15 +34,14 @@ import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.ImageButton +import android.window.DesktopModeFlags import androidx.core.view.ViewCompat import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat import com.android.internal.policy.SystemBarUtils -import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.shared.animation.Interpolators import com.android.wm.shell.windowdecor.WindowManagerWrapper import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer -import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder.Data /** * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split). @@ -141,7 +140,7 @@ internal class AppHandleViewHolder( private fun createStatusBarInputLayer(handlePosition: Point, handleWidth: Int, handleHeight: Int) { - if (!Flags.enableHandleInputFix()) return + if (!DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) return statusBarInputLayer = AdditionalSystemViewContainer(context, windowManagerWrapper, taskInfo.taskId, handlePosition.x, handlePosition.y, handleWidth, handleHeight, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt index 4fe66f3357a3..4cddf31321d6 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt @@ -23,8 +23,9 @@ import android.tools.flicker.assertors.assertions.AppLayerIncreasesInSize import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart -import android.tools.flicker.assertors.assertions.AppWindowBecomesVisible import android.tools.flicker.assertors.assertions.AppWindowAlignsWithOnlyOneDisplayCornerAtEnd +import android.tools.flicker.assertors.assertions.AppWindowBecomesInvisible +import android.tools.flicker.assertors.assertions.AppWindowBecomesVisible import android.tools.flicker.assertors.assertions.AppWindowCoversLeftHalfScreenAtEnd import android.tools.flicker.assertors.assertions.AppWindowCoversRightHalfScreenAtEnd import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd @@ -44,6 +45,7 @@ import android.tools.flicker.assertors.assertions.LauncherWindowReplacesAppAsTop import android.tools.flicker.config.AssertionTemplates import android.tools.flicker.config.FlickerConfigEntry import android.tools.flicker.config.ScenarioId +import android.tools.flicker.config.common.Components.LAUNCHER import android.tools.flicker.config.desktopmode.Components.DESKTOP_MODE_APP import android.tools.flicker.config.desktopmode.Components.DESKTOP_WALLPAPER import android.tools.flicker.config.desktopmode.Components.NON_RESIZABLE_APP @@ -365,5 +367,57 @@ class DesktopModeFlickerScenarios { AppWindowAlignsWithOnlyOneDisplayCornerAtEnd(DESKTOP_MODE_APP) ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) + + val MINIMIZE_APP = + FlickerConfigEntry( + scenarioId = ScenarioId("MINIMIZE_APP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions + .filter { it.type == TransitionType.MINIMIZE } + .sortedByDescending { it.id } + .drop(1) + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppWindowOnTopAtStart(DESKTOP_MODE_APP), + AppWindowBecomesInvisible(DESKTOP_MODE_APP), + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }) + ) + + val MINIMIZE_LAST_APP = + FlickerConfigEntry( + scenarioId = ScenarioId("MINIMIZE_LAST_APP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + val lastTransition = + transitions + .filter { it.type == TransitionType.MINIMIZE } + .maxByOrNull { it.id }!! + return listOf(lastTransition) + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppWindowOnTopAtStart(DESKTOP_MODE_APP), + AppWindowBecomesInvisible(DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(LAUNCHER), + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }) + ) } } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsLandscape.kt new file mode 100644 index 000000000000..58582b02c212 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsLandscape.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.Rotation.ROTATION_90 +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_APP +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_LAST_APP +import com.android.wm.shell.scenarios.MinimizeAppWindows +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Minimize app windows by pressing the minimize button. + * + * Assert that the app windows gets hidden. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MinimizeAppsLandscape : MinimizeAppWindows(rotation = ROTATION_90) { + @ExpectedScenarios(["MINIMIZE_APP", "MINIMIZE_LAST_APP"]) + @Test + override fun minimizeAllAppWindows() = super.minimizeAllAppWindows() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(MINIMIZE_APP) + .use(MINIMIZE_LAST_APP) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsPortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsPortrait.kt new file mode 100644 index 000000000000..7970426a6ee8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsPortrait.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.wm.shell.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_APP +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_LAST_APP +import com.android.wm.shell.scenarios.MinimizeAppWindows +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Minimize app windows by pressing the minimize button. + * + * Assert that the app windows gets hidden. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MinimizeAppsPortrait : MinimizeAppWindows() { + @ExpectedScenarios(["MINIMIZE_APP", "MINIMIZE_LAST_APP"]) + @Test + override fun minimizeAllAppWindows() = super.minimizeAllAppWindows() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(MINIMIZE_APP) + .use(MINIMIZE_LAST_APP) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt index 824c4482c1e6..f442fdb31592 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.scenarios import android.tools.NavBar import android.tools.Rotation +import com.android.internal.R import com.android.window.flags.Flags import com.android.wm.shell.Utils import org.junit.After @@ -40,6 +41,9 @@ constructor( @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + // Skip the test when the drag-to-maximize is enabled on this device. + Assume.assumeFalse(Flags.enableDragToMaximize() && + instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode)) tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) testApp.enterDesktopWithDrag(wmHelper, device) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt new file mode 100644 index 000000000000..6df8d6fd7717 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt @@ -0,0 +1,173 @@ +/* + * 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.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WindowingMode +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.SurfaceControl +import android.view.WindowManager +import android.view.WindowManager.TRANSIT_CLOSE +import android.window.TransitionInfo +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.ShellExecutor +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@SmallTest +@RunWithLooper +@RunWith(AndroidTestingRunner::class) +class DesktopBackNavigationTransitionHandlerTest : ShellTestCase() { + + private val testExecutor = mock<ShellExecutor>() + private val closingTaskLeash = mock<SurfaceControl>() + private val displayController = mock<DisplayController>() + + private lateinit var handler: DesktopBackNavigationTransitionHandler + + @Before + fun setUp() { + handler = + DesktopBackNavigationTransitionHandler( + testExecutor, + testExecutor, + displayController + ) + whenever(displayController.getDisplayContext(any())).thenReturn(mContext) + } + + @Test + fun handleRequest_returnsNull() { + assertNull(handler.handleRequest(mock(), mock())) + } + + @Test + fun startAnimation_openTransition_returnsFalse() { + val animates = + handler.startAnimation( + transition = mock(), + info = + createTransitionInfo( + type = WindowManager.TRANSIT_OPEN, + task = createTask(WINDOWING_MODE_FREEFORM) + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertFalse("Should not animate open transition", animates) + } + + @Test + fun startAnimation_toBackTransitionFullscreenTask_returnsFalse() { + val animates = + handler.startAnimation( + transition = mock(), + info = createTransitionInfo(task = createTask(WINDOWING_MODE_FULLSCREEN)), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertFalse("Should not animate fullscreen task to back transition", animates) + } + + @Test + fun startAnimation_toBackTransitionOpeningFreeformTask_returnsFalse() { + val animates = + handler.startAnimation( + transition = mock(), + info = + createTransitionInfo( + changeMode = WindowManager.TRANSIT_OPEN, + task = createTask(WINDOWING_MODE_FREEFORM) + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertFalse("Should not animate opening freeform task to back transition", animates) + } + + @Test + fun startAnimation_toBackTransitionToBackFreeformTask_returnsTrue() { + val animates = + handler.startAnimation( + transition = mock(), + info = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertTrue("Should animate going to back freeform task close transition", animates) + } + + @Test + fun startAnimation_closeTransitionClosingFreeformTask_returnsTrue() { + val animates = + handler.startAnimation( + transition = mock(), + info = createTransitionInfo( + type = TRANSIT_CLOSE, + changeMode = TRANSIT_CLOSE, + task = createTask(WINDOWING_MODE_FREEFORM) + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertTrue("Should animate going to back freeform task close transition", animates) + } + private fun createTransitionInfo( + type: Int = WindowManager.TRANSIT_TO_BACK, + changeMode: Int = WindowManager.TRANSIT_TO_BACK, + task: RunningTaskInfo + ): TransitionInfo = + TransitionInfo(type, 0 /* flags */).apply { + addChange( + TransitionInfo.Change(mock(), closingTaskLeash).apply { + mode = changeMode + parent = null + taskInfo = task + } + ) + } + + private fun createTask(@WindowingMode windowingMode: Int): RunningTaskInfo = + TestRunningTaskInfoBuilder() + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(windowingMode) + .build() +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt index b06c2dad4ffc..f21f26443748 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt @@ -32,6 +32,7 @@ import android.testing.TestableLooper.RunWithLooper import android.view.SurfaceControl import android.view.WindowManager import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TransitionType import android.window.TransitionInfo import android.window.WindowContainerTransaction @@ -77,16 +78,28 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { @JvmField @Rule val setFlagsRule = SetFlagsRule() - @Mock lateinit var transitions: Transitions - @Mock lateinit var desktopRepository: DesktopRepository - @Mock lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler - @Mock lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler - @Mock lateinit var desktopImmersiveController: DesktopImmersiveController - @Mock lateinit var interactionJankMonitor: InteractionJankMonitor - @Mock lateinit var mockHandler: Handler - @Mock lateinit var closingTaskLeash: SurfaceControl - @Mock lateinit var shellInit: ShellInit - @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + @Mock + lateinit var transitions: Transitions + @Mock + lateinit var desktopRepository: DesktopRepository + @Mock + lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler + @Mock + lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler + @Mock + lateinit var desktopBackNavigationTransitionHandler: DesktopBackNavigationTransitionHandler + @Mock + lateinit var desktopImmersiveController: DesktopImmersiveController + @Mock + lateinit var interactionJankMonitor: InteractionJankMonitor + @Mock + lateinit var mockHandler: Handler + @Mock + lateinit var closingTaskLeash: SurfaceControl + @Mock + lateinit var shellInit: ShellInit + @Mock + lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer private lateinit var mixedHandler: DesktopMixedTransitionHandler @@ -100,6 +113,7 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { freeformTaskTransitionHandler, closeDesktopTaskTransitionHandler, desktopImmersiveController, + desktopBackNavigationTransitionHandler, interactionJankMonitor, mockHandler, shellInit, @@ -595,6 +609,87 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { assertThat(mixedHandler.pendingMixedTransitions).isEmpty() } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun startAnimation_withMinimizingDesktopTask_callsBackNavigationHandler() { + val minimizingTask = createTask(WINDOWING_MODE_FREEFORM) + val transition = Binder() + whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2) + whenever( + desktopBackNavigationTransitionHandler.startAnimation(any(), any(), any(), any(), any()) + ) + .thenReturn(true) + mixedHandler.addPendingMixedTransition( + PendingMixedTransition.Minimize( + transition = transition, + minimizingTask = minimizingTask.taskId, + isLastTask = false, + ) + ) + + val minimizingTaskChange = createChange(minimizingTask) + val started = mixedHandler.startAnimation( + transition = transition, + info = + createTransitionInfo( + TRANSIT_TO_BACK, + listOf(minimizingTaskChange) + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertTrue("Should delegate animation to back navigation transition handler", started) + verify(desktopBackNavigationTransitionHandler) + .startAnimation( + eq(transition), + argThat { info -> info.changes.contains(minimizingTaskChange) }, + any(), any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun startAnimation_withMinimizingLastDesktopTask_dispatchesTransition() { + val minimizingTask = createTask(WINDOWING_MODE_FREEFORM) + val transition = Binder() + whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2) + whenever( + desktopBackNavigationTransitionHandler.startAnimation(any(), any(), any(), any(), any()) + ) + .thenReturn(true) + mixedHandler.addPendingMixedTransition( + PendingMixedTransition.Minimize( + transition = transition, + minimizingTask = minimizingTask.taskId, + isLastTask = true, + ) + ) + + val minimizingTaskChange = createChange(minimizingTask) + mixedHandler.startAnimation( + transition = transition, + info = + createTransitionInfo( + TRANSIT_TO_BACK, + listOf(minimizingTaskChange) + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + verify(transitions) + .dispatchTransition( + eq(transition), + argThat { info -> info.changes.contains(minimizingTaskChange) }, + any(), + any(), + any(), + eq(mixedHandler) + ) + } + private fun createTransitionInfo( type: Int = WindowManager.TRANSIT_CLOSE, changeMode: Int = WindowManager.TRANSIT_CLOSE, 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 315a46fcbd7b..ad266ead774e 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 @@ -3038,6 +3038,21 @@ class DesktopTasksControllerTest : ShellTestCase() { // Assert bounds set to stable bounds val wct = getLatestToggleResizeDesktopTaskWct() assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS) + // Assert event is properly logged + verify(desktopModeEventLogger, times(1)).logTaskResizingStarted( + ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, + motionEvent, + task, + displayController + ) + verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( + ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, + motionEvent, + task, + STABLE_BOUNDS.height(), + STABLE_BOUNDS.width(), + displayController + ) } @Test @@ -3082,6 +3097,13 @@ class DesktopTasksControllerTest : ShellTestCase() { eq(STABLE_BOUNDS), anyOrNull(), ) + // Assert no event is logged + verify(desktopModeEventLogger, never()).logTaskResizingStarted( + any(), any(), any(), any(), any() + ) + verify(desktopModeEventLogger, never()).logTaskResizingEnded( + any(), any(), any(), any(), any(), any(), any() + ) } @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 index 737439ce3cfe..7f1c1db3207a 100644 --- 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 @@ -76,6 +76,7 @@ class DesktopTasksTransitionObserverTest { private val context = mock<Context>() private val shellTaskOrganizer = mock<ShellTaskOrganizer>() private val taskRepository = mock<DesktopRepository>() + private val mixedHandler = mock<DesktopMixedTransitionHandler>() private lateinit var transitionObserver: DesktopTasksTransitionObserver private lateinit var shellInit: ShellInit @@ -87,7 +88,7 @@ class DesktopTasksTransitionObserverTest { transitionObserver = DesktopTasksTransitionObserver( - context, taskRepository, transitions, shellTaskOrganizer, shellInit + context, taskRepository, transitions, shellTaskOrganizer, mixedHandler, shellInit ) } @@ -106,6 +107,7 @@ class DesktopTasksTransitionObserverTest { ) verify(taskRepository).minimizeTask(task.displayId, task.taskId) + verify(mixedHandler).addPendingMixedTransition(any()) } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java index 72950a8dc139..6d37ed766aef 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java @@ -28,8 +28,11 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import android.app.AppCompatTaskInfo; import android.app.TaskInfo; import android.graphics.Rect; import android.testing.AndroidTestingRunner; @@ -75,6 +78,7 @@ public class PipAnimationControllerTest extends ShellTestCase { .setContainerLayer() .setName("FakeLeash") .build(); + mTaskInfo.appCompatTaskInfo = mock(AppCompatTaskInfo.class); } @Test @@ -93,7 +97,8 @@ public class PipAnimationControllerTest extends ShellTestCase { final Rect endValue1 = new Rect(100, 100, 200, 200); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null, - TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); assertEquals("Expect ANIM_TYPE_BOUNDS animation", animator.getAnimationType(), PipAnimationController.ANIM_TYPE_BOUNDS); @@ -107,14 +112,16 @@ public class PipAnimationControllerTest extends ShellTestCase { final Rect endValue2 = new Rect(200, 200, 300, 300); final PipAnimationController.PipTransitionAnimator oldAnimator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null, - TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); oldAnimator.setSurfaceControlTransactionFactory( MockSurfaceControlHelper::createMockSurfaceControlTransaction); oldAnimator.start(); final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue2, null, - TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); assertEquals("getAnimator with same type returns same animator", oldAnimator, newAnimator); @@ -145,7 +152,8 @@ public class PipAnimationControllerTest extends ShellTestCase { // Fullscreen to PiP. PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, null, startBounds, endBounds, null, - TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_90); + TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_90, + false /* alwaysAnimateTaskBounds */); // Apply fraction 1 to compute the end value. animator.applySurfaceControlTransaction(mLeash, tx, 1); final Rect rotatedEndBounds = new Rect(endBounds); @@ -157,7 +165,8 @@ public class PipAnimationControllerTest extends ShellTestCase { startBounds.set(0, 0, 1000, 500); endBounds.set(200, 100, 400, 500); animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, startBounds, startBounds, - endBounds, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_270); + endBounds, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_270, + false /* alwaysAnimateTaskBounds */); animator.applySurfaceControlTransaction(mLeash, tx, 1); rotatedEndBounds.set(endBounds); rotateBounds(rotatedEndBounds, startBounds, ROTATION_270); @@ -166,6 +175,37 @@ public class PipAnimationControllerTest extends ShellTestCase { } @Test + public void pipTransitionAnimator_rotatedEndValue_overrideMainWindowFrame() { + final SurfaceControl.Transaction tx = createMockSurfaceControlTransaction(); + final Rect startBounds = new Rect(200, 700, 400, 800); + final Rect endBounds = new Rect(0, 0, 500, 1000); + mTaskInfo.topActivityMainWindowFrame = new Rect(0, 250, 1000, 500); + + // Fullscreen task to PiP. + PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController + .getAnimator(mTaskInfo, mLeash, null, startBounds, endBounds, null, + TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_90, + false /* alwaysAnimateTaskBounds */); + // Apply fraction 1 to compute the end value. + animator.applySurfaceControlTransaction(mLeash, tx, 1); + + assertEquals("Expect use main window frame", mTaskInfo.topActivityMainWindowFrame, + animator.mCurrentValue); + + // PiP to fullscreen. + mTaskInfo.topActivityMainWindowFrame = new Rect(0, 250, 1000, 500); + startBounds.set(0, 0, 1000, 500); + endBounds.set(200, 100, 400, 500); + animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, startBounds, startBounds, + endBounds, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_270, + false /* alwaysAnimateTaskBounds */); + animator.applySurfaceControlTransaction(mLeash, tx, 1); + + assertEquals("Expect use main window frame", mTaskInfo.topActivityMainWindowFrame, + animator.mCurrentValue); + } + + @Test @SuppressWarnings("unchecked") public void pipTransitionAnimator_updateEndValue() { final Rect baseValue = new Rect(0, 0, 100, 100); @@ -174,7 +214,8 @@ public class PipAnimationControllerTest extends ShellTestCase { final Rect endValue2 = new Rect(200, 200, 300, 300); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null, - TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); animator.updateEndValue(endValue2); @@ -188,7 +229,8 @@ public class PipAnimationControllerTest extends ShellTestCase { final Rect endValue = new Rect(100, 100, 200, 200); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null, - TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); animator.setSurfaceControlTransactionFactory( MockSurfaceControlHelper::createMockSurfaceControlTransaction); @@ -207,4 +249,126 @@ public class PipAnimationControllerTest extends ShellTestCase { verify(mPipAnimationCallback).onPipAnimationEnd(eq(mTaskInfo), any(SurfaceControl.Transaction.class), eq(animator)); } + + @Test + public void pipTransitionAnimator_overrideMainWindowFrame() { + final Rect baseValue = new Rect(0, 0, 100, 100); + final Rect startValue = new Rect(0, 0, 100, 100); + final Rect endValue = new Rect(100, 100, 200, 200); + mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100); + PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController + .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null, + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is overridden for in-PIP transition", + mTaskInfo.topActivityMainWindowFrame, animator.getBaseValue()); + assertEquals("Expect start value is overridden for in-PIP transition", + mTaskInfo.topActivityMainWindowFrame, animator.getStartValue()); + assertEquals("Expect end value is not overridden for in-PIP transition", + endValue, animator.getEndValue()); + + animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue, + endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is not overridden for leave-PIP transition", + baseValue, animator.getBaseValue()); + assertEquals("Expect start value is not overridden for leave-PIP transition", + startValue, animator.getStartValue()); + assertEquals("Expect end value is overridden for leave-PIP transition", + mTaskInfo.topActivityMainWindowFrame, animator.getEndValue()); + } + + @Test + public void pipTransitionAnimator_animateTaskBounds() { + final Rect baseValue = new Rect(0, 0, 100, 100); + final Rect startValue = new Rect(0, 0, 100, 100); + final Rect endValue = new Rect(100, 100, 200, 200); + mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100); + PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController + .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null, + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + true /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is not overridden for in-PIP transition", + baseValue, animator.getBaseValue()); + assertEquals("Expect start value is not overridden for in-PIP transition", + startValue, animator.getStartValue()); + assertEquals("Expect end value is not overridden for in-PIP transition", + endValue, animator.getEndValue()); + + animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue, + endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0, + true /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is not overridden for leave-PIP transition", + baseValue, animator.getBaseValue()); + assertEquals("Expect start value is not overridden for leave-PIP transition", + startValue, animator.getStartValue()); + assertEquals("Expect end value is not overridden for leave-PIP transition", + endValue, animator.getEndValue()); + } + + @Test + public void pipTransitionAnimator_letterboxed_animateTaskBounds() { + final Rect baseValue = new Rect(0, 0, 100, 100); + final Rect startValue = new Rect(0, 0, 100, 100); + final Rect endValue = new Rect(100, 100, 200, 200); + mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100); + doReturn(true).when(mTaskInfo.appCompatTaskInfo).isTopActivityLetterboxed(); + PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController + .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null, + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is not overridden for in-PIP transition", + baseValue, animator.getBaseValue()); + assertEquals("Expect start value is not overridden for in-PIP transition", + startValue, animator.getStartValue()); + assertEquals("Expect end value is not overridden for in-PIP transition", + endValue, animator.getEndValue()); + + animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue, + endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is not overridden for leave-PIP transition", + baseValue, animator.getBaseValue()); + assertEquals("Expect start value is not overridden for leave-PIP transition", + startValue, animator.getStartValue()); + assertEquals("Expect end value is not overridden for leave-PIP transition", + endValue, animator.getEndValue()); + } + + @Test + public void pipTransitionAnimator_sizeCompat_animateTaskBounds() { + final Rect baseValue = new Rect(0, 0, 100, 100); + final Rect startValue = new Rect(0, 0, 100, 100); + final Rect endValue = new Rect(100, 100, 200, 200); + mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100); + doReturn(true).when(mTaskInfo.appCompatTaskInfo).isTopActivityInSizeCompat(); + PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController + .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null, + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is not overridden for in-PIP transition", + baseValue, animator.getBaseValue()); + assertEquals("Expect start value is not overridden for in-PIP transition", + startValue, animator.getStartValue()); + assertEquals("Expect end value is not overridden for in-PIP transition", + endValue, animator.getEndValue()); + + animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue, + endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is not overridden for leave-PIP transition", + baseValue, animator.getBaseValue()); + assertEquals("Expect start value is not overridden for leave-PIP transition", + startValue, animator.getStartValue()); + assertEquals("Expect end value is not overridden for leave-PIP transition", + endValue, animator.getEndValue()); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt index 5ebf5170bf86..59141ca39487 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager import android.app.WindowConfiguration import android.content.ComponentName +import android.graphics.Region import android.testing.AndroidTestingRunner import android.view.Display import android.view.InsetsState @@ -33,6 +34,9 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidTestingRunner::class) class CaptionWindowDecorationTests : ShellTestCase() { + + private val exclusionRegion = Region.obtain() + @Test fun updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() { val taskInfo = createTaskInfo() @@ -50,7 +54,8 @@ class CaptionWindowDecorationTests : ShellTestCase() { true /* isStatusBarVisible */, false /* isKeyguardVisibleAndOccluded */, InsetsState(), - true /* hasGlobalFocus */ + true /* hasGlobalFocus */, + exclusionRegion ) Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isTrue() @@ -72,7 +77,8 @@ class CaptionWindowDecorationTests : ShellTestCase() { true /* isStatusBarVisible */, false /* isKeyguardVisibleAndOccluded */, InsetsState(), - true /* hasGlobalFocus */ + true /* hasGlobalFocus */, + exclusionRegion ) Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isFalse() @@ -90,7 +96,8 @@ class CaptionWindowDecorationTests : ShellTestCase() { true /* isStatusBarVisible */, false /* isKeyguardVisibleAndOccluded */, InsetsState(), - true /* hasGlobalFocus */ + true /* hasGlobalFocus */, + exclusionRegion ) Truth.assertThat(relayoutParams.mOccludingCaptionElements.size).isEqualTo(2) Truth.assertThat(relayoutParams.mOccludingCaptionElements[0].mAlignment).isEqualTo( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index 956100d9bc03..be664f86e9f5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -30,6 +30,7 @@ import android.content.Intent import android.content.Intent.ACTION_MAIN import android.content.pm.ActivityInfo import android.graphics.Rect +import android.graphics.Region import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.hardware.input.InputManager @@ -48,6 +49,7 @@ import android.testing.TestableLooper.RunWithLooper import android.util.SparseArray import android.view.Choreographer import android.view.Display.DEFAULT_DISPLAY +import android.view.ISystemGestureExclusionListener import android.view.IWindowManager import android.view.InputChannel import android.view.InputMonitor @@ -84,7 +86,6 @@ import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayInsetsController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.MultiInstanceHelper -import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler import com.android.wm.shell.desktopmode.DesktopModeEventLogger @@ -131,6 +132,7 @@ import org.mockito.Mockito.times import org.mockito.kotlin.KArgumentCaptor import org.mockito.kotlin.verify import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doNothing @@ -175,7 +177,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockInputMonitorFactory: DesktopModeWindowDecorViewModel.InputMonitorFactory @Mock private lateinit var mockShellController: ShellController - @Mock private lateinit var mockShellExecutor: ShellExecutor + private val testShellExecutor = TestShellExecutor() @Mock private lateinit var mockAppHeaderViewHolderFactory: AppHeaderViewHolder.Factory @Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var mockShellCommandHandler: ShellCommandHandler @@ -230,13 +232,13 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { spyContext = spy(mContext) doNothing().`when`(spyContext).startActivity(any()) - shellInit = ShellInit(mockShellExecutor) + shellInit = ShellInit(testShellExecutor) windowDecorByTaskIdSpy.clear() spyContext.addMockSystemService(InputManager::class.java, mockInputManager) desktopModeEventLogger = mock<DesktopModeEventLogger>() desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel( spyContext, - mockShellExecutor, + testShellExecutor, mockMainHandler, mockMainChoreographer, bgExecutor, @@ -1321,11 +1323,11 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { decoration.mHasGlobalFocus = true desktopModeWindowDecorViewModel.onTaskInfoChanged(task) - verify(decoration).relayout(task, true) + verify(decoration).relayout(eq(task), eq(true), anyOrNull()) decoration.mHasGlobalFocus = false desktopModeWindowDecorViewModel.onTaskInfoChanged(task) - verify(decoration).relayout(task, false) + verify(decoration).relayout(eq(task), eq(false), anyOrNull()) } @Test @@ -1342,17 +1344,66 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { task.isFocused = true desktopModeWindowDecorViewModel.onTaskInfoChanged(task) - verify(decoration).relayout(task, true) + verify(decoration).relayout(eq(task), eq(true), anyOrNull()) task.isFocused = false desktopModeWindowDecorViewModel.onTaskInfoChanged(task) - verify(decoration).relayout(task, false) + verify(decoration).relayout(eq(task), eq(false), anyOrNull()) + } + + @Test + fun testGestureExclusionChanged_updatesDecorations() { + val captor = argumentCaptor<ISystemGestureExclusionListener>() + verify(mockWindowManager) + .registerSystemGestureExclusionListener(captor.capture(), eq(DEFAULT_DISPLAY)) + val task = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + displayId = DEFAULT_DISPLAY + ) + val task2 = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + displayId = DEFAULT_DISPLAY + ) + val newRegion = Region.obtain().apply { + set(Rect(0, 0, 1600, 80)) + } + + captor.firstValue.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, newRegion, newRegion) + testShellExecutor.flushAll() + + verify(task).onExclusionRegionChanged(newRegion) + verify(task2).onExclusionRegionChanged(newRegion) + } + + @Test + fun testGestureExclusionChanged_otherDisplay_skipsDecorationUpdate() { + val captor = argumentCaptor<ISystemGestureExclusionListener>() + verify(mockWindowManager) + .registerSystemGestureExclusionListener(captor.capture(), eq(DEFAULT_DISPLAY)) + val task = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + displayId = DEFAULT_DISPLAY + ) + val task2 = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + displayId = 2 + ) + val newRegion = Region.obtain().apply { + set(Rect(0, 0, 1600, 80)) + } + + captor.firstValue.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, newRegion, newRegion) + testShellExecutor.flushAll() + + verify(task).onExclusionRegionChanged(newRegion) + verify(task2, never()).onExclusionRegionChanged(newRegion) } private fun createOpenTaskDecoration( @WindowingMode windowingMode: Int, taskSurface: SurfaceControl = SurfaceControl(), requestingImmersive: Boolean = false, + displayId: Int = DEFAULT_DISPLAY, onMaxOrRestoreListenerCaptor: ArgumentCaptor<Function0<Unit>> = forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>, onImmersiveOrRestoreListenerCaptor: KArgumentCaptor<() -> Unit> = @@ -1376,6 +1427,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { ): DesktopModeWindowDecoration { val decor = setUpMockDecorationForTask(createTask( windowingMode = windowingMode, + displayId = displayId, requestingImmersive = requestingImmersive )) onTaskOpening(decor.mTaskInfo, taskSurface) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 41f57ae0fd97..1d2d0f078817 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -64,6 +64,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.Region; import android.net.Uri; import android.os.Handler; import android.os.SystemProperties; @@ -224,6 +225,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private TestableContext mTestableContext; private final ShellExecutor mBgExecutor = new TestShellExecutor(); private final AssistContent mAssistContent = new AssistContent(); + private final Region mExclusionRegion = Region.obtain(); /** Set up run before test class. */ @BeforeClass @@ -262,8 +264,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY); doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt()); when(mMockHandleMenuFactory.create(any(), any(), anyInt(), any(), any(), any(), - anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(), - anyInt(), anyInt())) + anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), any(), + anyInt(), anyInt(), anyInt(), anyInt())) .thenReturn(mMockHandleMenu); when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any())).thenReturn(false); when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any(), any(), @@ -283,7 +285,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); - spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */, mExclusionRegion); // Menus should close if open before the task being invisible causes relayout to return. verify(spyWindowDecor).closeHandleMenu(); @@ -303,7 +305,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mShadowRadiusId).isNotEqualTo(Resources.ID_NULL); } @@ -324,7 +327,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mCornerRadius).isGreaterThan(0); } @@ -350,7 +354,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(customTaskDensity); } @@ -377,12 +382,14 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(systemDensity); } @Test + @DisableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS) public void updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); @@ -400,12 +407,39 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.hasInputFeatureSpy()).isTrue(); } @Test + @EnableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS) + public void updateRelayoutParams_freeformAndTransparentAppearance_limitedTouchRegion() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance( + APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false, + /* isStatusBarVisible */ true, + /* isKeyguardVisibleAndOccluded */ false, + /* inFullImmersiveMode */ false, + new InsetsState(), + /* hasGlobalFocus= */ true, + mExclusionRegion); + + assertThat(relayoutParams.mLimitTouchRegionToSystemAreas).isTrue(); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS) public void updateRelayoutParams_freeformButOpaqueAppearance_disallowsInputFallthrough() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); @@ -422,12 +456,38 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.hasInputFeatureSpy()).isFalse(); } @Test + @EnableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS) + public void updateRelayoutParams_freeformButOpaqueAppearance_unlimitedTouchRegion() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance(0); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false, + /* isStatusBarVisible */ true, + /* isKeyguardVisibleAndOccluded */ false, + /* inFullImmersiveMode */ false, + new InsetsState(), + /* hasGlobalFocus= */ true, + mExclusionRegion); + + assertThat(relayoutParams.mLimitTouchRegionToSystemAreas).isFalse(); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS) public void updateRelayoutParams_fullscreen_disallowsInputFallthrough() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); @@ -443,12 +503,36 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.hasInputFeatureSpy()).isFalse(); } @Test + @EnableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS) + public void updateRelayoutParams_fullscreen_unlimitedTouchRegion() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false, + /* isStatusBarVisible */ true, + /* isKeyguardVisibleAndOccluded */ false, + /* inFullImmersiveMode */ false, + new InsetsState(), + /* hasGlobalFocus= */ true, + mExclusionRegion); + + assertThat(relayoutParams.mLimitTouchRegionToSystemAreas).isFalse(); + } + + @Test public void updateRelayoutParams_freeform_inputChannelNeeded() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); @@ -464,7 +548,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(hasNoInputChannelFeature(relayoutParams)).isFalse(); } @@ -486,7 +571,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue(); } @@ -508,7 +594,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue(); } @@ -531,7 +618,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) != 0).isTrue(); } @@ -555,7 +643,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) == 0).isTrue(); } @@ -577,7 +666,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat( (relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) != 0) @@ -601,7 +691,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat( (relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) == 0) @@ -631,7 +722,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ true, insetsState, - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); // Takes status bar inset as padding, ignores caption bar inset. assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50); @@ -654,7 +746,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ true, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mIsInsetSource).isFalse(); } @@ -676,7 +769,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); // Header is always shown because it's assumed the status bar is always visible. assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -698,7 +792,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mIsCaptionVisible).isTrue(); } @@ -719,7 +814,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -740,7 +836,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ true, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -762,7 +859,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ true, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -776,7 +874,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ true, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -798,7 +897,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ true, /* inFullImmersiveMode */ true, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -809,7 +909,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockTransaction).apply(); verify(mMockRootSurfaceControl, never()).applyTransactionOnDraw(any()); @@ -824,7 +924,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT) taskInfo.isResizeable = false; - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockTransaction, never()).apply(); verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockTransaction); @@ -836,7 +936,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any()); } @@ -848,7 +948,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); // Once for view host, the other for the AppHandle input layer. verify(mMockHandler, times(2)).post(runnableArgument.capture()); @@ -865,7 +965,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT) taskInfo.isResizeable = false; - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any()); verify(mMockHandler, never()).post(any()); @@ -877,11 +977,11 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); // Once for view host, the other for the AppHandle input layer. verify(mMockHandler, times(2)).post(runnableArgument.capture()); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockHandler).removeCallbacks(runnableArgument.getValue()); } @@ -892,7 +992,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); // Once for view host, the other for the AppHandle input layer. verify(mMockHandler, times(2)).post(runnableArgument.capture()); @@ -1132,7 +1232,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { runnableArgument.getValue().run(); // Relayout decor with same captured link - decor.relayout(taskInfo, true /* hasGlobalFocus */); + decor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); // Verify handle menu's browser link not set to captured link since link is expired createHandleMenu(decor); @@ -1313,7 +1413,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockCaptionHandleRepository, never()).notifyCaptionChanged(any()); } @@ -1330,7 +1430,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( CaptionState.class); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( captionStateArgumentCaptor.capture()); @@ -1357,7 +1457,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( CaptionState.class); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockAppHeaderViewHolder, atLeastOnce()).runOnAppChipGlobalLayout( runnableArgumentCaptor.capture()); runnableArgumentCaptor.getValue().invoke(); @@ -1380,7 +1480,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( CaptionState.class); - spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */, mExclusionRegion); verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( captionStateArgumentCaptor.capture()); @@ -1400,7 +1500,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( CaptionState.class); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); createHandleMenu(spyWindowDecor); verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( @@ -1425,7 +1525,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( CaptionState.class); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); createHandleMenu(spyWindowDecor); spyWindowDecor.closeHandleMenu(); @@ -1440,9 +1540,30 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) + public void browserApp_webUriUsedForBrowserApp() { + // Make {@link AppToWebUtils#isBrowserApp} return true + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.handleAllWebDataURI = true; + resolveInfo.activityInfo = createActivityInfo(); + when(mMockPackageManager.queryIntentActivitiesAsUser(any(), anyInt(), anyInt())) + .thenReturn(List.of(resolveInfo)); + + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final DesktopModeWindowDecoration decor = createWindowDecoration( + taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* web uri */, + TEST_URI3 /* generic link */); + + // Verify web uri used for browser applications + createHandleMenu(decor); + verifyHandleMenuCreated(TEST_URI2); + } + + private void verifyHandleMenuCreated(@Nullable Uri uri) { verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(), - any(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), + any(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), argThat(intent -> (uri == null && intent == null) || intent.getData().equals(uri)), anyInt(), anyInt(), anyInt(), anyInt()); } @@ -1522,7 +1643,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { windowDecor.setOpenInBrowserClickListener(mMockOpenInBrowserClickListener); windowDecor.mDecorWindowContext = mContext; if (relayout) { - windowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); } return windowDecor; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index ade17c61eda1..7ec2cbf9460e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -242,7 +242,7 @@ class HandleMenuTest : ShellTestCase() { private fun createAndShowHandleMenu( splitPosition: Int? = null, - forceShowSystemBars: Boolean = false, + forceShowSystemBars: Boolean = false ): HandleMenu { val layoutId = if (mockDesktopWindowDecoration.mTaskInfo.isFreeform) { R.layout.desktop_mode_app_header @@ -266,8 +266,9 @@ class HandleMenuTest : ShellTestCase() { WindowManagerWrapper(mockWindowManager), layoutId, appIcon, appName, splitScreenController, shouldShowWindowingPill = true, shouldShowNewWindowButton = true, shouldShowManageWindowsButton = false, - shouldShowChangeAspectRatioButton = false, - null /* openInBrowserLink */, captionWidth = HANDLE_WIDTH, captionHeight = 50, + shouldShowChangeAspectRatioButton = false, isBrowserApp = false, + null /* openInAppOrBrowserIntent */, captionWidth = HANDLE_WIDTH, + captionHeight = 50, captionX = captionX, captionY = 0, ) @@ -278,7 +279,7 @@ class HandleMenuTest : ShellTestCase() { onNewWindowClickListener = mock(), onManageWindowsClickListener = mock(), onChangeAspectRatioClickListener = mock(), - openInBrowserClickListener = mock(), + openInAppOrBrowserClickListener = mock(), onOpenByDefaultClickListener = mock(), onCloseMenuClickListener = mock(), onOutsideTouchListener = mock(), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index 8e0434cb28f7..534803db5fe0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -60,6 +60,7 @@ import android.content.res.Resources; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.Region; import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.util.DisplayMetrics; @@ -508,7 +509,7 @@ public class WindowDecorationTests extends ShellTestCase { final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); windowDecor.relayout(taskInfo, true /* applyStartTransactionOnDraw */, - true /* hasGlobalFocus */); + true /* hasGlobalFocus */, Region.obtain()); verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockSurfaceControlStartT); } @@ -525,7 +526,7 @@ public class WindowDecorationTests extends ShellTestCase { mRelayoutParams.mCaptionTopPadding = 50; windowDecor.relayout(taskInfo, false /* applyStartTransactionOnDraw */, - true /* hasGlobalFocus */); + true /* hasGlobalFocus */, Region.obtain()); assertEquals(50, mRelayoutResult.mCaptionTopPadding); } @@ -944,7 +945,7 @@ public class WindowDecorationTests extends ShellTestCase { decor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */)); - verify(decor, times(2)).relayout(task, true /* hasGlobalFocus */); + verify(decor, times(2)).relayout(any(), any(), any(), any(), any(), any()); } @Test @@ -958,7 +959,7 @@ public class WindowDecorationTests extends ShellTestCase { decor.onInsetsStateChanged(createInsetsState(statusBars(), true /* visible */)); - verify(decor, times(1)).relayout(task, true /* hasGlobalFocus */); + verify(decor, times(1)).relayout(any(), any(), any(), any(), any(), any()); } @Test @@ -973,7 +974,7 @@ public class WindowDecorationTests extends ShellTestCase { decor.onKeyguardStateChanged(true /* visible */, true /* occluding */); assertTrue(decor.mIsKeyguardVisibleAndOccluded); - verify(decor, times(2)).relayout(task, true /* hasGlobalFocus */); + verify(decor, times(2)).relayout(any(), any(), any(), any(), any(), any()); } @Test @@ -987,7 +988,7 @@ public class WindowDecorationTests extends ShellTestCase { decor.onKeyguardStateChanged(false /* visible */, true /* occluding */); - verify(decor, times(1)).relayout(task, true /* hasGlobalFocus */); + verify(decor, times(1)).relayout(any(), any(), any(), any(), any(), any()); } private ActivityManager.RunningTaskInfo createTaskInfo() { @@ -1061,9 +1062,16 @@ public class WindowDecorationTests extends ShellTestCase { surfaceControlViewHostFactory, desktopModeEventLogger); } - @Override void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus) { - relayout(taskInfo, false /* applyStartTransactionOnDraw */, hasGlobalFocus); + relayout(taskInfo, false /* applyStartTransactionOnDraw */, hasGlobalFocus, + Region.obtain()); + } + + @Override + void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus, + @NonNull Region displayExclusionRegion) { + relayout(taskInfo, false /* applyStartTransactionOnDraw */, hasGlobalFocus, + displayExclusionRegion); } @Override @@ -1085,11 +1093,13 @@ public class WindowDecorationTests extends ShellTestCase { } void relayout(ActivityManager.RunningTaskInfo taskInfo, - boolean applyStartTransactionOnDraw, boolean hasGlobalFocus) { + boolean applyStartTransactionOnDraw, boolean hasGlobalFocus, + @NonNull Region displayExclusionRegion) { mRelayoutParams.mRunningTaskInfo = taskInfo; mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; mRelayoutParams.mLayoutResId = R.layout.caption_layout; mRelayoutParams.mHasGlobalFocus = hasGlobalFocus; + mRelayoutParams.mDisplayExclusionRegion.set(displayExclusionRegion); relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mMockView, mRelayoutResult); } diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java index 28c3b3df9b1c..2540236f2ce5 100644 --- a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java @@ -125,6 +125,7 @@ public final class AppFunctionException extends Exception { public AppFunctionException( int errorCode, @Nullable String errorMessage, @NonNull Bundle extras) { + super(errorMessage); mErrorCode = errorCode; mErrorMessage = errorMessage; mExtras = extras; diff --git a/libs/hwui/FeatureFlags.h b/libs/hwui/FeatureFlags.h index fddcf29b9197..5f84f47b725d 100644 --- a/libs/hwui/FeatureFlags.h +++ b/libs/hwui/FeatureFlags.h @@ -33,9 +33,9 @@ inline bool letter_spacing_justification() { #endif // __ANDROID__ } -inline bool typeface_redesign() { +inline bool typeface_redesign_readonly() { #ifdef __ANDROID__ - static bool flag = com_android_text_flags_typeface_redesign(); + static bool flag = com_android_text_flags_typeface_redesign_readonly(); return flag; #else return true; diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 5ad788c67816..fa27af671be6 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -154,3 +154,13 @@ flag { description: "API's that enable animated image drawables to use nearest sampling when scaling." bug: "370523334" } + +flag { + name: "remove_vri_sketchy_destroy" + namespace: "core_graphics" + description: "Remove the eager yet thread-violating destroyHardwareResources in VRI#die" + bug: "377057106" + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/libs/hwui/hwui/MinikinUtils.h b/libs/hwui/hwui/MinikinUtils.h index 1510ce1378d8..20acf981d9b9 100644 --- a/libs/hwui/hwui/MinikinUtils.h +++ b/libs/hwui/hwui/MinikinUtils.h @@ -73,7 +73,7 @@ public: static void forFontRun(const minikin::Layout& layout, Paint* paint, F& f) { float saveSkewX = paint->getSkFont().getSkewX(); bool savefakeBold = paint->getSkFont().isEmbolden(); - if (text_feature::typeface_redesign()) { + if (text_feature::typeface_redesign_readonly()) { for (uint32_t runIdx = 0; runIdx < layout.getFontRunCount(); ++runIdx) { uint32_t start = layout.getFontRunStart(runIdx); uint32_t end = layout.getFontRunEnd(runIdx); diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp index 70e6beda6cb9..5f693462af91 100644 --- a/libs/hwui/jni/text/TextShaper.cpp +++ b/libs/hwui/jni/text/TextShaper.cpp @@ -86,7 +86,7 @@ static jlong shapeTextRun(const uint16_t* text, int textSize, int start, int cou overallDescent = std::max(overallDescent, extent.descent); } - if (text_feature::typeface_redesign()) { + if (text_feature::typeface_redesign_readonly()) { uint32_t runCount = layout.getFontRunCount(); std::unordered_map<minikin::FakedFont, uint32_t, FakedFontKey> fakedToFontIds; @@ -229,7 +229,7 @@ float findValueFromVariationSettings(const minikin::FontFakery& fakery, minikin: // CriticalNative static jfloat TextShaper_Result_getWeightOverride(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) { const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); - if (text_feature::typeface_redesign()) { + if (text_feature::typeface_redesign_readonly()) { float value = findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_wght); return std::isnan(value) ? NO_OVERRIDE : value; @@ -241,7 +241,7 @@ static jfloat TextShaper_Result_getWeightOverride(CRITICAL_JNI_PARAMS_COMMA jlon // CriticalNative static jfloat TextShaper_Result_getItalicOverride(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) { const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); - if (text_feature::typeface_redesign()) { + if (text_feature::typeface_redesign_readonly()) { float value = findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_ital); return std::isnan(value) ? NO_OVERRIDE : value; diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java index 88efed55c11f..3f9126aa9456 100644 --- a/media/java/android/media/MediaCas.java +++ b/media/java/android/media/MediaCas.java @@ -1000,7 +1000,10 @@ public final class MediaCas implements AutoCloseable { @SystemApi @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS) public boolean updateResourcePriority(int priority, int niceValue) { - return mTunerResourceManager.updateClientPriority(mClientId, priority, niceValue); + if (mTunerResourceManager != null) { + return mTunerResourceManager.updateClientPriority(mClientId, priority, niceValue); + } + return false; } /** @@ -1017,7 +1020,9 @@ public final class MediaCas implements AutoCloseable { @SystemApi @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS) public void setResourceHolderRetain(boolean resourceHolderRetain) { - mTunerResourceManager.setResourceHolderRetain(mClientId, resourceHolderRetain); + if (mTunerResourceManager != null) { + mTunerResourceManager.setResourceHolderRetain(mClientId, resourceHolderRetain); + } } IHwBinder getBinder() { diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java index e575daeb8d29..2ae89d3300c1 100644 --- a/media/java/android/media/MediaCodec.java +++ b/media/java/android/media/MediaCodec.java @@ -18,6 +18,7 @@ package android.media; import static android.media.codec.Flags.FLAG_NULL_OUTPUT_SURFACE; import static android.media.codec.Flags.FLAG_REGION_OF_INTEREST; +import static android.media.codec.Flags.FLAG_SUBSESSION_METRICS; import static com.android.media.codec.flags.Flags.FLAG_LARGE_AUDIO_FRAME; @@ -890,7 +891,7 @@ import java.util.function.Supplier; any start codes), and submit it as a <strong>regular</strong> input buffer. <p> You will receive an {@link #INFO_OUTPUT_FORMAT_CHANGED} return value from {@link - #dequeueOutputBuffer dequeueOutputBuffer} or a {@link Callback#onOutputBufferAvailable + #dequeueOutputBuffer dequeueOutputBuffer} or a {@link Callback#onOutputFormatChanged onOutputFormatChanged} callback just after the picture-size change takes place and before any frames with the new size have been returned. <p class=note> @@ -1835,6 +1836,13 @@ final public class MediaCodec { private static final int CB_CRYPTO_ERROR = 6; private static final int CB_LARGE_FRAME_OUTPUT_AVAILABLE = 7; + /** + * Callback ID for when the metrics for this codec have been flushed due to + * the start of a new subsession. The associated Java Message object will + * contain the flushed metrics as a PersistentBundle in the obj field. + */ + private static final int CB_METRICS_FLUSHED = 8; + private class EventHandler extends Handler { private MediaCodec mCodec; @@ -2007,6 +2015,15 @@ final public class MediaCodec { break; } + case CB_METRICS_FLUSHED: + { + + if (GetFlag(() -> android.media.codec.Flags.subsessionMetrics())) { + mCallback.onMetricsFlushed(mCodec, (PersistableBundle)msg.obj); + } + break; + } + default: { break; @@ -4958,14 +4975,24 @@ final public class MediaCodec { public native final String getCanonicalName(); /** - * Return Metrics data about the current codec instance. + * Return Metrics data about the current codec instance. + * <p> + * Call this method after configuration, during execution, or after + * the codec has been already stopped. + * <p> + * Beginning with {@link android.os.Build.VERSION_CODES#B} + * this method can be used to get the Metrics data prior to an error. + * (e.g. in {@link Callback#onError} or after a method throws + * {@link MediaCodec.CodecException}.) Before that, the Metrics data was + * cleared on error, resulting in a null return value. * * @return a {@link PersistableBundle} containing the set of attributes and values * available for the media being handled by this instance of MediaCodec * The attributes are descibed in {@link MetricsConstants}. * * Additional vendor-specific fields may also be present in - * the return value. + * the return value. Returns null if there is no Metrics data. + * */ public PersistableBundle getMetrics() { PersistableBundle bundle = native_getMetrics(); @@ -5692,6 +5719,27 @@ final public class MediaCodec { */ public abstract void onOutputFormatChanged( @NonNull MediaCodec codec, @NonNull MediaFormat format); + + /** + * Called when the metrics for this codec have been flushed due to the + * start of a new subsession. + * <p> + * This can happen when the codec is reconfigured after stop(), or + * mid-stream e.g. if the video size changes. When this happens, the + * metrics for the previous subsession are flushed, and + * {@link MediaCodec#getMetrics} will return the metrics for the + * new subsession. This happens just before the {@link Callback#onOutputFormatChanged} + * event, so this <b>optional</b> callback is provided to be able to + * capture the final metrics for the previous subsession. + * + * @param codec The MediaCodec object. + * @param metrics The flushed metrics for this codec. + */ + @FlaggedApi(FLAG_SUBSESSION_METRICS) + public void onMetricsFlushed( + @NonNull MediaCodec codec, @NonNull PersistableBundle metrics) { + // default implementation ignores this callback. + } } private void postEventFromNative( diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 3499c438086d..20108e7369d7 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -1771,10 +1771,12 @@ public final class MediaRouter2 { } /** - * A class to control media routing session in media route provider. For example, - * selecting/deselecting/transferring to routes of a session can be done through this. Instances - * are created when {@link TransferCallback#onTransfer(RoutingController, RoutingController)} is - * called, which is invoked after {@link #transferTo(MediaRoute2Info)} is called. + * Controls a media routing session. + * + * <p>Routing controllers wrap a {@link RoutingSessionInfo}, taking care of mapping route ids to + * {@link MediaRoute2Info} instances. You can still access the underlying session using {@link + * #getRoutingSessionInfo()}, but keep in mind it can be changed by other threads. Changes to + * the routing session are notified via {@link ControllerCallback}. */ public class RoutingController { private final Object mControllerLock = new Object(); @@ -1836,7 +1838,9 @@ public final class MediaRouter2 { } /** - * @return the unmodifiable list of currently selected routes + * Returns the unmodifiable list of currently selected routes + * + * @see RoutingSessionInfo#getSelectedRoutes() */ @NonNull public List<MediaRoute2Info> getSelectedRoutes() { @@ -1848,7 +1852,9 @@ public final class MediaRouter2 { } /** - * @return the unmodifiable list of selectable routes for the session. + * Returns the unmodifiable list of selectable routes for the session. + * + * @see RoutingSessionInfo#getSelectableRoutes() */ @NonNull public List<MediaRoute2Info> getSelectableRoutes() { @@ -1860,7 +1866,9 @@ public final class MediaRouter2 { } /** - * @return the unmodifiable list of deselectable routes for the session. + * Returns the unmodifiable list of deselectable routes for the session. + * + * @see RoutingSessionInfo#getDeselectableRoutes() */ @NonNull public List<MediaRoute2Info> getDeselectableRoutes() { diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java index 83a4dd5a682a..3b8cf3fb2909 100644 --- a/media/java/android/media/RoutingSessionInfo.java +++ b/media/java/android/media/RoutingSessionInfo.java @@ -262,7 +262,8 @@ public final class RoutingSessionInfo implements Parcelable { } /** - * Gets the provider id of the session. + * Gets the provider ID of the session. + * * @hide */ @Nullable @@ -271,7 +272,15 @@ public final class RoutingSessionInfo implements Parcelable { } /** - * Gets the list of IDs of selected routes for the session. It shouldn't be empty. + * Gets the list of IDs of selected routes for the session. + * + * <p>Selected routes are the routes that this session is actively routing media to. + * + * <p>The behavior of a routing session with multiple selected routes is ultimately defined by + * the {@link MediaRoute2ProviderService} implementation. However, typically, it's expected that + * all the selected routes of a routing session are playing the same media in sync. + * + * @return A non-empty list of selected route ids. */ @NonNull public List<String> getSelectedRoutes() { @@ -280,6 +289,16 @@ public final class RoutingSessionInfo implements Parcelable { /** * Gets the list of IDs of selectable routes for the session. + * + * <p>Selectable routes can be added to a routing session (via {@link + * MediaRouter2.RoutingController#selectRoute}) in order to add them to the {@link + * #getSelectedRoutes() selected routes}, so that media plays on the newly selected route along + * with the other selected routes. + * + * <p>Not to be confused with {@link #getTransferableRoutes() transferable routes}. Transferring + * to a route makes it the sole selected route. + * + * @return A possibly empty list of selectable route ids. */ @NonNull public List<String> getSelectableRoutes() { @@ -288,6 +307,17 @@ public final class RoutingSessionInfo implements Parcelable { /** * Gets the list of IDs of deselectable routes for the session. + * + * <p>Deselectable routes can be removed from the {@link #getSelectedRoutes() selected routes}, + * so that the routing session stops routing to the newly deselected route, but continues on any + * remaining selected routes. + * + * <p>Deselectable routes should be a subset of the {@link #getSelectedRoutes() selected + * routes}, meaning not all of the selected routes might be deselectable. For example, one of + * the selected routes may be a leader device coordinating group playback, which must always + * remain selected while the session is active. + * + * @return A possibly empty list of deselectable route ids. */ @NonNull public List<String> getDeselectableRoutes() { @@ -296,6 +326,24 @@ public final class RoutingSessionInfo implements Parcelable { /** * Gets the list of IDs of transferable routes for the session. + * + * <p>Transferring to a route (for example, using {@link MediaRouter2#transferTo}) replaces the + * list of {@link #getSelectedRoutes() selected routes} with the target route, causing playback + * to move from one route to another. + * + * <p>Note that this is different from {@link #getSelectableRoutes() selectable routes}, because + * selecting a route makes it part of the selected routes, while transferring to a route makes + * it the selected route. A route can be both transferable and selectable. + * + * <p>Note that playback may transfer across routes without the target route being in the list + * of transferable routes. This can happen by creating a new routing session to the target + * route, and releasing the routing session being transferred from. The difference is that a + * transfer to a route in the transferable list can happen with no intervention from the app, + * with the route provider taking care of the entire operation. A transfer to a route that is + * not in the list of transferable routes (by creating a new session) requires the app to move + * the playback state from one device to the other. + * + * @return A possibly empty list of transferable route ids. */ @NonNull public List<String> getTransferableRoutes() { diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index ba2398c12607..7895eb27b372 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -166,3 +166,10 @@ flag { description: "Allows audio input devices routing and volume control via system settings." bug: "355684672" } + +flag { + name: "enable_mirroring_in_media_router_2" + namespace: "media_better_together" + description: "Enables support for mirroring routes in the MediaRouter2 framework, allowing Output Switcher to offer mirroring routes." + bug: "362507305" +} diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt index 675c8f80add2..a23845fa17e9 100644 --- a/nfc/api/system-current.txt +++ b/nfc/api/system-current.txt @@ -100,6 +100,7 @@ package android.nfc { method public void onHceEventReceived(int); method public void onLaunchHceAppChooserActivity(@NonNull String, @NonNull java.util.List<android.nfc.cardemulation.ApduServiceInfo>, @NonNull android.content.ComponentName, @NonNull String); method public void onLaunchHceTapAgainDialog(@NonNull android.nfc.cardemulation.ApduServiceInfo, @NonNull String); + method public void onLogEventNotified(@NonNull android.nfc.OemLogItems); method public void onNdefMessage(@NonNull android.nfc.Tag, @NonNull android.nfc.NdefMessage, @NonNull java.util.function.Consumer<java.lang.Boolean>); method public void onNdefRead(@NonNull java.util.function.Consumer<java.lang.Boolean>); method public void onReaderOptionChanged(boolean); @@ -115,6 +116,27 @@ package android.nfc { method public int getNfceeId(); } + @FlaggedApi("android.nfc.nfc_oem_extension") public final class OemLogItems implements android.os.Parcelable { + method public int describeContents(); + method public int getAction(); + method public int getCallingPid(); + method @Nullable public byte[] getCommandApdu(); + method public int getEvent(); + method @Nullable public byte[] getResponseApdu(); + method @Nullable public java.time.Instant getRfFieldEventTimeMillis(); + method @Nullable public android.nfc.Tag getTag(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.nfc.OemLogItems> CREATOR; + field public static final int EVENT_DISABLE = 2; // 0x2 + field public static final int EVENT_ENABLE = 1; // 0x1 + field public static final int EVENT_UNSET = 0; // 0x0 + field public static final int LOG_ACTION_HCE_DATA = 516; // 0x204 + field public static final int LOG_ACTION_NFC_TOGGLE = 513; // 0x201 + field public static final int LOG_ACTION_RF_FIELD_STATE_CHANGED = 1; // 0x1 + field public static final int LOG_ACTION_SCREEN_STATE_CHANGED = 518; // 0x206 + field public static final int LOG_ACTION_TAG_DETECTED = 3; // 0x3 + } + @FlaggedApi("android.nfc.nfc_oem_extension") public class RoutingStatus { method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int getDefaultIsoDepRoute(); method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int getDefaultOffHostRoute(); diff --git a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl index 7f1fd15fe68a..b102e873d737 100644 --- a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl +++ b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl @@ -18,6 +18,7 @@ package android.nfc; import android.content.ComponentName; import android.nfc.cardemulation.ApduServiceInfo; import android.nfc.NdefMessage; +import android.nfc.OemLogItems; import android.nfc.Tag; import android.os.ResultReceiver; @@ -51,4 +52,5 @@ interface INfcOemExtensionCallback { void onNdefMessage(in Tag tag, in NdefMessage message, in ResultReceiver hasOemExecutableContent); void onLaunchHceAppChooserActivity(in String selectedAid, in List<ApduServiceInfo> services, in ComponentName failedComponent, in String category); void onLaunchHceTapAgainActivity(in ApduServiceInfo service, in String category); + void onLogEventNotified(in OemLogItems item); } diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java index 1bfe71461ac3..abd99bc02f55 100644 --- a/nfc/java/android/nfc/NfcOemExtension.java +++ b/nfc/java/android/nfc/NfcOemExtension.java @@ -392,6 +392,12 @@ public final class NfcOemExtension { * @param category the category of the service */ void onLaunchHceTapAgainDialog(@NonNull ApduServiceInfo service, @NonNull String category); + + /** + * Callback when OEM specified log event are notified. + * @param item the log items that contains log information of NFC event. + */ + void onLogEventNotified(@NonNull OemLogItems item); } @@ -900,6 +906,12 @@ public final class NfcOemExtension { handleVoid2ArgCallback(service, category, cb::onLaunchHceTapAgainDialog, ex)); } + @Override + public void onLogEventNotified(OemLogItems item) throws RemoteException { + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback(item, cb::onLogEventNotified, ex)); + } + private <T> void handleVoidCallback( T input, Consumer<T> callbackMethod, Executor executor) { synchronized (mLock) { diff --git a/nfc/java/android/nfc/OemLogItems.aidl b/nfc/java/android/nfc/OemLogItems.aidl new file mode 100644 index 000000000000..3bcb445fc7d2 --- /dev/null +++ b/nfc/java/android/nfc/OemLogItems.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 android.nfc; + +parcelable OemLogItems;
\ No newline at end of file diff --git a/nfc/java/android/nfc/OemLogItems.java b/nfc/java/android/nfc/OemLogItems.java new file mode 100644 index 000000000000..6671941c1cc9 --- /dev/null +++ b/nfc/java/android/nfc/OemLogItems.java @@ -0,0 +1,325 @@ +/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nfc;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Instant;
+
+/**
+ * A log class for OEMs to get log information of NFC events.
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+@SystemApi
+public final class OemLogItems implements Parcelable {
+ /**
+ * Used when RF field state is changed.
+ */
+ public static final int LOG_ACTION_RF_FIELD_STATE_CHANGED = 0X01;
+ /**
+ * Used when NFC is toggled. Event should be set to {@link LogEvent#EVENT_ENABLE} or
+ * {@link LogEvent#EVENT_DISABLE} if this action is used.
+ */
+ public static final int LOG_ACTION_NFC_TOGGLE = 0x0201;
+ /**
+ * Used when sending host routing status.
+ */
+ public static final int LOG_ACTION_HCE_DATA = 0x0204;
+ /**
+ * Used when screen state is changed.
+ */
+ public static final int LOG_ACTION_SCREEN_STATE_CHANGED = 0x0206;
+ /**
+ * Used when tag is detected.
+ */
+ public static final int LOG_ACTION_TAG_DETECTED = 0x03;
+
+ /**
+ * @hide
+ */
+ @IntDef(prefix = { "LOG_ACTION_" }, value = {
+ LOG_ACTION_RF_FIELD_STATE_CHANGED,
+ LOG_ACTION_NFC_TOGGLE,
+ LOG_ACTION_HCE_DATA,
+ LOG_ACTION_SCREEN_STATE_CHANGED,
+ LOG_ACTION_TAG_DETECTED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LogAction {}
+
+ /**
+ * Represents the event is not set.
+ */
+ public static final int EVENT_UNSET = 0;
+ /**
+ * Represents nfc enable is called.
+ */
+ public static final int EVENT_ENABLE = 1;
+ /**
+ * Represents nfc disable is called.
+ */
+ public static final int EVENT_DISABLE = 2;
+ /** @hide */
+ @IntDef(prefix = { "EVENT_" }, value = {
+ EVENT_UNSET,
+ EVENT_ENABLE,
+ EVENT_DISABLE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LogEvent {}
+ private int mAction;
+ private int mEvent;
+ private int mCallingPid;
+ private byte[] mCommandApdus;
+ private byte[] mResponseApdus;
+ private Instant mRfFieldOnTime;
+ private Tag mTag;
+
+ /** @hide */
+ public OemLogItems(@LogAction int action, @LogEvent int event, int callingPid,
+ byte[] commandApdus, byte[] responseApdus, Instant rfFieldOnTime,
+ Tag tag) {
+ mAction = action;
+ mEvent = event;
+ mTag = tag;
+ mCallingPid = callingPid;
+ mCommandApdus = commandApdus;
+ mResponseApdus = responseApdus;
+ mRfFieldOnTime = rfFieldOnTime;
+ }
+
+ /**
+ * Describe the kinds of special objects contained in this Parcelable
+ * instance's marshaled representation. For example, if the object will
+ * include a file descriptor in the output of {@link #writeToParcel(Parcel, int)},
+ * the return value of this method must include the
+ * {@link #CONTENTS_FILE_DESCRIPTOR} bit.
+ *
+ * @return a bitmask indicating the set of special object types marshaled
+ * by this Parcelable object instance.
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Flatten this object in to a Parcel.
+ *
+ * @param dest The Parcel in which the object should be written.
+ * @param flags Additional flags about how the object should be written.
+ * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mAction);
+ dest.writeInt(mEvent);
+ dest.writeInt(mCallingPid);
+ dest.writeInt(mCommandApdus.length);
+ dest.writeByteArray(mCommandApdus);
+ dest.writeInt(mResponseApdus.length);
+ dest.writeByteArray(mResponseApdus);
+ dest.writeLong(mRfFieldOnTime.getEpochSecond());
+ dest.writeInt(mRfFieldOnTime.getNano());
+ dest.writeParcelable(mTag, 0);
+ }
+
+ /** @hide */
+ public static class Builder {
+ private final OemLogItems mItem;
+
+ public Builder(@LogAction int type) {
+ mItem = new OemLogItems(type, EVENT_UNSET, 0, new byte[0], new byte[0], null, null);
+ }
+
+ /** Setter of the log action. */
+ public OemLogItems.Builder setAction(@LogAction int action) {
+ mItem.mAction = action;
+ return this;
+ }
+
+ /** Setter of the log calling event. */
+ public OemLogItems.Builder setCallingEvent(@LogEvent int event) {
+ mItem.mEvent = event;
+ return this;
+ }
+
+ /** Setter of the log calling Pid. */
+ public OemLogItems.Builder setCallingPid(int pid) {
+ mItem.mCallingPid = pid;
+ return this;
+ }
+
+ /** Setter of APDU command. */
+ public OemLogItems.Builder setApduCommand(byte[] apdus) {
+ mItem.mCommandApdus = apdus;
+ return this;
+ }
+
+ /** Setter of RF field on time. */
+ public OemLogItems.Builder setRfFieldOnTime(Instant time) {
+ mItem.mRfFieldOnTime = time;
+ return this;
+ }
+
+ /** Setter of APDU response. */
+ public OemLogItems.Builder setApduResponse(byte[] apdus) {
+ mItem.mResponseApdus = apdus;
+ return this;
+ }
+
+ /** Setter of dispatched tag. */
+ public OemLogItems.Builder setTag(Tag tag) {
+ mItem.mTag = tag;
+ return this;
+ }
+
+ /** Builds an {@link OemLogItems} instance. */
+ public OemLogItems build() {
+ return mItem;
+ }
+ }
+
+ /**
+ * Gets the action of this log.
+ * @return one of {@link LogAction}
+ */
+ @LogAction
+ public int getAction() {
+ return mAction;
+ }
+
+ /**
+ * Gets the event of this log. This will be set to {@link LogEvent#EVENT_ENABLE} or
+ * {@link LogEvent#EVENT_DISABLE} only when action is set to
+ * {@link LogAction#LOG_ACTION_NFC_TOGGLE}
+ * @return one of {@link LogEvent}
+ */
+ @LogEvent
+ public int getEvent() {
+ return mEvent;
+ }
+
+ /**
+ * Gets the calling Pid of this log. This field will be set only when action is set to
+ * {@link LogAction#LOG_ACTION_NFC_TOGGLE}
+ * @return calling Pid
+ */
+ public int getCallingPid() {
+ return mCallingPid;
+ }
+
+ /**
+ * Gets the command APDUs of this log. This field will be set only when action is set to
+ * {@link LogAction#LOG_ACTION_HCE_DATA}
+ * @return a byte array of command APDUs with the same format as
+ * {@link android.nfc.cardemulation.HostApduService#sendResponseApdu(byte[])}
+ */
+ @Nullable
+ public byte[] getCommandApdu() {
+ return mCommandApdus;
+ }
+
+ /**
+ * Gets the response APDUs of this log. This field will be set only when action is set to
+ * {@link LogAction#LOG_ACTION_HCE_DATA}
+ * @return a byte array of response APDUs with the same format as
+ * {@link android.nfc.cardemulation.HostApduService#sendResponseApdu(byte[])}
+ */
+ @Nullable
+ public byte[] getResponseApdu() {
+ return mResponseApdus;
+ }
+
+ /**
+ * Gets the RF field event time in this log in millisecond. This field will be set only when
+ * action is set to {@link LogAction#LOG_ACTION_RF_FIELD_STATE_CHANGED}
+ * @return an {@link Instant} of RF field event time.
+ */
+ @Nullable
+ public Instant getRfFieldEventTimeMillis() {
+ return mRfFieldOnTime;
+ }
+
+ /**
+ * Gets the tag of this log. This field will be set only when action is set to
+ * {@link LogAction#LOG_ACTION_TAG_DETECTED}
+ * @return a detected {@link Tag} in {@link #LOG_ACTION_TAG_DETECTED} case. Return
+ * null otherwise.
+ */
+ @Nullable
+ public Tag getTag() {
+ return mTag;
+ }
+
+ private String byteToHex(byte[] bytes) {
+ char[] HexArray = "0123456789ABCDEF".toCharArray();
+ char[] hexChars = new char[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = HexArray[v >>> 4];
+ hexChars[j * 2 + 1] = HexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+
+ @Override
+ public String toString() {
+ return "[mCommandApdus: "
+ + ((mCommandApdus != null) ? byteToHex(mCommandApdus) : "null")
+ + "[mResponseApdus: "
+ + ((mResponseApdus != null) ? byteToHex(mResponseApdus) : "null")
+ + ", mCallingApi= " + mEvent
+ + ", mAction= " + mAction
+ + ", mCallingPId = " + mCallingPid
+ + ", mRfFieldOnTime= " + mRfFieldOnTime;
+ }
+ private OemLogItems(Parcel in) {
+ this.mAction = in.readInt();
+ this.mEvent = in.readInt();
+ this.mCallingPid = in.readInt();
+ this.mCommandApdus = new byte[in.readInt()];
+ in.readByteArray(this.mCommandApdus);
+ this.mResponseApdus = new byte[in.readInt()];
+ in.readByteArray(this.mResponseApdus);
+ this.mRfFieldOnTime = Instant.ofEpochSecond(in.readLong(), in.readInt());
+ this.mTag = in.readParcelable(Tag.class.getClassLoader(), Tag.class);
+ }
+
+ public static final @NonNull Parcelable.Creator<OemLogItems> CREATOR =
+ new Parcelable.Creator<OemLogItems>() {
+ @Override
+ public OemLogItems createFromParcel(Parcel in) {
+ return new OemLogItems(in);
+ }
+
+ @Override
+ public OemLogItems[] newArray(int size) {
+ return new OemLogItems[size];
+ }
+ };
+
+}
diff --git a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java index 3b52df7e5fbb..c3f6eb71c2e7 100644 --- a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java +++ b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java @@ -30,21 +30,28 @@ public class AppPreference extends Preference { public AppPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - setLayoutResource(R.layout.preference_app); + init(context); } public AppPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - setLayoutResource(R.layout.preference_app); + init(context); } public AppPreference(Context context) { super(context); - setLayoutResource(R.layout.preference_app); + init(context); } public AppPreference(Context context, AttributeSet attrs) { super(context, attrs); - setLayoutResource(R.layout.preference_app); + init(context); + } + + private void init(Context context) { + int resId = SettingsThemeHelper.isExpressiveTheme(context) + ? com.android.settingslib.widget.theme.R.layout.settingslib_expressive_preference + : R.layout.preference_app; + setLayoutResource(resId); } } diff --git a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java index ecd500e1a160..3dcdfbaeb8b3 100644 --- a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java +++ b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java @@ -32,22 +32,29 @@ public class AppSwitchPreference extends SwitchPreferenceCompat { public AppSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - setLayoutResource(R.layout.preference_app); + init(context); } public AppSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - setLayoutResource(R.layout.preference_app); + init(context); } public AppSwitchPreference(Context context, AttributeSet attrs) { super(context, attrs); - setLayoutResource(R.layout.preference_app); + init(context); } public AppSwitchPreference(Context context) { super(context); - setLayoutResource(R.layout.preference_app); + init(context); + } + + private void init(Context context) { + int resId = SettingsThemeHelper.isExpressiveTheme(context) + ? com.android.settingslib.widget.theme.R.layout.settingslib_expressive_preference + : R.layout.preference_app; + setLayoutResource(resId); } @Override diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt index 843d2aadf333..cd03dd7ca1b3 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt @@ -202,6 +202,12 @@ open class KeyedDataObservable<K> : KeyedObservable<K> { entry.value.execute { observer.onKeyChanged(key, reason) } } } + + fun hasAnyObserver(): Boolean { + synchronized(observers) { if (observers.isNotEmpty()) return true } + synchronized(keyedObservers) { if (keyedObservers.isNotEmpty()) return true } + return false + } } /** [KeyedObservable] with no-op implementations for all interfaces. */ diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt index 94d373bed0a5..bde4217b3962 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt @@ -136,6 +136,18 @@ class PreferenceHierarchy internal constructor(metadata: PreferenceMetadata) : for (it in children) action(it) } + /** Traversals preference hierarchy recursively and applies given action. */ + fun forEachRecursively(action: (PreferenceHierarchyNode) -> Unit) { + action(this) + for (child in children) { + if (child is PreferenceHierarchy) { + child.forEachRecursively(action) + } else { + action(child) + } + } + } + /** Traversals preference hierarchy and applies given action. */ suspend fun forEachAsync(action: suspend (PreferenceHierarchyNode) -> Unit) { for (it in children) action(it) @@ -157,18 +169,7 @@ class PreferenceHierarchy internal constructor(metadata: PreferenceMetadata) : /** Returns all the [PreferenceHierarchyNode]s appear in the hierarchy. */ fun getAllPreferences(): List<PreferenceHierarchyNode> = - mutableListOf<PreferenceHierarchyNode>().also { getAllPreferences(it) } - - private fun getAllPreferences(result: MutableList<PreferenceHierarchyNode>) { - result.add(this) - for (child in children) { - if (child is PreferenceHierarchy) { - child.getAllPreferences(result) - } else { - result.add(child) - } - } - } + mutableListOf<PreferenceHierarchyNode>().apply { forEachRecursively { add(it) } } } /** diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt index 41a626fe8efa..991d5b7791e9 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt @@ -32,7 +32,7 @@ import com.android.settingslib.widget.SettingsBasePreferenceFragment open class PreferenceFragment : SettingsBasePreferenceFragment(), PreferenceScreenProvider, PreferenceScreenBindingKeyProvider { - private var preferenceScreenBindingHelper: PreferenceScreenBindingHelper? = null + protected var preferenceScreenBindingHelper: PreferenceScreenBindingHelper? = null override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceScreen = createPreferenceScreen() @@ -129,7 +129,9 @@ open class PreferenceFragment : } protected fun getPreferenceKeysInHierarchy(): Set<String> = - preferenceScreenBindingHelper?.getPreferences()?.map { it.metadata.key }?.toSet() ?: setOf() + preferenceScreenBindingHelper?.let { + mutableSetOf<String>().apply { it.forEachRecursively { add(it.metadata.key) } } + } ?: setOf() companion object { private const val TAG = "PreferenceFragment" diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt index 022fb1dbe99c..fbe892710d40 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt @@ -143,7 +143,8 @@ class PreferenceScreenBindingHelper( } } - fun getPreferences() = preferenceHierarchy.getAllPreferences() + fun forEachRecursively(action: (PreferenceHierarchyNode) -> Unit) = + preferenceHierarchy.forEachRecursively(action) fun onCreate() { for (preference in lifecycleAwarePreferences) { @@ -191,11 +192,11 @@ class PreferenceScreenBindingHelper( companion object { /** Preference value is changed. */ - private const val CHANGE_REASON_VALUE = 0 + const val CHANGE_REASON_VALUE = 0 /** Preference state (title/summary, enable state, etc.) is changed. */ - private const val CHANGE_REASON_STATE = 1 + const val CHANGE_REASON_STATE = 1 /** Dependent preference state is changed. */ - private const val CHANGE_REASON_DEPENDENT = 2 + const val CHANGE_REASON_DEPENDENT = 2 /** Updates preference screen that has incomplete hierarchy. */ @JvmStatic diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_preference_icon_frame.xml b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_preference_icon_frame.xml index ccdf37d452b0..0cd0b3cb14f1 100644 --- a/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_preference_icon_frame.xml +++ b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_preference_icon_frame.xml @@ -22,7 +22,7 @@ android:minWidth="@dimen/settingslib_expressive_space_medium3" android:minHeight="@dimen/settingslib_expressive_space_medium3" android:gravity="center" - android:layout_marginEnd="-8dp" + android:layout_marginEnd="-4dp" android:filterTouchesWhenObscured="false"> <androidx.preference.internal.PreferenceImageView diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_preference_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_preference_expressive.xml index 3c69027c2080..cec8e45e2bfb 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_preference_expressive.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_preference_expressive.xml @@ -36,33 +36,34 @@ <style name="SettingsLibPreference.SwitchPreference" parent="SettingsSwitchPreference.SettingsLib"/> <style name="SettingsLibPreference.Expressive"> - <item name="android:layout">@layout/settingslib_expressive_preference</item> + <item name="layout">@layout/settingslib_expressive_preference</item> </style> <style name="SettingsLibPreference.Category.Expressive"> </style> <style name="SettingsLibPreference.CheckBoxPreference.Expressive"> - <item name="android:layout">@layout/settingslib_expressive_preference</item> + <item name="layout">@layout/settingslib_expressive_preference</item> </style> <style name="SettingsLibPreference.SwitchPreferenceCompat.Expressive"> - <item name="android:layout">@layout/settingslib_expressive_preference</item> + <item name="layout">@layout/settingslib_expressive_preference</item> <item name="android:widgetLayout">@layout/settingslib_expressive_preference_switch</item> </style> <style name="SettingsLibPreference.SeekBarPreference.Expressive"/> <style name="SettingsLibPreference.PreferenceScreen.Expressive"> - <item name="android:layout">@layout/settingslib_expressive_preference</item> + <item name="layout">@layout/settingslib_expressive_preference</item> </style> <style name="SettingsLibPreference.DialogPreference.Expressive"> + <item name="layout">@layout/settingslib_expressive_preference</item> </style> <style name="SettingsLibPreference.DialogPreference.EditTextPreference.Expressive"> - <item name="android:layout">@layout/settingslib_expressive_preference</item> - <item name="android:dialogLayout">@layout/settingslib_preference_dialog_edittext</item> + <item name="layout">@layout/settingslib_expressive_preference</item> + <item name="dialogLayout">@layout/settingslib_preference_dialog_edittext</item> </style> <style name="SettingsLibPreference.DropDown.Expressive"> 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 edd49c5a8fb7..0209eb8c3fbf 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 @@ -21,6 +21,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection +import android.os.DeadObjectException import android.os.IBinder import android.os.IInterface import android.os.RemoteException @@ -52,6 +53,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filterIsInstance @@ -304,6 +306,14 @@ class DeviceSettingServiceConnection( service.registerDeviceSettingsListener(deviceInfo, listener) awaitClose { service.unregisterDeviceSettingsListener(deviceInfo, listener) } } + .catch { e -> + if (e is DeadObjectException) { + Log.e(TAG, "DeadObjectException happens when registering listener.", e) + emit(listOf()) + } else { + throw e + } + } .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 064198fc5e46..927a1c59cc76 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -284,5 +284,6 @@ public class SecureSettings { Settings.Secure.MANDATORY_BIOMETRICS, Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED, Settings.Secure.ADVANCED_PROTECTION_MODE, + Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS, }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index c002a04d5b11..6d73ee27f076 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -332,6 +332,9 @@ public class SecureSettingsValidators { VALIDATORS.put( Secure.ACCESSIBILITY_QS_TARGETS, ACCESSIBILITY_SHORTCUT_TARGET_LIST_VALIDATOR); + VALIDATORS.put( + Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS, + ACCESSIBILITY_SHORTCUT_TARGET_LIST_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ONE_HANDED_MODE_ACTIVATED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ONE_HANDED_MODE_ENABLED, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 2034f36c558b..fb0aaf8e5ae1 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -1823,6 +1823,9 @@ class SettingsProtoDumpUtil { Settings.Secure.ACCESSIBILITY_QS_TARGETS, SecureSettingsProto.Accessibility.QS_TARGETS); dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS, + SecureSettingsProto.Accessibility.ACCESSIBILITY_KEY_GESTURE_TARGETS); + dumpSetting(s, p, Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY, SecureSettingsProto.Accessibility.ACCESSIBILITY_MAGNIFICATION_CAPABILITY); dumpSetting(s, p, diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 0724410a2954..7b6321d1cc7d 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -962,6 +962,10 @@ <!-- Permission required for ExecutableMethodFileOffsetsTest --> <uses-permission android:name="android.permission.DYNAMIC_INSTRUMENTATION" /> + <!-- Permissions required for CTS test - SettingsPreferenceServiceClientTest --> + <uses-permission android:name="android.permission.READ_SYSTEM_PREFERENCES" /> + <uses-permission android:name="android.permission.WRITE_SYSTEM_PREFERENCES" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 207ed71c955d..3df96030d221 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -267,7 +267,7 @@ flag { flag { name: "dual_shade" namespace: "systemui" - description: "Enables the BC25 Dual Shade (go/bc25-dual-shade-design)." + description: "Enables Dual Shade (go/dual-shade-design-doc)." bug: "337259436" } @@ -1360,16 +1360,6 @@ flag { } flag { - name: "notification_pulsing_fix" - namespace: "systemui" - description: "Allow showing new pulsing notifications when the device is already pulsing." - bug: "335560575" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "media_lockscreen_launch_animation" namespace : "systemui" description : "Enable the origin launch animation for UMO when opening on top of lockscreen." @@ -1784,3 +1774,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "keyguard_transition_force_finish_on_screen_off" + namespace: "systemui" + description: "Forces KTF transitions to finish if the screen turns all the way off." + bug: "331636736" + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml new file mode 100644 index 000000000000..651e401681c6 --- /dev/null +++ b/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml @@ -0,0 +1,19 @@ +<?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> + <dimen name="keyguard_smartspace_top_offset">0dp</dimen> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/customization/res/values-sw600dp/dimens.xml b/packages/SystemUI/customization/res/values-sw600dp/dimens.xml new file mode 100644 index 000000000000..10e630d44488 --- /dev/null +++ b/packages/SystemUI/customization/res/values-sw600dp/dimens.xml @@ -0,0 +1,20 @@ +<?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> + <!-- For portrait direction in unfold foldable device, we don't need keyguard_smartspace_top_offset--> + <dimen name="keyguard_smartspace_top_offset">0dp</dimen> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/customization/res/values/dimens.xml b/packages/SystemUI/customization/res/values/dimens.xml index c574d1fc674b..7feea6e5e8dd 100644 --- a/packages/SystemUI/customization/res/values/dimens.xml +++ b/packages/SystemUI/customization/res/values/dimens.xml @@ -33,4 +33,10 @@ <dimen name="small_clock_height">114dp</dimen> <dimen name="small_clock_padding_top">28dp</dimen> <dimen name="clock_padding_start">28dp</dimen> + + <!-- When large clock is showing, offset the smartspace by this amount --> + <dimen name="keyguard_smartspace_top_offset">12dp</dimen> + <!--Dimens used in both lockscreen preview and smartspace --> + <dimen name="date_weather_view_height">24dp</dimen> + <dimen name="enhanced_smartspace_height">104dp</dimen> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt index a4782acaed9b..ee21ea6ee126 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt @@ -55,10 +55,7 @@ class FlexClockFaceController( override val view: View get() = layerController.view - override val config = - ClockFaceConfig( - hasCustomPositionUpdatedAnimation = false // TODO(b/364673982) - ) + override val config = ClockFaceConfig(hasCustomPositionUpdatedAnimation = true) override var theme = ThemeConfig(true, assets.seedColor) @@ -96,6 +93,19 @@ class FlexClockFaceController( layerController.view.layoutParams = lp } + /** See documentation at [FlexClockView.offsetGlyphsForStepClockAnimation]. */ + private fun offsetGlyphsForStepClockAnimation( + clockStartLeft: Int, + direction: Int, + fraction: Float + ) { + (view as? FlexClockView)?.offsetGlyphsForStepClockAnimation( + clockStartLeft, + direction, + fraction, + ) + } + override val layout: ClockFaceLayout = DefaultClockFaceLayout(view).apply { views[0].id = @@ -248,10 +258,12 @@ class FlexClockFaceController( override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) { layerController.animations.onPositionUpdated(fromLeft, direction, fraction) + if (isLargeClock) offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction) } override fun onPositionUpdated(distance: Float, fraction: Float) { layerController.animations.onPositionUpdated(distance, fraction) + // TODO(b/378128811) port stepping animation } } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt index d86c0d664590..593eba9d05cc 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt @@ -19,6 +19,7 @@ package com.android.systemui.shared.clocks.view import android.content.Context import android.graphics.Canvas import android.graphics.Point +import android.util.MathUtils.constrainedMap import android.view.View import android.view.ViewGroup import android.widget.RelativeLayout @@ -50,6 +51,8 @@ class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: Me ) } + private val digitOffsets = mutableMapOf<Int, Float>() + override fun addView(child: View?) { super.addView(child) (child as SimpleDigitalClockTextView).digitTranslateAnimator = @@ -76,7 +79,7 @@ class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: Me digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitSize.x, 0) digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitSize.y) digitLeftTopMap[R.id.MINUTE_SECOND_DIGIT] = Point(maxSingleDigitSize) - digitLeftTopMap.forEach { _, point -> + digitLeftTopMap.forEach { (_, point) -> point.x += abs(aodTranslate.x) point.y += abs(aodTranslate.y) } @@ -89,11 +92,17 @@ class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: Me override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - digitalClockTextViewMap.forEach { (id, _) -> - val textView = digitalClockTextViewMap[id]!! - canvas.translate(digitLeftTopMap[id]!!.x.toFloat(), digitLeftTopMap[id]!!.y.toFloat()) + digitalClockTextViewMap.forEach { (id, textView) -> + // save canvas location in anticipation of restoration later + canvas.save() + val xTranslateAmount = + digitOffsets.getOrDefault(id, 0f) + digitLeftTopMap[id]!!.x.toFloat() + // move canvas to location that the textView would like + canvas.translate(xTranslateAmount, digitLeftTopMap[id]!!.y.toFloat()) + // draw the textView at the location of the canvas above textView.draw(canvas) - canvas.translate(-digitLeftTopMap[id]!!.x.toFloat(), -digitLeftTopMap[id]!!.y.toFloat()) + // reset the canvas location back to 0 without drawing + canvas.restore() } } @@ -157,10 +166,108 @@ class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: Me } } + /** + * Offsets the textViews of the clock for the step clock animation. + * + * The animation makes the textViews of the clock move at different speeds, when the clock is + * moving horizontally. + * + * @param clockStartLeft the [getLeft] position of the clock, before it started moving. + * @param clockMoveDirection the direction in which it is moving. A positive number means right, + * and negative means left. + * @param moveFraction fraction of the clock movement. 0 means it is at the beginning, and 1 + * means it finished moving. + */ + fun offsetGlyphsForStepClockAnimation( + clockStartLeft: Int, + clockMoveDirection: Int, + moveFraction: Float, + ) { + val isMovingToCenter = if (isLayoutRtl) clockMoveDirection < 0 else clockMoveDirection > 0 + // The sign of moveAmountDeltaForDigit is already set here + // we can interpret (left - clockStartLeft) as (destinationPosition - originPosition) + // so we no longer need to multiply direct sign to moveAmountDeltaForDigit + val currentMoveAmount = left - clockStartLeft + for (i in 0 until NUM_DIGITS) { + val mapIndexToId = + when (i) { + 0 -> R.id.HOUR_FIRST_DIGIT + 1 -> R.id.HOUR_SECOND_DIGIT + 2 -> R.id.MINUTE_FIRST_DIGIT + 3 -> R.id.MINUTE_SECOND_DIGIT + else -> -1 + } + val digitFraction = + getDigitFraction( + digit = i, + isMovingToCenter = isMovingToCenter, + fraction = moveFraction, + ) + // left here is the final left position after the animation is done + val moveAmountForDigit = currentMoveAmount * digitFraction + var moveAmountDeltaForDigit = moveAmountForDigit - currentMoveAmount + if (isMovingToCenter && moveAmountForDigit < 0) moveAmountDeltaForDigit *= -1 + digitOffsets[mapIndexToId] = moveAmountDeltaForDigit + invalidate() + } + } + + private val moveToCenterDelays: List<Int> + get() = if (isLayoutRtl) MOVE_LEFT_DELAYS else MOVE_RIGHT_DELAYS + + private val moveToSideDelays: List<Int> + get() = if (isLayoutRtl) MOVE_RIGHT_DELAYS else MOVE_LEFT_DELAYS + + private fun getDigitFraction(digit: Int, isMovingToCenter: Boolean, fraction: Float): Float { + // The delay for the digit, in terms of fraction. + // (i.e. the digit should not move during 0.0 - 0.1). + val delays = if (isMovingToCenter) moveToCenterDelays else moveToSideDelays + val digitInitialDelay = delays[digit] * MOVE_DIGIT_STEP + return MOVE_INTERPOLATOR.getInterpolation( + constrainedMap( + /* rangeMin= */ 0.0f, + /* rangeMax= */ 1.0f, + /* valueMin= */ digitInitialDelay, + /* valueMax= */ digitInitialDelay + AVAILABLE_ANIMATION_TIME, + /* value= */ fraction, + ) + ) + } + companion object { val AOD_TRANSITION_DURATION = 750L val CHARGING_TRANSITION_DURATION = 300L + // Calculate the positions of all of the digits... + // Offset each digit by, say, 0.1 + // This means that each digit needs to move over a slice of "fractions", i.e. digit 0 should + // move from 0.0 - 0.7, digit 1 from 0.1 - 0.8, digit 2 from 0.2 - 0.9, and digit 3 + // from 0.3 - 1.0. + private const val NUM_DIGITS = 4 + + // Delays. Each digit's animation should have a slight delay, so we get a nice + // "stepping" effect. When moving right, the second digit of the hour should move first. + // When moving left, the first digit of the hour should move first. The lists encode + // the delay for each digit (hour[0], hour[1], minute[0], minute[1]), to be multiplied + // by delayMultiplier. + private val MOVE_LEFT_DELAYS = listOf(0, 1, 2, 3) + private val MOVE_RIGHT_DELAYS = listOf(1, 0, 3, 2) + + // How much delay to apply to each subsequent digit. This is measured in terms of "fraction" + // (i.e. a value of 0.1 would cause a digit to wait until fraction had hit 0.1, or 0.2 etc + // before moving). + // + // The current specs dictate that each digit should have a 33ms gap between them. The + // overall time is 1s right now. + private const val MOVE_DIGIT_STEP = 0.033f + + // Constants for the animation + private val MOVE_INTERPOLATOR = Interpolators.EMPHASIZED + + // Total available transition time for each digit, taking into account the step. If step is + // 0.1, then digit 0 would animate over 0.0 - 0.7, making availableTime 0.7. + private const val AVAILABLE_ANIMATION_TIME = 1.0f - MOVE_DIGIT_STEP * (NUM_DIGITS - 1) + // Use the sign of targetTranslation to control the direction of digit translation fun updateDirectionalTargetTranslate(id: Int, targetTranslation: Point): Point { val outPoint = Point(targetTranslation) @@ -169,17 +276,14 @@ class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: Me outPoint.x *= -1 outPoint.y *= -1 } - R.id.HOUR_SECOND_DIGIT -> { outPoint.x *= 1 outPoint.y *= -1 } - R.id.MINUTE_FIRST_DIGIT -> { outPoint.x *= -1 outPoint.y *= 1 } - R.id.MINUTE_SECOND_DIGIT -> { outPoint.x *= 1 outPoint.y *= 1 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt index e1421691a92d..58fe2c9cbe57 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt @@ -36,6 +36,7 @@ import org.mockito.junit.MockitoJUnit private const val USER_ID = 22 private const val OWNER_ID = 10 +private const val PASSWORD_ID = 30 private const val OPERATION_ID = 100L private const val MAX_ATTEMPTS = 5 @@ -247,7 +248,11 @@ class CredentialInteractorImplTest : SysuiTestCase() { private fun pinRequest(credentialOwner: Int = USER_ID): BiometricPromptRequest.Credential.Pin = BiometricPromptRequest.Credential.Pin( promptInfo(), - BiometricUserInfo(userId = USER_ID, deviceCredentialOwnerId = credentialOwner), + BiometricUserInfo( + userId = USER_ID, + deviceCredentialOwnerId = credentialOwner, + userIdForPasswordEntry = PASSWORD_ID, + ), BiometricOperationInfo(OPERATION_ID), ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt index 81d3f7232c78..f0d79bb83652 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt @@ -17,6 +17,8 @@ package com.android.systemui.deviceentry.domain.interactor import android.content.pm.UserInfo +import android.os.PowerManager +import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils @@ -31,6 +33,7 @@ import com.android.systemui.flags.fakeSystemPropertiesHelper import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeTrustRepository import com.android.systemui.keyguard.shared.model.AuthenticationFlags import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus @@ -38,12 +41,18 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.user.domain.interactor.selectedUserInteractor +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.map import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -225,6 +234,8 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { @Test fun deviceUnlockStatus_isResetToFalse_whenDeviceGoesToSleep() = testScope.runTest { + setLockAfterScreenTimeout(0) + kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = false val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( @@ -240,8 +251,52 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { } @Test + fun deviceUnlockStatus_isResetToFalse_whenDeviceGoesToSleep_afterDelay() = + testScope.runTest { + val delay = 5000 + setLockAfterScreenTimeout(delay) + kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = false + val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + + kosmos.powerInteractor.setAsleepForTest() + runCurrent() + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + + advanceTimeBy(delay.toLong()) + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + } + + @Test + fun deviceUnlockStatus_isResetToFalse_whenDeviceGoesToSleep_powerButtonLocksInstantly() = + testScope.runTest { + setLockAfterScreenTimeout(5000) + kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = true + val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + + kosmos.powerInteractor.setAsleepForTest( + sleepReason = PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON + ) + runCurrent() + + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + } + + @Test fun deviceUnlockStatus_becomesUnlocked_whenFingerprintUnlocked_whileDeviceAsleep() = testScope.runTest { + setLockAfterScreenTimeout(0) val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) assertThat(deviceUnlockStatus?.isUnlocked).isFalse() @@ -450,6 +505,98 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate) } + @Test + fun deviceUnlockStatus_locksImmediately_whenDreamStarts_noTimeout() = + testScope.runTest { + setLockAfterScreenTimeout(0) + val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked }) + unlockDevice() + + startDreaming() + + assertThat(isUnlocked).isFalse() + } + + @Test + fun deviceUnlockStatus_locksWithDelay_afterDreamStarts_withTimeout() = + testScope.runTest { + val delay = 5000 + setLockAfterScreenTimeout(delay) + val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked }) + unlockDevice() + + startDreaming() + assertThat(isUnlocked).isTrue() + + advanceTimeBy(delay - 1L) + assertThat(isUnlocked).isTrue() + + advanceTimeBy(1L) + assertThat(isUnlocked).isFalse() + } + + @Test + fun deviceUnlockStatus_doesNotLockWithDelay_whenDreamStopsBeforeTimeout() = + testScope.runTest { + val delay = 5000 + setLockAfterScreenTimeout(delay) + val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked }) + unlockDevice() + + startDreaming() + assertThat(isUnlocked).isTrue() + + advanceTimeBy(delay - 1L) + assertThat(isUnlocked).isTrue() + + stopDreaming() + assertThat(isUnlocked).isTrue() + + advanceTimeBy(1L) + assertThat(isUnlocked).isTrue() + } + + @Test + fun deviceUnlockStatus_doesNotLock_whenDreamStarts_ifNotInteractive() = + testScope.runTest { + setLockAfterScreenTimeout(0) + val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked }) + unlockDevice() + + startDreaming() + + assertThat(isUnlocked).isFalse() + } + + private fun TestScope.unlockDevice() { + val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + kosmos.sceneInteractor.changeScene(Scenes.Gone, "reason") + runCurrent() + } + + private fun setLockAfterScreenTimeout(timeoutMs: Int) { + kosmos.fakeSettings.putIntForUser( + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + timeoutMs, + kosmos.selectedUserInteractor.getSelectedUserId(), + ) + } + + private fun TestScope.startDreaming() { + kosmos.fakeKeyguardRepository.setDreaming(true) + runCurrent() + } + + private fun TestScope.stopDreaming() { + kosmos.fakeKeyguardRepository.setDreaming(false) + runCurrent() + } + private fun TestScope.verifyRestrictionReasonsForAuthFlags( vararg authFlagToDeviceEntryRestriction: Pair<Int, DeviceEntryRestrictionReason?> ) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt index bfe89de6229d..3d5498b61471 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt @@ -16,11 +16,14 @@ package com.android.systemui.keyguard.data.repository +import android.animation.Animator import android.animation.ValueAnimator +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.FlakyTest import androidx.test.filters.SmallTest import com.android.app.animation.Interpolators +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues import com.android.systemui.keyguard.shared.model.KeyguardState @@ -41,6 +44,8 @@ import com.google.common.truth.Truth.assertThat import java.math.BigDecimal import java.math.RoundingMode import java.util.UUID +import kotlin.test.assertEquals +import kotlin.test.assertTrue import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.dropWhile @@ -53,6 +58,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock @SmallTest @RunWith(AndroidJUnit4::class) @@ -65,6 +71,8 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { private lateinit var underTest: KeyguardTransitionRepository private lateinit var runner: KeyguardTransitionRunner + private val animatorListener = mock<Animator.AnimatorListener>() + @Before fun setUp() { underTest = KeyguardTransitionRepositoryImpl(Dispatchers.Main) @@ -80,7 +88,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { runner.startTransition( this, TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()), - maxFrames = 100 + maxFrames = 100, ) assertSteps(steps, listWithStep(BigDecimal(.1)), AOD, LOCKSCREEN) @@ -107,7 +115,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { LOCKSCREEN, AOD, getAnimator(), - TransitionModeOnCanceled.LAST_VALUE + TransitionModeOnCanceled.LAST_VALUE, ), ) @@ -142,7 +150,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { LOCKSCREEN, AOD, getAnimator(), - TransitionModeOnCanceled.RESET + TransitionModeOnCanceled.RESET, ), ) @@ -177,7 +185,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { LOCKSCREEN, AOD, getAnimator(), - TransitionModeOnCanceled.REVERSE + TransitionModeOnCanceled.REVERSE, ), ) @@ -476,6 +484,49 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { assertThat(steps.size).isEqualTo(3) } + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_TRANSITION_FORCE_FINISH_ON_SCREEN_OFF) + fun forceFinishCurrentTransition_noFurtherStepsEmitted() = + testScope.runTest { + val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF }) + + var sentForceFinish = false + + runner.startTransition( + this, + TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()), + maxFrames = 100, + // Force-finish on the second frame. + frameCallback = { frameNumber -> + if (!sentForceFinish && frameNumber > 1) { + testScope.launch { underTest.forceFinishCurrentTransition() } + sentForceFinish = true + } + }, + ) + + val lastTwoRunningSteps = + steps.filter { it.transitionState == TransitionState.RUNNING }.takeLast(2) + + // Make sure we stopped emitting RUNNING steps early, but then emitted a final 1f step. + assertTrue(lastTwoRunningSteps[0].value < 0.5f) + assertTrue(lastTwoRunningSteps[1].value == 1f) + + assertEquals(steps.last().from, AOD) + assertEquals(steps.last().to, LOCKSCREEN) + assertEquals(steps.last().transitionState, TransitionState.FINISHED) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_TRANSITION_FORCE_FINISH_ON_SCREEN_OFF) + fun forceFinishCurrentTransition_noTransitionStarted_noStepsEmitted() = + testScope.runTest { + val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF }) + + underTest.forceFinishCurrentTransition() + assertEquals(0, steps.size) + } + private fun listWithStep( step: BigDecimal, start: BigDecimal = BigDecimal.ZERO, @@ -505,7 +556,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { to, fractions[0].toFloat(), TransitionState.STARTED, - OWNER_NAME + OWNER_NAME, ) ) fractions.forEachIndexed { index, fraction -> @@ -519,7 +570,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { to, fraction.toFloat(), TransitionState.RUNNING, - OWNER_NAME + OWNER_NAME, ) ) } @@ -538,6 +589,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) setDuration(10) + addListener(animatorListener) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt index 8914c80cdd5e..ae2a5c5fe501 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt @@ -34,6 +34,7 @@ package com.android.systemui.keyguard.domain.interactor +import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -43,6 +44,7 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -60,10 +62,13 @@ import com.android.systemui.scene.data.repository.setSceneTransition import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor import com.android.systemui.testKosmos +import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -96,7 +101,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { from = KeyguardState.LOCKSCREEN, to = KeyguardState.AOD, testScope = testScope, - throughTransitionState = TransitionState.RUNNING + throughTransitionState = TransitionState.RUNNING, ) powerInteractor.onCameraLaunchGestureDetected() @@ -134,7 +139,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { from = KeyguardState.AOD, to = KeyguardState.LOCKSCREEN, testScope = testScope, - throughTransitionState = TransitionState.RUNNING + throughTransitionState = TransitionState.RUNNING, ) powerInteractor.onCameraLaunchGestureDetected() @@ -182,21 +187,12 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true) runCurrent() - assertThat(values) - .containsExactly( - false, - true, - ) + assertThat(values).containsExactly(false, true) kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(false) runCurrent() - assertThat(values) - .containsExactly( - false, - true, - false, - ) + assertThat(values).containsExactly(false, true, false) kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true) runCurrent() @@ -228,7 +224,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { from = KeyguardState.GONE, to = KeyguardState.AOD, testScope = testScope, - throughTransitionState = TransitionState.RUNNING + throughTransitionState = TransitionState.RUNNING, ) powerInteractor.onCameraLaunchGestureDetected() @@ -242,10 +238,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { testScope = testScope, ) - assertThat(values) - .containsExactly( - false, - ) + assertThat(values).containsExactly(false) } @Test @@ -263,7 +256,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { from = KeyguardState.UNDEFINED, to = KeyguardState.AOD, testScope = testScope, - throughTransitionState = TransitionState.RUNNING + throughTransitionState = TransitionState.RUNNING, ) powerInteractor.onCameraLaunchGestureDetected() @@ -278,10 +271,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { testScope = testScope, ) - assertThat(values) - .containsExactly( - false, - ) + assertThat(values).containsExactly(false) } @Test @@ -304,8 +294,19 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { assertThat(occludingActivityWillDismissKeyguard).isTrue() // Re-lock device: - kosmos.powerInteractor.setAsleepForTest() - runCurrent() + lockDevice() assertThat(occludingActivityWillDismissKeyguard).isFalse() } + + private suspend fun TestScope.lockDevice() { + kosmos.powerInteractor.setAsleepForTest() + advanceTimeBy( + kosmos.userAwareSecureSettingsRepository + .getInt( + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT, + ) + .toLong() + ) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt index ecc62e908a4f..87ab3c89a671 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt @@ -69,7 +69,9 @@ class ClockSectionTest : SysuiTestCase() { get() = kosmos.fakeSystemBarUtilsProxy.getStatusBarHeight() + context.resources.getDimensionPixelSize(customR.dimen.small_clock_padding_top) + - context.resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) + context.resources.getDimensionPixelSize( + customR.dimen.keyguard_smartspace_top_offset + ) private val LARGE_CLOCK_TOP get() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt index 1abb441439fe..5798e0776c4f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt @@ -21,6 +21,7 @@ import android.animation.ValueAnimator import android.view.Choreographer.FrameCallback import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.TransitionInfo +import java.util.function.Consumer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -35,9 +36,8 @@ import org.junit.Assert.fail * Gives direct control over ValueAnimator, in order to make transition tests deterministic. See * [AnimationHandler]. Animators are required to be run on the main thread, so dispatch accordingly. */ -class KeyguardTransitionRunner( - val repository: KeyguardTransitionRepository, -) : AnimationFrameCallbackProvider { +class KeyguardTransitionRunner(val repository: KeyguardTransitionRepository) : + AnimationFrameCallbackProvider { private var frameCount = 1L private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null)) @@ -48,7 +48,12 @@ class KeyguardTransitionRunner( * For transitions being directed by an animator. Will control the number of frames being * generated so the values are deterministic. */ - suspend fun startTransition(scope: CoroutineScope, info: TransitionInfo, maxFrames: Int = 100) { + suspend fun startTransition( + scope: CoroutineScope, + info: TransitionInfo, + maxFrames: Int = 100, + frameCallback: Consumer<Long>? = null, + ) { // AnimationHandler uses ThreadLocal storage, and ValueAnimators MUST start from main // thread withContext(Dispatchers.Main) { @@ -62,7 +67,12 @@ class KeyguardTransitionRunner( isTerminated = frameNumber >= maxFrames if (!isTerminated) { - withContext(Dispatchers.Main) { callback?.doFrame(frameNumber) } + try { + withContext(Dispatchers.Main) { callback?.doFrame(frameNumber) } + frameCallback?.accept(frameNumber) + } catch (e: IllegalStateException) { + e.printStackTrace() + } } } } @@ -90,9 +100,13 @@ class KeyguardTransitionRunner( override fun postFrameCallback(cb: FrameCallback) { frames.value = Pair(frameCount++, cb) } + override fun postCommitCallback(runnable: Runnable) {} + override fun getFrameTime() = frameCount + override fun getFrameDelay() = 1L + override fun setFrameDelay(delay: Long) {} companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt index 8e67e602abd9..f8f6fe246563 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt @@ -38,6 +38,7 @@ import com.android.systemui.media.controls.util.fakeMediaControllerFactory import com.android.systemui.media.controls.util.fakeSessionTokenFactory import com.android.systemui.res.R import com.android.systemui.testKosmos +import com.android.systemui.util.concurrency.execution import com.google.common.collect.ImmutableList import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runCurrent @@ -105,6 +106,7 @@ class Media3ActionFactoryTest : SysuiTestCase() { kosmos.looper, handler, kosmos.testScope, + kosmos.execution, ) controllerFactory.setMedia3Controller(media3Controller) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt index 3910903af4aa..ae7c44e9b146 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt @@ -35,7 +35,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class EditTileListStateTest : SysuiTestCase() { - private val underTest = EditTileListState(TestEditTiles, 4) + private val underTest = EditTileListState(TestEditTiles, columns = 4, largeTilesSpan = 2) @Test fun startDrag_listHasSpacers() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java index 0729e2fcd35f..03c1f92aad4c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java @@ -93,6 +93,8 @@ public class QuickAccessWalletTileTest extends SysuiTestCase { private static final String CARD_DESCRIPTION = "•••• 1234"; private static final Icon CARD_IMAGE = Icon.createWithBitmap(Bitmap.createBitmap(70, 50, Bitmap.Config.ARGB_8888)); + private static final Icon INVALID_CARD_IMAGE = + Icon.createWithContentUri("content://media/external/images/media"); private static final int PRIMARY_USER_ID = 0; private static final int SECONDARY_USER_ID = 10; @@ -444,6 +446,14 @@ public class QuickAccessWalletTileTest extends SysuiTestCase { } @Test + public void testQueryCards_invalidDrawable_noSideViewDrawable() { + when(mKeyguardStateController.isUnlocked()).thenReturn(true); + setUpInvalidWalletCard(/* hasCard= */ true); + + assertNull(mTile.getState().sideViewCustomDrawable); + } + + @Test public void testQueryCards_error_notUpdateSideViewDrawable() { String errorMessage = "getWalletCardsError"; GetWalletCardsError error = new GetWalletCardsError(CARD_IMAGE, errorMessage); @@ -503,9 +513,33 @@ public class QuickAccessWalletTileTest extends SysuiTestCase { mTestableLooper.processAllMessages(); } + private void setUpInvalidWalletCard(boolean hasCard) { + GetWalletCardsResponse response = + new GetWalletCardsResponse( + hasCard + ? Collections.singletonList(createInvalidWalletCard(mContext)) + : Collections.EMPTY_LIST, 0); + + mTile.handleSetListening(true); + + verify(mController).queryWalletCards(mCallbackCaptor.capture()); + + mCallbackCaptor.getValue().onWalletCardsRetrieved(response); + mTestableLooper.processAllMessages(); + } + private WalletCard createWalletCard(Context context) { PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE); return new WalletCard.Builder(CARD_ID, CARD_IMAGE, CARD_DESCRIPTION, pendingIntent).build(); } + + private WalletCard createInvalidWalletCard(Context context) { + PendingIntent pendingIntent = + PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE); + return new WalletCard.Builder( + CARD_ID, INVALID_CARD_IMAGE, CARD_DESCRIPTION, pendingIntent).build(); + } + + } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 3be8a380b191..b5f005cdc706 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.scene +import android.provider.Settings import android.telephony.TelephonyManager import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -42,6 +43,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.ui.viewmodel.lockscreenUserActionsViewModel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue @@ -64,6 +66,7 @@ import com.android.systemui.telephony.data.repository.fakeTelephonyRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository import com.android.telecom.mockTelecomManager import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage @@ -72,6 +75,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import org.junit.Before import org.junit.Test @@ -541,7 +545,14 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { .isTrue() powerInteractor.setAsleepForTest() - testScope.runCurrent() + testScope.advanceTimeBy( + kosmos.userAwareSecureSettingsRepository + .getInt( + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT, + ) + .toLong() + ) powerInteractor.setAwakeForTest() testScope.runCurrent() @@ -631,14 +642,25 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { } /** Changes device wakefulness state from awake to asleep, going through intermediary states. */ - private fun Kosmos.putDeviceToSleep() { + private suspend fun Kosmos.putDeviceToSleep(waitForLock: Boolean = true) { val wakefulnessModel = powerInteractor.detailedWakefulness.value assertWithMessage("Cannot put device to sleep as it's already asleep!") .that(wakefulnessModel.isAwake()) .isTrue() powerInteractor.setAsleepForTest() - testScope.runCurrent() + if (waitForLock) { + testScope.advanceTimeBy( + kosmos.userAwareSecureSettingsRepository + .getInt( + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT, + ) + .toLong() + ) + } else { + testScope.runCurrent() + } } /** Emulates the dismissal of the IME (soft keyboard). */ diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 08b996146f2c..152911a30524 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -23,6 +23,7 @@ import android.hardware.face.FaceManager import android.os.PowerManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.provider.Settings import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -59,6 +60,7 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.haptics.msdl.fakeMSDLPlayer import com.android.systemui.haptics.vibratorHelper import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository +import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository @@ -106,6 +108,7 @@ import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvision import com.android.systemui.statusbar.sysuiStatusBarStateController import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock +import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository import com.google.android.msdl.data.model.MSDLToken import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -1339,7 +1342,14 @@ class SceneContainerStartableTest : SysuiTestCase() { // Putting the device to sleep to lock it again, which shouldn't report another // successful unlock. kosmos.powerInteractor.setAsleepForTest() - runCurrent() + advanceTimeBy( + kosmos.userAwareSecureSettingsRepository + .getInt( + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT, + ) + .toLong() + ) // Verify that the startable changed the scene to Lockscreen because the device locked // following the sleep. assertThat(currentScene).isEqualTo(Scenes.Lockscreen) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java index ea5c29ef30aa..3ad41a54ac7e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.collection.coordinator; +import static com.android.systemui.flags.SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag; + import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertFalse; @@ -32,9 +34,9 @@ import static org.mockito.Mockito.when; import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.FlagsParameterization; import android.testing.TestableLooper; -import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.compose.animation.scene.ObservableTransitionState; @@ -42,6 +44,7 @@ import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.communal.shared.model.CommunalScenes; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.BrokenWithSceneContainer; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.keyguard.shared.model.TransitionState; @@ -78,14 +81,23 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.verification.VerificationMode; +import java.util.List; + import kotlinx.coroutines.flow.MutableStateFlow; import kotlinx.coroutines.test.TestScope; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper public class VisualStabilityCoordinatorTest extends SysuiTestCase { + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return parameterizeSceneContainerFlag(); + } + private VisualStabilityCoordinator mCoordinator; @Mock private DumpManager mDumpManager; @@ -117,6 +129,11 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { private NotificationEntry mEntry; private GroupEntry mGroupEntry; + public VisualStabilityCoordinatorTest(FlagsParameterization flags) { + super(); + mSetFlagsRule.setFlagsParameterization(flags); + } + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -251,6 +268,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { } @Test + @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer public void testLockscreenPartlyShowing_groupAndSectionChangesNotAllowed() { // GIVEN the panel true expanded and device isn't pulsing setFullyDozed(false); @@ -267,6 +285,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { } @Test + @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer public void testLockscreenFullyShowing_groupAndSectionChangesNotAllowed() { // GIVEN the panel true expanded and device isn't pulsing setFullyDozed(false); @@ -520,6 +539,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { @Test @EnableFlags(Flags.FLAG_CHECK_LOCKSCREEN_GONE_TRANSITION) + @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer public void testNotLockscreenInGoneTransition_invalidationCalled() { // GIVEN visual stability is being maintained b/c animation is playing mKosmos.getKeyguardTransitionRepository().sendTransitionStepJava( @@ -589,6 +609,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { } @Test + @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer public void testCommunalShowingWillNotSuppressReordering() { // GIVEN panel is expanded, communal is showing, and QS is collapsed setPulsing(false); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index a8bcfbcfc539..39a1c106cfcf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.telephony.CellSignalStrength import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN @@ -735,9 +736,10 @@ class MobileIconInteractorTest : SysuiTestCase() { } @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test // See b/346904529 for more context - fun satBasedIcon_doesNotInflateSignalStrength() = + fun satBasedIcon_doesNotInflateSignalStrength_flagOff() = testScope.runTest { val latest by collectLastValue(underTest.signalLevelIcon) @@ -756,7 +758,75 @@ class MobileIconInteractorTest : SysuiTestCase() { assertThat(latest!!.level).isEqualTo(4) } + @EnableFlags( + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG, + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN, + ) + @Test + // See b/346904529 for more context + fun satBasedIcon_doesNotInflateSignalStrength_flagOn() = + testScope.runTest { + val latest by collectLastValue(underTest.signalLevelIcon) + + // GIVEN a satellite connection + connectionRepository.isNonTerrestrial.value = true + // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH + connectionRepository.inflateSignalStrength.value = true + + connectionRepository.satelliteLevel.value = 4 + assertThat(latest!!.level).isEqualTo(4) + + connectionRepository.inflateSignalStrength.value = true + connectionRepository.primaryLevel.value = 4 + + // Icon level is unaffected + assertThat(latest!!.level).isEqualTo(4) + } + + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) + @Test + fun satBasedIcon_usesPrimaryLevel_flagOff() = + testScope.runTest { + val latest by collectLastValue(underTest.signalLevelIcon) + + // GIVEN a satellite connection + connectionRepository.isNonTerrestrial.value = true + + // GIVEN primary level is set + connectionRepository.primaryLevel.value = 4 + connectionRepository.satelliteLevel.value = 0 + + // THEN icon uses the primary level because the flag is off + assertThat(latest!!.level).isEqualTo(4) + } + + @EnableFlags( + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG, + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN, + ) + @Test + fun satBasedIcon_usesSatelliteLevel_flagOn() = + testScope.runTest { + val latest by collectLastValue(underTest.signalLevelIcon) + + // GIVEN a satellite connection + connectionRepository.isNonTerrestrial.value = true + + // GIVEN satellite level is set + connectionRepository.satelliteLevel.value = 4 + connectionRepository.primaryLevel.value = 0 + + // THEN icon uses the satellite level because the flag is on + assertThat(latest!!.level).isEqualTo(4) + } + + /** + * Context (b/377518113), this test will not be needed after FLAG_CARRIER_ROAMING_NB_IOT_NTN is + * rolled out. The new API should report 0 automatically if not in service. + */ @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test fun satBasedIcon_reportsLevelZeroWhenOutOfService() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index 4c7cdfa7fb67..038722cd9608 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -151,7 +151,7 @@ class MobileIconViewModelTest : SysuiTestCase() { iconsInteractor.isForceHidden, repository, context, - MobileIconCarrierIdOverridesFake() + MobileIconCarrierIdOverridesFake(), ) createAndSetViewModel() } @@ -359,7 +359,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) connectionsRepository.mobileIsDefault.value = true repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) @@ -406,7 +406,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) repository.setDataEnabled(true) @@ -426,7 +426,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val initial = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) @@ -448,7 +448,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) repository.dataEnabled.value = true var latest: Icon? = null @@ -477,7 +477,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) assertThat(latest).isEqualTo(expected) @@ -499,7 +499,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) assertThat(latest).isEqualTo(expected) @@ -520,7 +520,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) assertThat(latest).isEqualTo(expected) @@ -542,7 +542,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( connectionsRepository.defaultMobileIconGroup.value.dataType, - ContentDescription.Resource(G.dataContentDescription) + ContentDescription.Resource(G.dataContentDescription), ) assertThat(latest).isEqualTo(expected) @@ -564,7 +564,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) assertThat(latest).isEqualTo(expected) @@ -621,10 +621,7 @@ class MobileIconViewModelTest : SysuiTestCase() { underTest.activityInVisible.onEach { containerVisible = it }.launchIn(this) repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = true, - hasActivityOut = true, - ) + DataActivityModel(hasActivityIn = true, hasActivityOut = true) assertThat(inVisible).isFalse() assertThat(outVisible).isFalse() @@ -654,10 +651,7 @@ class MobileIconViewModelTest : SysuiTestCase() { underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this) repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = true, - hasActivityOut = false, - ) + DataActivityModel(hasActivityIn = true, hasActivityOut = false) yield() @@ -666,20 +660,14 @@ class MobileIconViewModelTest : SysuiTestCase() { assertThat(containerVisible).isTrue() repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = false, - hasActivityOut = true, - ) + DataActivityModel(hasActivityIn = false, hasActivityOut = true) assertThat(inVisible).isFalse() assertThat(outVisible).isTrue() assertThat(containerVisible).isTrue() repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = false, - hasActivityOut = false, - ) + DataActivityModel(hasActivityIn = false, hasActivityOut = false) assertThat(inVisible).isFalse() assertThat(outVisible).isFalse() @@ -709,10 +697,7 @@ class MobileIconViewModelTest : SysuiTestCase() { underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this) repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = true, - hasActivityOut = false, - ) + DataActivityModel(hasActivityIn = true, hasActivityOut = false) yield() @@ -721,20 +706,14 @@ class MobileIconViewModelTest : SysuiTestCase() { assertThat(containerVisible).isTrue() repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = false, - hasActivityOut = true, - ) + DataActivityModel(hasActivityIn = false, hasActivityOut = true) assertThat(inVisible).isFalse() assertThat(outVisible).isTrue() assertThat(containerVisible).isTrue() repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = false, - hasActivityOut = false, - ) + DataActivityModel(hasActivityIn = false, hasActivityOut = false) assertThat(inVisible).isFalse() assertThat(outVisible).isFalse() @@ -824,10 +803,7 @@ class MobileIconViewModelTest : SysuiTestCase() { // sets the background on cellular repository.hasPrioritizedNetworkCapabilities.value = true repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = true, - hasActivityOut = true, - ) + DataActivityModel(hasActivityIn = true, hasActivityOut = true) assertThat(roaming).isFalse() assertThat(networkTypeIcon).isNull() @@ -838,11 +814,13 @@ class MobileIconViewModelTest : SysuiTestCase() { } @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test - fun nonTerrestrial_usesSatelliteIcon() = + fun nonTerrestrial_usesSatelliteIcon_flagOff() = testScope.runTest { repository.isNonTerrestrial.value = true repository.setAllLevels(0) + repository.satelliteLevel.value = 0 val latest by collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) @@ -853,28 +831,66 @@ class MobileIconViewModelTest : SysuiTestCase() { // 1-2 -> 1 bar repository.setAllLevels(1) + repository.satelliteLevel.value = 1 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) repository.setAllLevels(2) + repository.satelliteLevel.value = 2 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) // 3-4 -> 2 bars repository.setAllLevels(3) + repository.satelliteLevel.value = 3 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) repository.setAllLevels(4) + repository.satelliteLevel.value = 4 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + } + + @EnableFlags( + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG, + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN, + ) + @Test + fun nonTerrestrial_usesSatelliteIcon_flagOn() = + testScope.runTest { + repository.isNonTerrestrial.value = true + repository.satelliteLevel.value = 0 + + val latest by + collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) + + // Level 0 -> no connection + assertThat(latest).isNotNull() + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) + + // 1-2 -> 1 bar + repository.satelliteLevel.value = 1 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + repository.satelliteLevel.value = 2 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + // 3-4 -> 2 bars + repository.satelliteLevel.value = 3 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + + repository.satelliteLevel.value = 4 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) } @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test - fun satelliteIcon_ignoresInflateSignalStrength() = + fun satelliteIcon_ignoresInflateSignalStrength_flagOff() = testScope.runTest { // Note that this is the exact same test as above, but with inflateSignalStrength set to // true we note that the level is unaffected by inflation repository.inflateSignalStrength.value = true repository.isNonTerrestrial.value = true repository.setAllLevels(0) + repository.satelliteLevel.value = 0 val latest by collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) @@ -885,16 +901,55 @@ class MobileIconViewModelTest : SysuiTestCase() { // 1-2 -> 1 bar repository.setAllLevels(1) + repository.satelliteLevel.value = 1 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) repository.setAllLevels(2) + repository.satelliteLevel.value = 2 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) // 3-4 -> 2 bars repository.setAllLevels(3) + repository.satelliteLevel.value = 3 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) repository.setAllLevels(4) + repository.satelliteLevel.value = 4 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + } + + @EnableFlags( + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG, + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN, + ) + @Test + fun satelliteIcon_ignoresInflateSignalStrength_flagOn() = + testScope.runTest { + // Note that this is the exact same test as above, but with inflateSignalStrength set to + // true we note that the level is unaffected by inflation + repository.inflateSignalStrength.value = true + repository.isNonTerrestrial.value = true + repository.satelliteLevel.value = 0 + + val latest by + collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) + + // Level 0 -> no connection + assertThat(latest).isNotNull() + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) + + // 1-2 -> 1 bar + repository.satelliteLevel.value = 1 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + repository.satelliteLevel.value = 2 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + // 3-4 -> 2 bars + repository.satelliteLevel.value = 3 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + + repository.satelliteLevel.value = 4 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt index 7d55169e048a..89da46544f1e 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt @@ -13,11 +13,20 @@ */ package com.android.systemui.plugins.clocks +import android.content.Context import android.graphics.Rect import android.graphics.drawable.Drawable +import android.util.DisplayMetrics import android.view.View import androidx.constraintlayout.widget.ConstraintSet +import androidx.constraintlayout.widget.ConstraintSet.BOTTOM +import androidx.constraintlayout.widget.ConstraintSet.END +import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID +import androidx.constraintlayout.widget.ConstraintSet.START +import androidx.constraintlayout.widget.ConstraintSet.TOP +import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import com.android.internal.annotations.Keep +import com.android.internal.policy.SystemBarUtils import com.android.systemui.log.core.MessageBuffer import com.android.systemui.plugins.Plugin import com.android.systemui.plugins.annotations.GeneratedImport @@ -149,7 +158,7 @@ interface ClockFaceLayout { @ProtectedReturn("return constraints;") /** Custom constraints to apply to preview ConstraintLayout. */ - fun applyPreviewConstraints(constraints: ConstraintSet): ConstraintSet + fun applyPreviewConstraints(context: Context, constraints: ConstraintSet): ConstraintSet fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel) } @@ -169,13 +178,84 @@ class DefaultClockFaceLayout(val view: View) : ClockFaceLayout { return constraints } - override fun applyPreviewConstraints(constraints: ConstraintSet): ConstraintSet { - return constraints + override fun applyPreviewConstraints( + context: Context, + constraints: ConstraintSet, + ): ConstraintSet { + return applyDefaultPreviewConstraints(context, constraints) } override fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel) { // Default clock doesn't need detailed control of view } + + companion object { + fun applyDefaultPreviewConstraints( + context: Context, + constraints: ConstraintSet, + ): ConstraintSet { + constraints.apply { + val lockscreenClockViewLargeId = getId(context, "lockscreen_clock_view_large") + constrainWidth(lockscreenClockViewLargeId, WRAP_CONTENT) + constrainHeight(lockscreenClockViewLargeId, WRAP_CONTENT) + constrainMaxHeight(lockscreenClockViewLargeId, 0) + + val largeClockTopMargin = + SystemBarUtils.getStatusBarHeight(context) + + getDimen(context, "small_clock_padding_top") + + getDimen(context, "keyguard_smartspace_top_offset") + + getDimen(context, "date_weather_view_height") + + getDimen(context, "enhanced_smartspace_height") + connect(lockscreenClockViewLargeId, TOP, PARENT_ID, TOP, largeClockTopMargin) + connect(lockscreenClockViewLargeId, START, PARENT_ID, START) + connect(lockscreenClockViewLargeId, END, PARENT_ID, END) + + // In preview, we'll show UDFPS icon for UDFPS devices + // and nothing for non-UDFPS devices, + // and we're not planning to add this vide in clockHostView + // so we only need position of device entry icon to constrain clock + // Copied calculation codes from applyConstraints in DefaultDeviceEntrySection + val bottomPaddingPx = getDimen(context, "lock_icon_margin_bottom") + val defaultDensity = + DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() / + DisplayMetrics.DENSITY_DEFAULT.toFloat() + val lockIconRadiusPx = (defaultDensity * 36).toInt() + val clockBottomMargin = bottomPaddingPx + 2 * lockIconRadiusPx + + connect(lockscreenClockViewLargeId, BOTTOM, PARENT_ID, BOTTOM, clockBottomMargin) + val smallClockViewId = getId(context, "lockscreen_clock_view") + constrainWidth(smallClockViewId, WRAP_CONTENT) + constrainHeight(smallClockViewId, getDimen(context, "small_clock_height")) + connect( + smallClockViewId, + START, + PARENT_ID, + START, + getDimen(context, "clock_padding_start") + + getDimen(context, "status_view_margin_horizontal"), + ) + val smallClockTopMargin = + getDimen(context, "keyguard_clock_top_margin") + + SystemBarUtils.getStatusBarHeight(context) + connect(smallClockViewId, TOP, PARENT_ID, TOP, smallClockTopMargin) + } + return constraints + } + + fun getId(context: Context, name: String): Int { + val packageName = context.packageName + val res = context.packageManager.getResourcesForApplication(packageName) + val id = res.getIdentifier(name, "id", packageName) + return id + } + + fun getDimen(context: Context, name: String): Int { + val packageName = context.packageName + val res = context.packageManager.getResourcesForApplication(packageName) + val id = res.getIdentifier(name, "dimen", packageName) + return if (id == 0) 0 else res.getDimensionPixelSize(id) + } + } } /** Events that should call when various rendering parameters change */ diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml index 2a27b47e54ca..4a53df9c2f29 100644 --- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml @@ -24,7 +24,6 @@ <!-- margin from keyguard status bar to clock. For split shade it should be keyguard_split_shade_top_margin - status_bar_header_height_keyguard = 8dp --> <dimen name="keyguard_clock_top_margin">8dp</dimen> - <dimen name="keyguard_smartspace_top_offset">0dp</dimen> <!-- QS--> <dimen name="qs_panel_padding_top">16dp</dimen> diff --git a/packages/SystemUI/res/values-sw600dp-port/config.xml b/packages/SystemUI/res/values-sw600dp-port/config.xml index f556b97eefc2..53d921b5e534 100644 --- a/packages/SystemUI/res/values-sw600dp-port/config.xml +++ b/packages/SystemUI/res/values-sw600dp-port/config.xml @@ -33,6 +33,9 @@ <!-- The number of columns in the infinite grid QuickSettings --> <integer name="quick_settings_infinite_grid_num_columns">6</integer> + <!-- The maximum width of large tiles in the infinite grid QuickSettings --> + <integer name="quick_settings_infinite_grid_tile_max_width">3</integer> + <integer name="power_menu_lite_max_columns">2</integer> <integer name="power_menu_lite_max_rows">3</integer> diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml index 393631e3364b..26f32ef60851 100644 --- a/packages/SystemUI/res/values-sw600dp/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp/dimens.xml @@ -126,6 +126,4 @@ <dimen name="controls_content_padding">24dp</dimen> <dimen name="control_list_vertical_spacing">8dp</dimen> <dimen name="control_list_horizontal_spacing">16dp</dimen> - <!-- For portrait direction in unfold foldable device, we don't need keyguard_smartspace_top_offset--> - <dimen name="keyguard_smartspace_top_offset">0dp</dimen> </resources> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 0854eb46ffdd..48af82ad7943 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -79,6 +79,9 @@ <!-- The number of columns in the infinite grid QuickSettings --> <integer name="quick_settings_infinite_grid_num_columns">4</integer> + <!-- The maximum width of large tiles in the infinite grid QuickSettings --> + <integer name="quick_settings_infinite_grid_tile_max_width">4</integer> + <!-- The number of columns in the Dual Shade QuickSettings --> <integer name="quick_settings_dual_shade_num_columns">4</integer> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 7fa287944956..67eb5b0fdf6b 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -815,8 +815,7 @@ <dimen name="keyguard_clock_top_margin">18dp</dimen> <!-- The amount to shift the clocks during a small/large transition --> <dimen name="keyguard_clock_switch_y_shift">14dp</dimen> - <!-- When large clock is showing, offset the smartspace by this amount --> - <dimen name="keyguard_smartspace_top_offset">12dp</dimen> + <!-- The amount to translate lockscreen elements on the GONE->AOD transition --> <dimen name="keyguard_enter_from_top_translation_y">-100dp</dimen> <!-- The amount to translate lockscreen elements on the GONE->AOD transition, on device fold --> diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 24d619119983..df9f7053c3f3 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -35,6 +35,7 @@ import android.view.ViewTreeObserver.OnGlobalLayoutListener import androidx.annotation.VisibleForTesting import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.customization.R import com.android.systemui.dagger.qualifiers.Background @@ -62,6 +63,7 @@ import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.plugins.clocks.ZenData import com.android.systemui.plugins.clocks.ZenData.ZenMode import com.android.systemui.res.R as SysuiR +import com.android.systemui.settings.UserTracker import com.android.systemui.shared.regionsampling.RegionSampler import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback @@ -80,7 +82,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge -import com.android.app.tracing.coroutines.launchTraced as launch /** * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by @@ -103,6 +104,7 @@ constructor( private val featureFlags: FeatureFlagsClassic, private val zenModeController: ZenModeController, private val zenModeInteractor: ZenModeInteractor, + private val userTracker: UserTracker, ) { var loggers = listOf( @@ -120,6 +122,10 @@ constructor( connectClock(value) } + private fun is24HourFormat(userId: Int? = null): Boolean { + return DateFormat.is24HourFormat(context, userId ?: userTracker.userId) + } + private fun disconnectClock(clock: ClockController?) { if (clock == null) { return @@ -186,7 +192,7 @@ constructor( var pastVisibility: Int? = null override fun onViewAttachedToWindow(view: View) { - clock.events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) + clock.events.onTimeFormatChanged(is24HourFormat()) // Match the asing for view.parent's layout classes. smallClockFrame = (view.parent as ViewGroup)?.also { frame -> @@ -218,7 +224,7 @@ constructor( largeClockOnAttachStateChangeListener = object : OnAttachStateChangeListener { override fun onViewAttachedToWindow(p0: View) { - clock.events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) + clock.events.onTimeFormatChanged(is24HourFormat()) } override fun onViewDetachedFromWindow(p0: View) {} @@ -358,7 +364,7 @@ constructor( } override fun onTimeFormatChanged(timeFormat: String?) { - clock?.run { events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) } + clock?.run { events.onTimeFormatChanged(is24HourFormat()) } } override fun onTimeZoneChanged(timeZone: TimeZone) { @@ -366,7 +372,7 @@ constructor( } override fun onUserSwitchComplete(userId: Int) { - clock?.run { events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) } + clock?.run { events.onTimeFormatChanged(is24HourFormat(userId)) } zenModeCallback.onNextAlarmChanged() } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 11dde6aa0dfb..71d4e9af6f55 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -147,7 +147,7 @@ public class KeyguardClockSwitch extends RelativeLayout { mClockSwitchYAmount = mContext.getResources().getDimensionPixelSize( R.dimen.keyguard_clock_switch_y_shift); mSmartspaceTopOffset = (int) (mContext.getResources().getDimensionPixelSize( - R.dimen.keyguard_smartspace_top_offset) + com.android.systemui.customization.R.dimen.keyguard_smartspace_top_offset) * mContext.getResources().getConfiguration().fontScale / mContext.getResources().getDisplayMetrics().density * SMARTSPACE_TOP_PADDING_MULTIPLIER); diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index 811b47d57c1d..a46b236d46fb 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -16,6 +16,7 @@ package com.android.systemui; +import android.animation.Animator; import android.annotation.SuppressLint; import android.app.ActivityThread; import android.app.Application; @@ -135,6 +136,9 @@ public class SystemUIApplication extends Application implements if (Flags.enableLayoutTracing()) { View.setTraceLayoutSteps(true); } + if (com.android.window.flags.Flags.systemUiPostAnimationEnd()) { + Animator.setPostNotifyEndListenerEnabled(true); + } if (mProcessWrapper.isSystemUser()) { IntentFilter bootCompletedFilter = new diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt index c9c4fd594adc..6635d8b06a5d 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt @@ -22,6 +22,7 @@ import android.annotation.UserIdInt import android.app.admin.DevicePolicyManager import android.content.IntentFilter import android.os.UserHandle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockscreenCredential import com.android.keyguard.KeyguardSecurityModel @@ -57,7 +58,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.withContext /** Defines interface for classes that can access authentication-related application state. */ @@ -178,6 +178,16 @@ interface AuthenticationRepository { * profile of an organization-owned device. */ @UserIdInt suspend fun getProfileWithMinFailedUnlockAttemptsForWipe(): Int + + /** + * Returns the device policy enforced maximum time to lock the device, in milliseconds. When the + * device goes to sleep, this is the maximum time the device policy allows to wait before + * locking the device, despite what the user setting might be set to. + */ + suspend fun getMaximumTimeToLock(): Long + + /** Returns `true` if the power button should instantly lock the device, `false` otherwise. */ + suspend fun getPowerButtonInstantlyLocks(): Boolean } @SysUISingleton @@ -324,6 +334,19 @@ constructor( } } + override suspend fun getMaximumTimeToLock(): Long { + return withContext(backgroundDispatcher) { + devicePolicyManager.getMaximumTimeToLock(/* admin= */ null, selectedUserId) + } + } + + /** Returns `true` if the power button should instantly lock the device, `false` otherwise. */ + override suspend fun getPowerButtonInstantlyLocks(): Boolean { + return withContext(backgroundDispatcher) { + lockPatternUtils.getPowerButtonInstantlyLocks(selectedUserId) + } + } + private val selectedUserId: Int @UserIdInt get() = userRepository.getSelectedUserInfo().id diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt index 67579fd7f696..1c994731c393 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -17,6 +17,7 @@ package com.android.systemui.authentication.domain.interactor import android.os.UserHandle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternView import com.android.internal.widget.LockscreenCredential @@ -49,7 +50,6 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch /** * Hosts application business logic related to user authentication. @@ -215,7 +215,7 @@ constructor( */ suspend fun authenticate( input: List<Any>, - tryAutoConfirm: Boolean = false + tryAutoConfirm: Boolean = false, ): AuthenticationResult { if (input.isEmpty()) { throw IllegalArgumentException("Input was empty!") @@ -254,6 +254,20 @@ constructor( return AuthenticationResult.FAILED } + /** + * Returns the device policy enforced maximum time to lock the device, in milliseconds. When the + * device goes to sleep, this is the maximum time the device policy allows to wait before + * locking the device, despite what the user setting might be set to. + */ + suspend fun getMaximumTimeToLock(): Long { + return repository.getMaximumTimeToLock() + } + + /** Returns `true` if the power button should instantly lock the device, `false` otherwise. */ + suspend fun getPowerButtonInstantlyLocks(): Boolean { + return !getAuthenticationMethod().isSecure || repository.getPowerButtonInstantlyLocks() + } + private suspend fun shouldSkipAuthenticationAttempt( authenticationMethod: AuthenticationMethodModel, isAutoConfirmAttempt: Boolean, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt index b07006887011..08b3e99fadd0 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt @@ -72,7 +72,7 @@ constructor( // Request LockSettingsService to return the Gatekeeper Password in the // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the // Gatekeeper Password and operationId. - var effectiveUserId = request.userInfo.userIdForPasswordEntry + var effectiveUserId = request.userInfo.deviceCredentialOwnerId val response = if (Flags.privateSpaceBp() && effectiveUserId != request.userInfo.userId) { effectiveUserId = request.userInfo.userId diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt index 24278ecc76bd..b74ca035a229 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.deviceentry.domain.interactor +import android.provider.Settings import android.util.Log import androidx.annotation.VisibleForTesting import com.android.systemui.CoreStartable @@ -28,20 +29,29 @@ import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReaso import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus import com.android.systemui.flags.SystemPropertiesHelper +import com.android.systemui.keyguard.KeyguardViewMediator +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.TrustInteractor import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import javax.inject.Inject +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map @@ -61,6 +71,8 @@ constructor( private val powerInteractor: PowerInteractor, private val biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor, private val systemPropertiesHelper: SystemPropertiesHelper, + private val userAwareSecureSettingsRepository: UserAwareSecureSettingsRepository, + private val keyguardInteractor: KeyguardInteractor, ) : ExclusiveActivatable() { private val deviceUnlockSource = @@ -176,45 +188,146 @@ constructor( Log.d(TAG, "remaining locked because SIM locked") repository.deviceUnlockStatus.value = DeviceUnlockStatus(false, null) } else { - try { - Log.d(TAG, "started watching for lock and unlock events") - coroutineScope { - launch { - // Unlock the device when a new unlock source is detected. - deviceUnlockSource.collect { - Log.d(TAG, "unlocking due to \"$it\"") - repository.deviceUnlockStatus.value = DeviceUnlockStatus(true, it) + handleLockAndUnlockEvents() + } + } + + awaitCancellation() + } + + private suspend fun handleLockAndUnlockEvents() { + try { + Log.d(TAG, "started watching for lock and unlock events") + coroutineScope { + launch { handleUnlockEvents() } + launch { handleLockEvents() } + } + } finally { + Log.d(TAG, "stopped watching for lock and unlock events") + } + } + + private suspend fun handleUnlockEvents() { + // Unlock the device when a new unlock source is detected. + deviceUnlockSource.collect { + Log.d(TAG, "unlocking due to \"$it\"") + repository.deviceUnlockStatus.value = DeviceUnlockStatus(true, it) + } + } + + private suspend fun handleLockEvents() { + merge( + // Device wakefulness events. + powerInteractor.detailedWakefulness + .map { Pair(it.isAsleep(), it.lastSleepReason) } + .distinctUntilChangedBy { it.first } + .map { (isAsleep, lastSleepReason) -> + if (isAsleep) { + if ( + lastSleepReason == WakeSleepReason.POWER_BUTTON && + authenticationInteractor.getPowerButtonInstantlyLocks() + ) { + LockImmediately("locked instantly from power button") + } else { + LockWithDelay("entering sleep") } + } else { + CancelDelayedLock("waking up") } - - launch { - // Lock events. - merge( - // Device goes to sleep. - powerInteractor.isAsleep - .distinctUntilChanged() - .filter { it } - .map { "asleep" }, - // Device enters lockdown. - isInLockdown - .distinctUntilChanged() - .filter { it } - .map { "lockdown" }, - ) - .collect { reason: String -> - Log.d(TAG, "locking due to \"$reason\"") - repository.deviceUnlockStatus.value = - DeviceUnlockStatus(false, null) - } + }, + // Device enters lockdown. + isInLockdown + .distinctUntilChanged() + .filter { it } + .map { LockImmediately("lockdown") }, + // Started dreaming + powerInteractor.isInteractive.flatMapLatestConflated { isInteractive -> + // Only respond to dream state changes while the device is interactive. + if (isInteractive) { + keyguardInteractor.isDreamingAny.distinctUntilChanged().map { isDreaming -> + if (isDreaming) { + LockWithDelay("started dreaming") + } else { + CancelDelayedLock("stopped dreaming") + } } + } else { + emptyFlow() } - } finally { - Log.d(TAG, "stopped watching for lock and unlock events") + }, + ) + .collectLatest(::onLockEvent) + } + + private suspend fun onLockEvent(event: LockEvent) { + val debugReason = event.debugReason + when (event) { + is LockImmediately -> { + Log.d(TAG, "locking without delay due to \"$debugReason\"") + repository.deviceUnlockStatus.value = DeviceUnlockStatus(false, null) + } + + is LockWithDelay -> { + val lockDelay = lockDelay() + Log.d(TAG, "locking in ${lockDelay}ms due to \"$debugReason\"") + try { + delay(lockDelay) + Log.d( + TAG, + "locking after having waited for ${lockDelay}ms due to \"$debugReason\"", + ) + repository.deviceUnlockStatus.value = DeviceUnlockStatus(false, null) + } catch (_: CancellationException) { + Log.d( + TAG, + "delayed locking canceled, original delay was ${lockDelay}ms and reason was \"$debugReason\"", + ) } } + + is CancelDelayedLock -> { + // Do nothing, the mere receipt of this inside of a "latest" block means that any + // previous coroutine is automatically canceled. + } } + } - awaitCancellation() + /** + * Returns the amount of time to wait before locking down the device after the device has been + * put to sleep by the user, in milliseconds. + */ + private suspend fun lockDelay(): Long { + val lockAfterScreenTimeoutSetting = + userAwareSecureSettingsRepository + .getInt( + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT, + ) + .toLong() + Log.d(TAG, "Lock after screen timeout setting set to ${lockAfterScreenTimeoutSetting}ms") + + val maxTimeToLockDevicePolicy = authenticationInteractor.getMaximumTimeToLock() + Log.d(TAG, "Device policy max set to ${maxTimeToLockDevicePolicy}ms") + + if (maxTimeToLockDevicePolicy <= 0) { + // No device policy enforced maximum. + Log.d(TAG, "No device policy max, delay is ${lockAfterScreenTimeoutSetting}ms") + return lockAfterScreenTimeoutSetting + } + + val screenOffTimeoutSetting = + userAwareSecureSettingsRepository + .getInt( + Settings.System.SCREEN_OFF_TIMEOUT, + KeyguardViewMediator.KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT, + ) + .coerceAtLeast(0) + .toLong() + Log.d(TAG, "Screen off timeout setting set to ${screenOffTimeoutSetting}ms") + + return (maxTimeToLockDevicePolicy - screenOffTimeoutSetting) + .coerceIn(minimumValue = 0, maximumValue = lockAfterScreenTimeoutSetting) + .also { Log.d(TAG, "Device policy max enforced, delay is ${it}ms") } } private fun DeviceEntryRestrictionReason?.isInLockdown(): Boolean { @@ -254,6 +367,16 @@ constructor( } } + private sealed interface LockEvent { + val debugReason: String + } + + private data class LockImmediately(override val debugReason: String) : LockEvent + + private data class LockWithDelay(override val debugReason: String) : LockEvent + + private data class CancelDelayedLock(override val debugReason: String) : LockEvent + companion object { private val TAG = "DeviceUnlockedInteractor" @VisibleForTesting const val SYS_BOOT_REASON_PROP = "sys.boot.reason.last" diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java index dd08d3262546..7a95a41770ac 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java @@ -40,7 +40,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; -import com.android.systemui.Flags; import com.android.systemui.biometrics.AuthController; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dock.DockManager; @@ -566,8 +565,7 @@ public class DozeTriggers implements DozeMachine.Part { } // When already in pulsing, we can show the new Notification without requesting a new pulse. - if (Flags.notificationPulsingFix() - && dozeState == State.DOZE_PULSING && reason == DozeLog.PULSE_REASON_NOTIFICATION) { + if (dozeState == State.DOZE_PULSING && reason == DozeLog.PULSE_REASON_NOTIFICATION) { return; } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 60a306b3e245..2ee9ddb0e453 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -239,7 +239,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private static final boolean ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS = Flags.ensureKeyguardDoesTransitionStarting(); - private static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000; + public static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000; private static final long KEYGUARD_DONE_PENDING_TIMEOUT_MS = 3000; private static final boolean DEBUG = KeyguardConstants.DEBUG; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index 3a5614fbc430..eaf8fa9585f6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -114,6 +114,18 @@ interface KeyguardTransitionRepository { @FloatRange(from = 0.0, to = 1.0) value: Float, state: TransitionState, ) + + /** + * Forces the current transition to emit FINISHED, foregoing any additional RUNNING steps that + * otherwise would have been emitted. + * + * When the screen is off, upcoming performance changes cause all Animators to cease emitting + * frames, which means the Animator passed to [startTransition] will never finish if it was + * running when the screen turned off. Also, there's simply no reason to emit RUNNING steps when + * the screen isn't even on. As long as we emit FINISHED, everything should end up in the + * correct state. + */ + suspend fun forceFinishCurrentTransition() } @SysUISingleton @@ -134,6 +146,7 @@ constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionR override val transitions = _transitions.asSharedFlow().distinctUntilChanged() private var lastStep: TransitionStep = TransitionStep() private var lastAnimator: ValueAnimator? = null + private var animatorListener: AnimatorListenerAdapter? = null private val withContextMutex = Mutex() private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> = @@ -233,7 +246,7 @@ constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionR ) } - val adapter = + animatorListener = object : AnimatorListenerAdapter() { override fun onAnimationStart(animation: Animator) { emitTransition( @@ -254,9 +267,10 @@ constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionR animator.removeListener(this) animator.removeUpdateListener(updateListener) lastAnimator = null + animatorListener = null } } - animator.addListener(adapter) + animator.addListener(animatorListener) animator.addUpdateListener(updateListener) animator.start() return@withContext null @@ -290,6 +304,33 @@ constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionR } } + override suspend fun forceFinishCurrentTransition() { + withContextMutex.lock() + + if (lastAnimator?.isRunning != true) { + return + } + + return withContext("$TAG#forceFinishCurrentTransition", mainDispatcher) { + withContextMutex.unlock() + + Log.d(TAG, "forceFinishCurrentTransition() - emitting FINISHED early.") + + lastAnimator?.apply { + // Cancel the animator, but remove listeners first so we don't emit CANCELED. + removeAllListeners() + cancel() + + // Emit a final 1f RUNNING step to ensure that any transitions not listening for a + // FINISHED step end up in the right end state. + emitTransition(TransitionStep(currentTransitionInfo, 1f, TransitionState.RUNNING)) + + // Ask the listener to emit FINISHED and clean up its state. + animatorListener?.onAnimationEnd(this) + } + } + } + private suspend fun updateTransitionInternal( transitionId: UUID, @FloatRange(from = 0.0, to = 1.0) value: Float, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index b815f1988e7e..7cd2744cb7dc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -19,8 +19,10 @@ package com.android.systemui.keyguard.domain.interactor import android.annotation.SuppressLint import android.util.Log +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey +import com.android.systemui.Flags.keyguardTransitionForceFinishOnScreenOff import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository @@ -30,6 +32,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.OFF import com.android.systemui.keyguard.shared.model.KeyguardState.UNDEFINED import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.ScreenPowerState import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes @@ -59,7 +63,6 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch /** Encapsulates business-logic related to the keyguard transitions. */ @OptIn(ExperimentalCoroutinesApi::class) @@ -70,6 +73,7 @@ constructor( @Application val scope: CoroutineScope, private val repository: KeyguardTransitionRepository, private val sceneInteractor: SceneInteractor, + private val powerInteractor: PowerInteractor, ) { private val transitionMap = mutableMapOf<Edge.StateToState, MutableSharedFlow<TransitionStep>>() @@ -188,6 +192,18 @@ constructor( } } } + + if (keyguardTransitionForceFinishOnScreenOff()) { + /** + * If the screen is turning off, finish the current transition immediately. Further + * frames won't be visible anyway. + */ + scope.launch { + powerInteractor.screenPowerState + .filter { it == ScreenPowerState.SCREEN_TURNING_OFF } + .collect { repository.forceFinishCurrentTransition() } + } + } } fun transition(edge: Edge, edgeWithoutSceneContainer: Edge? = null): Flow<TransitionStep> { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt index 46f5c05092eb..914fdd20e48e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt @@ -18,34 +18,23 @@ package com.android.systemui.keyguard.ui.binder import android.content.Context -import android.util.DisplayMetrics import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import androidx.constraintlayout.widget.ConstraintSet.BOTTOM -import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID -import androidx.constraintlayout.widget.ConstraintSet.START -import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.internal.policy.SystemBarUtils -import com.android.systemui.customization.R as customR import com.android.systemui.keyguard.shared.model.ClockSizeSetting import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer -import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection.Companion.getDimen import com.android.systemui.keyguard.ui.view.layout.sections.setVisibility import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.clocks.ClockController -import com.android.systemui.res.R import com.android.systemui.shared.clocks.ClockRegistry -import com.android.systemui.util.Utils import kotlin.reflect.KSuspendFunction1 /** Binder for the small clock view, large clock view. */ @@ -131,78 +120,6 @@ object KeyguardPreviewClockViewBinder { } } - private fun applyClockDefaultConstraints(context: Context, constraints: ConstraintSet) { - constraints.apply { - constrainWidth(customR.id.lockscreen_clock_view_large, ConstraintSet.WRAP_CONTENT) - // The following two lines make lockscreen_clock_view_large is constrained to available - // height when it goes beyond constraints; otherwise, it use WRAP_CONTENT - constrainHeight(customR.id.lockscreen_clock_view_large, WRAP_CONTENT) - constrainMaxHeight(customR.id.lockscreen_clock_view_large, 0) - val largeClockTopMargin = - SystemBarUtils.getStatusBarHeight(context) + - context.resources.getDimensionPixelSize(customR.dimen.small_clock_padding_top) + - context.resources.getDimensionPixelSize( - R.dimen.keyguard_smartspace_top_offset - ) + - getDimen(context, DATE_WEATHER_VIEW_HEIGHT) + - getDimen(context, ENHANCED_SMARTSPACE_HEIGHT) - connect( - customR.id.lockscreen_clock_view_large, - TOP, - PARENT_ID, - TOP, - largeClockTopMargin, - ) - connect(customR.id.lockscreen_clock_view_large, START, PARENT_ID, START) - connect( - customR.id.lockscreen_clock_view_large, - ConstraintSet.END, - PARENT_ID, - ConstraintSet.END, - ) - - // In preview, we'll show UDFPS icon for UDFPS devices and nothing for non-UDFPS - // devices, but we need position of device entry icon to constrain clock - if (getConstraint(lockId) != null) { - connect(customR.id.lockscreen_clock_view_large, BOTTOM, lockId, TOP) - } else { - // Copied calculation codes from applyConstraints in DefaultDeviceEntrySection - val bottomPaddingPx = - context.resources.getDimensionPixelSize(R.dimen.lock_icon_margin_bottom) - val defaultDensity = - DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() / - DisplayMetrics.DENSITY_DEFAULT.toFloat() - val lockIconRadiusPx = (defaultDensity * 36).toInt() - val clockBottomMargin = bottomPaddingPx + 2 * lockIconRadiusPx - connect( - customR.id.lockscreen_clock_view_large, - BOTTOM, - PARENT_ID, - BOTTOM, - clockBottomMargin, - ) - } - - constrainWidth(customR.id.lockscreen_clock_view, WRAP_CONTENT) - constrainHeight( - customR.id.lockscreen_clock_view, - context.resources.getDimensionPixelSize(customR.dimen.small_clock_height), - ) - connect( - customR.id.lockscreen_clock_view, - START, - PARENT_ID, - START, - context.resources.getDimensionPixelSize(customR.dimen.clock_padding_start) + - context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal), - ) - val smallClockTopMargin = - context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) + - Utils.getStatusBarHeaderHeightKeyguard(context) - connect(customR.id.lockscreen_clock_view, TOP, PARENT_ID, TOP, smallClockTopMargin) - } - } - private fun applyPreviewConstraints( context: Context, rootView: ConstraintLayout, @@ -210,9 +127,8 @@ object KeyguardPreviewClockViewBinder { viewModel: KeyguardPreviewClockViewModel, ) { val cs = ConstraintSet().apply { clone(rootView) } - applyClockDefaultConstraints(context, cs) - previewClock.largeClock.layout.applyPreviewConstraints(cs) - previewClock.smallClock.layout.applyPreviewConstraints(cs) + previewClock.largeClock.layout.applyPreviewConstraints(context, cs) + previewClock.smallClock.layout.applyPreviewConstraints(context, cs) // When selectedClockSize is the initial value, make both clocks invisible to avoid // flickering diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index ee4f41ddd5a0..6096cf74a772 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -186,12 +186,23 @@ constructor( constraints.apply { connect(customR.id.lockscreen_clock_view_large, START, PARENT_ID, START) connect(customR.id.lockscreen_clock_view_large, END, guideline, END) - connect(customR.id.lockscreen_clock_view_large, BOTTOM, R.id.device_entry_icon_view, TOP) + connect( + customR.id.lockscreen_clock_view_large, + BOTTOM, + R.id.device_entry_icon_view, + TOP, + ) val largeClockTopMargin = keyguardClockViewModel.getLargeClockTopMargin() + getDimen(DATE_WEATHER_VIEW_HEIGHT) + getDimen(ENHANCED_SMARTSPACE_HEIGHT) - connect(customR.id.lockscreen_clock_view_large, TOP, PARENT_ID, TOP, largeClockTopMargin) + connect( + customR.id.lockscreen_clock_view_large, + TOP, + PARENT_ID, + TOP, + largeClockTopMargin, + ) constrainWidth(customR.id.lockscreen_clock_view_large, WRAP_CONTENT) // The following two lines make lockscreen_clock_view_large is constrained to available diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt index c11005d38986..a595d815e016 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt @@ -93,18 +93,18 @@ class ClockSizeTransition( fromBounds: Rect, toBounds: Rect, fromSSBounds: Rect?, - toSSBounds: Rect? + toSSBounds: Rect?, ) {} override fun createAnimator( sceenRoot: ViewGroup, startValues: TransitionValues?, - endValues: TransitionValues? + endValues: TransitionValues?, ): Animator? { if (startValues == null || endValues == null) { Log.w( TAG, - "Couldn't create animator: startValues=$startValues; endValues=$endValues" + "Couldn't create animator: startValues=$startValues; endValues=$endValues", ) return null } @@ -137,7 +137,7 @@ class ClockSizeTransition( "Skipping no-op transition: $toView; " + "vis: $fromVis -> $toVis; " + "alpha: $fromAlpha -> $toAlpha; " + - "bounds: $fromBounds -> $toBounds; " + "bounds: $fromBounds -> $toBounds; ", ) } return null @@ -151,7 +151,7 @@ class ClockSizeTransition( lerp(fromBounds.left, toBounds.left, fract), lerp(fromBounds.top, toBounds.top, fract), lerp(fromBounds.right, toBounds.right, fract), - lerp(fromBounds.bottom, toBounds.bottom, fract) + lerp(fromBounds.bottom, toBounds.bottom, fract), ) fun assignAnimValues(src: String, fract: Float, vis: Int? = null) { @@ -160,7 +160,7 @@ class ClockSizeTransition( if (DEBUG) { Log.i( TAG, - "$src: $toView; fract=$fract; alpha=$alpha; vis=$vis; bounds=$bounds;" + "$src: $toView; fract=$fract; alpha=$alpha; vis=$vis; bounds=$bounds;", ) } toView.setVisibility(vis ?: View.VISIBLE) @@ -174,7 +174,7 @@ class ClockSizeTransition( "transitioning: $toView; " + "vis: $fromVis -> $toVis; " + "alpha: $fromAlpha -> $toAlpha; " + - "bounds: $fromBounds -> $toBounds; " + "bounds: $fromBounds -> $toBounds; ", ) } @@ -258,7 +258,7 @@ class ClockSizeTransition( fromBounds: Rect, toBounds: Rect, fromSSBounds: Rect?, - toSSBounds: Rect? + toSSBounds: Rect?, ) { // Move normally if clock is not changing visibility if (fromIsVis == toIsVis) return @@ -347,12 +347,17 @@ class ClockSizeTransition( fromBounds: Rect, toBounds: Rect, fromSSBounds: Rect?, - toSSBounds: Rect? + toSSBounds: Rect?, ) { // If view is changing visibility, hold it in place if (fromIsVis == toIsVis) return if (DEBUG) Log.i(TAG, "Holding position of ${view.id}") - toBounds.set(fromBounds) + + if (fromIsVis) { + toBounds.set(fromBounds) + } else { + fromBounds.set(toBounds) + } } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt index 5c79c0b5c1bb..82adced1e1be 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt @@ -181,7 +181,7 @@ constructor( fun getLargeClockTopMargin(): Int { return systemBarUtils.getStatusBarHeight() + resources.getDimensionPixelSize(customR.dimen.small_clock_padding_top) + - resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) + resources.getDimensionPixelSize(customR.dimen.keyguard_smartspace_top_offset) } val largeClockTopMargin: Flow<Int> = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt index 6579ea162ee2..65c0f57b76f5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.Context import com.android.internal.policy.SystemBarUtils +import com.android.systemui.customization.R as customR import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.ClockSizeSetting import com.android.systemui.res.R @@ -39,20 +40,16 @@ constructor( val selectedClockSize: StateFlow<ClockSizeSetting> = interactor.selectedClockSize val shouldHideSmartspace: Flow<Boolean> = - combine( - interactor.selectedClockSize, - interactor.currentClockId, - ::Pair, - ) - .map { (size, currentClockId) -> - when (size) { - // TODO (b/284122375) This is temporary. We should use clockController - // .largeClock.config.hasCustomWeatherDataDisplay instead, but - // ClockRegistry.createCurrentClock is not reliable. - ClockSizeSetting.DYNAMIC -> currentClockId == "DIGITAL_CLOCK_WEATHER" - ClockSizeSetting.SMALL -> false - } + combine(interactor.selectedClockSize, interactor.currentClockId, ::Pair).map { + (size, currentClockId) -> + when (size) { + // TODO (b/284122375) This is temporary. We should use clockController + // .largeClock.config.hasCustomWeatherDataDisplay instead, but + // ClockRegistry.createCurrentClock is not reliable. + ClockSizeSetting.DYNAMIC -> currentClockId == "DIGITAL_CLOCK_WEATHER" + ClockSizeSetting.SMALL -> false } + } fun getSmartspaceStartPadding(context: Context): Int { return KeyguardSmartspaceViewModel.getSmartspaceStartMargin(context) @@ -83,7 +80,7 @@ constructor( } else { getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) + SystemBarUtils.getStatusBarHeight(context) + - getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) + getDimensionPixelSize(customR.dimen.keyguard_smartspace_top_offset) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt index 850e943d17eb..ef6ae0dd6427 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt @@ -17,8 +17,10 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.res.Resources +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.annotations.VisibleForTesting import com.android.systemui.biometrics.AuthController +import com.android.systemui.customization.R as customR import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor @@ -42,7 +44,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch class LockscreenContentViewModel @AssistedInject @@ -82,10 +83,7 @@ constructor( unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = true), unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false), ) { start, end -> - UnfoldTranslations( - start = start, - end = end, - ) + UnfoldTranslations(start = start, end = end) } .collect { _unfoldTranslations.value = it } } @@ -102,17 +100,15 @@ constructor( /** Returns a flow that indicates whether lockscreen notifications should be rendered. */ fun areNotificationsVisible(): Flow<Boolean> { - return combine( - clockSize, - shadeInteractor.isShadeLayoutWide, - ) { clockSize, isShadeLayoutWide -> + return combine(clockSize, shadeInteractor.isShadeLayoutWide) { clockSize, isShadeLayoutWide + -> clockSize == ClockSize.SMALL || isShadeLayoutWide } } fun getSmartSpacePaddingTop(resources: Resources): Int { return if (clockSize.value == ClockSize.LARGE) { - resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) + + resources.getDimensionPixelSize(customR.dimen.keyguard_smartspace_top_offset) + resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) } else { 0 diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt index d38e507082b9..913aa6f9d547 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt @@ -42,7 +42,7 @@ import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.util.MediaControllerFactory import com.android.systemui.media.controls.util.SessionTokenFactory import com.android.systemui.res.R -import com.android.systemui.util.Assert +import com.android.systemui.util.concurrency.Execution import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -63,6 +63,7 @@ constructor( @Background private val looper: Looper, @Background private val handler: Handler, @Background private val bgScope: CoroutineScope, + private val execution: Execution, ) { /** @@ -108,7 +109,7 @@ constructor( m3controller: Media3Controller, token: SessionToken, ): MediaButton? { - Assert.isNotMainThread() + require(!execution.isMainThread()) // First, get standard actions val playOrPause = diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt index 8d48c1d1d23f..1cf4c23415da 100644 --- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt @@ -26,11 +26,13 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.data.repository.PowerRepository import com.android.systemui.power.shared.model.ScreenPowerState import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.power.shared.model.WakefulnessModel import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.statusbar.phone.ScreenOffAnimationController import javax.inject.Inject import javax.inject.Provider import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -56,7 +58,7 @@ constructor( * Unless you need to respond differently to different [WakeSleepReason]s, you should use * [isAwake]. */ - val detailedWakefulness = repository.wakefulness + val detailedWakefulness: StateFlow<WakefulnessModel> = repository.wakefulness /** * Whether we're awake (screen is on and responding to user touch) or asleep (screen is off, or @@ -189,9 +191,7 @@ constructor( * In tests, you should be able to use [setAsleepForTest] rather than calling this method * directly. */ - fun onFinishedGoingToSleep( - powerButtonLaunchGestureTriggeredDuringSleep: Boolean, - ) { + fun onFinishedGoingToSleep(powerButtonLaunchGestureTriggeredDuringSleep: Boolean) { // If the launch gesture was previously detected via onCameraLaunchGestureDetected, carry // that state forward. It will be reset by the next onStartedGoingToSleep. val powerButtonLaunchGestureTriggered = @@ -255,7 +255,7 @@ constructor( @JvmOverloads fun PowerInteractor.setAwakeForTest( @PowerManager.WakeReason reason: Int = PowerManager.WAKE_REASON_UNKNOWN, - forceEmit: Boolean = false + forceEmit: Boolean = false, ) { emitDuplicateWakefulnessValue = forceEmit @@ -286,9 +286,7 @@ constructor( emitDuplicateWakefulnessValue = forceEmit this.onStartedGoingToSleep(reason = sleepReason) - this.onFinishedGoingToSleep( - powerButtonLaunchGestureTriggeredDuringSleep = false, - ) + this.onFinishedGoingToSleep(powerButtonLaunchGestureTriggeredDuringSleep = false) } /** Helper method for tests to simulate the device screen state change event. */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepository.kt new file mode 100644 index 000000000000..58834037e2b7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepository.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.systemui.qs.panels.data.repository + +import android.content.res.Resources +import com.android.systemui.common.ui.data.repository.ConfigurationRepository +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.res.R +import com.android.systemui.util.kotlin.emitOnStart +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class LargeTileSpanRepository +@Inject +constructor( + @Application scope: CoroutineScope, + @Main private val resources: Resources, + configurationRepository: ConfigurationRepository, +) { + val span: StateFlow<Int> = + configurationRepository.onConfigurationChange + .emitOnStart() + .mapLatest { + if (resources.configuration.fontScale >= FONT_SCALE_THRESHOLD) { + resources.getInteger(R.integer.quick_settings_infinite_grid_tile_max_width) + } else { + 2 + } + } + .distinctUntilChanged() + .stateIn(scope, SharingStarted.WhileSubscribed(), 2) + + private companion object { + const val FONT_SCALE_THRESHOLD = 2f + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt index ec61a0d5769e..23c79f576df5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt @@ -21,12 +21,14 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository +import com.android.systemui.qs.panels.data.repository.LargeTileSpanRepository import com.android.systemui.qs.panels.shared.model.PanelsLog import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor import com.android.systemui.qs.pipeline.shared.TileSpec import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn @@ -38,6 +40,7 @@ constructor( private val repo: DefaultLargeTilesRepository, private val currentTilesInteractor: CurrentTilesInteractor, private val preferencesInteractor: QSPreferencesInteractor, + largeTilesSpanRepo: LargeTileSpanRepository, @PanelsLog private val logBuffer: LogBuffer, @Application private val applicationScope: CoroutineScope, ) { @@ -46,6 +49,8 @@ constructor( .onEach { logChange(it) } .stateIn(applicationScope, SharingStarted.Eagerly, repo.defaultLargeTiles) + val largeTilesSpan: StateFlow<Int> = largeTilesSpanRepo.span + fun isIconTile(spec: TileSpec): Boolean = !largeTilesSpecs.value.contains(spec) fun setLargeTiles(specs: Set<TileSpec>) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt index 74fa0fef21d7..c729c7c15176 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt @@ -37,13 +37,17 @@ import com.android.systemui.qs.pipeline.shared.TileSpec fun rememberEditListState( tiles: List<SizedTile<EditTileViewModel>>, columns: Int, + largeTilesSpan: Int, ): EditTileListState { - return remember(tiles, columns) { EditTileListState(tiles, columns) } + return remember(tiles, columns) { EditTileListState(tiles, columns, largeTilesSpan) } } /** Holds the temporary state of the tile list during a drag movement where we move tiles around. */ -class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val columns: Int) : - DragAndDropState { +class EditTileListState( + tiles: List<SizedTile<EditTileViewModel>>, + private val columns: Int, + private val largeTilesSpan: Int, +) : DragAndDropState { private val _draggedCell = mutableStateOf<SizedTile<EditTileViewModel>?>(null) override val draggedCell get() = _draggedCell.value @@ -86,7 +90,7 @@ class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val c if (fromIndex != -1) { val cell = _tiles.removeAt(fromIndex) cell as TileGridCell - _tiles.add(fromIndex, cell.copy(width = if (cell.isIcon) 2 else 1)) + _tiles.add(fromIndex, cell.copy(width = if (cell.isIcon) largeTilesSpan else 1)) regenerateGrid(fromIndex) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index d10722287f5d..4a51bf06d4af 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -33,7 +33,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -63,6 +62,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.modifiers.size import com.android.compose.modifiers.thenIf +import com.android.compose.ui.graphics.painter.rememberDrawablePainter import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.Icon @@ -194,26 +194,35 @@ fun SmallTileContent( is Icon.Resource -> context.getDrawable(icon.res) } } - if (loadedDrawable !is Animatable) { - Icon(icon = icon, tint = animatedColor, modifier = iconModifier) - } else if (icon is Icon.Resource) { - val image = AnimatedImageVector.animatedVectorResource(id = icon.res) + if (loadedDrawable is Animatable) { val painter = - key(icon) { - if (animateToEnd) { - rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true) - } else { - var atEnd by remember(icon.res) { mutableStateOf(false) } - LaunchedEffect(key1 = icon.res) { atEnd = true } - rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd) + when (icon) { + is Icon.Resource -> { + val image = AnimatedImageVector.animatedVectorResource(id = icon.res) + key(icon) { + if (animateToEnd) { + rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true) + } else { + var atEnd by remember(icon.res) { mutableStateOf(false) } + LaunchedEffect(key1 = icon.res) { atEnd = true } + rememberAnimatedVectorPainter( + animatedImageVector = image, + atEnd = atEnd, + ) + } + } } + is Icon.Loaded -> rememberDrawablePainter(loadedDrawable) } + Image( painter = painter, contentDescription = icon.contentDescription?.load(), colorFilter = ColorFilter.tint(color = animatedColor), modifier = iconModifier, ) + } else { + Icon(icon = icon, tint = animatedColor, modifier = iconModifier) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt index b5cec120987f..31ea60e2f0bc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt @@ -26,7 +26,7 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.LocalOverscrollConfiguration +import androidx.compose.foundation.LocalOverscrollFactory import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.gestures.Orientation @@ -173,6 +173,7 @@ fun DefaultEditTileGrid( listState: EditTileListState, otherTiles: List<SizedTile<EditTileViewModel>>, columns: Int, + largeTilesSpan: Int, modifier: Modifier, onRemoveTile: (TileSpec) -> Unit, onSetTiles: (List<TileSpec>) -> Unit, @@ -203,7 +204,7 @@ fun DefaultEditTileGrid( containerColor = Color.Transparent, topBar = { EditModeTopBar(onStopEditing = onStopEditing, onReset = reset) }, ) { innerPadding -> - CompositionLocalProvider(LocalOverscrollConfiguration provides null) { + CompositionLocalProvider(LocalOverscrollFactory provides null) { val scrollState = rememberScrollState() LaunchedEffect(listState.dragInProgress) { if (listState.dragInProgress) { @@ -230,7 +231,14 @@ fun DefaultEditTileGrid( } } - CurrentTilesGrid(listState, selectionState, columns, onResize, onSetTiles) + CurrentTilesGrid( + listState, + selectionState, + columns, + largeTilesSpan, + onResize, + onSetTiles, + ) // Hide available tiles when dragging AnimatedVisibility( @@ -273,7 +281,7 @@ private fun EditGridHeader( ) { Box( contentAlignment = Alignment.Center, - modifier = modifier.fillMaxWidth().height(EditModeTileDefaults.EditGridHeaderHeight), + modifier = modifier.fillMaxWidth().wrapContentHeight(), ) { content() } @@ -300,6 +308,7 @@ private fun CurrentTilesGrid( listState: EditTileListState, selectionState: MutableSelectionState, columns: Int, + largeTilesSpan: Int, onResize: (TileSpec, toIcon: Boolean) -> Unit, onSetTiles: (List<TileSpec>) -> Unit, ) { @@ -340,7 +349,8 @@ private fun CurrentTilesGrid( } .testTag(CURRENT_TILES_GRID_TEST_TAG), ) { - EditTiles(cells, columns, listState, selectionState, coroutineScope) { spec -> + EditTiles(cells, columns, listState, selectionState, coroutineScope, largeTilesSpan) { spec + -> // Toggle the current size of the tile currentListState.isIcon(spec)?.let { onResize(spec, !it) } } @@ -425,6 +435,7 @@ fun LazyGridScope.EditTiles( dragAndDropState: DragAndDropState, selectionState: MutableSelectionState, coroutineScope: CoroutineScope, + largeTilesSpan: Int, onToggleSize: (spec: TileSpec) -> Unit, ) { items( @@ -456,6 +467,7 @@ fun LazyGridScope.EditTiles( onToggleSize = onToggleSize, coroutineScope = coroutineScope, bounceableInfo = cells.bounceableInfo(index, columns), + largeTilesSpan = largeTilesSpan, modifier = Modifier.animateItem(), ) } @@ -472,6 +484,7 @@ private fun TileGridCell( selectionState: MutableSelectionState, onToggleSize: (spec: TileSpec) -> Unit, coroutineScope: CoroutineScope, + largeTilesSpan: Int, bounceableInfo: BounceableInfo, modifier: Modifier = Modifier, ) { @@ -514,8 +527,11 @@ private fun TileGridCell( .fillMaxWidth() .onSizeChanged { // Grab the size before the bounceable to get the idle width - val min = if (cell.isIcon) it.width else (it.width - padding) / 2 - val max = if (cell.isIcon) (it.width * 2) + padding else it.width + val totalPadding = (largeTilesSpan - 1) * padding + val min = + if (cell.isIcon) it.width else (it.width - totalPadding) / largeTilesSpan + val max = + if (cell.isIcon) (it.width * largeTilesSpan) + totalPadding else it.width tileWidths = TileWidths(it.width, min, max) } .bounceable( @@ -554,15 +570,13 @@ private fun TileGridCell( val targetValue = if (cell.isIcon) 0f else 1f val animatedProgress = remember { Animatable(targetValue) } - if (selected) { - val resizingState = selectionState.resizingState - LaunchedEffect(targetValue, resizingState) { - if (resizingState == null) { - animatedProgress.animateTo(targetValue) - } else { - snapshotFlow { resizingState.progression } - .collectLatest { animatedProgress.snapTo(it) } - } + val resizingState = selectionState.resizingState?.takeIf { selected } + LaunchedEffect(targetValue, resizingState) { + if (resizingState == null) { + animatedProgress.animateTo(targetValue) + } else { + snapshotFlow { resizingState.progression } + .collectLatest { animatedProgress.snapTo(it) } } } @@ -705,7 +719,6 @@ private fun Modifier.tileBackground(color: Color): Modifier { private object EditModeTileDefaults { const val PLACEHOLDER_ALPHA = .3f - val EditGridHeaderHeight = 60.dp val CurrentTilesGridPadding = 8.dp @Composable diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index 5ac2ad02d671..29ff1715dea2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -79,7 +79,8 @@ constructor( } val columns = columnsWithMediaViewModel.columns - val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width()) } + val largeTilesSpan by iconTilesViewModel.largeTilesSpanState + val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width(largeTilesSpan)) } val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle() @@ -129,21 +130,23 @@ constructor( viewModel.columnsWithMediaViewModelFactory.createWithoutMediaTracking() } val columns = columnsViewModel.columns + val largeTilesSpan by iconTilesViewModel.largeTilesSpanState val largeTiles by iconTilesViewModel.largeTiles.collectAsStateWithLifecycle() // Non-current tiles should always be displayed as icon tiles. val sizedTiles = - remember(tiles, largeTiles) { + remember(tiles, largeTiles, largeTilesSpan) { tiles.map { SizedTileImpl( it, - if (!it.isCurrent || !largeTiles.contains(it.tileSpec)) 1 else 2, + if (!it.isCurrent || !largeTiles.contains(it.tileSpec)) 1 + else largeTilesSpan, ) } } val (currentTiles, otherTiles) = sizedTiles.partition { it.tile.isCurrent } - val currentListState = rememberEditListState(currentTiles, columns) + val currentListState = rememberEditListState(currentTiles, columns, largeTilesSpan) DefaultEditTileGrid( listState = currentListState, otherTiles = otherTiles, @@ -154,6 +157,7 @@ constructor( onResize = iconTilesViewModel::resize, onStopEditing = onStopEditing, onReset = viewModel::showResetDialog, + largeTilesSpan = largeTilesSpan, ) } @@ -171,7 +175,7 @@ constructor( .map { it.flatten().map { it.tile } } } - private fun TileSpec.width(): Int { - return if (iconTilesViewModel.isIconTile(this)) 1 else 2 + private fun TileSpec.width(largeSize: Int = iconTilesViewModel.largeTilesSpan.value): Int { + return if (iconTilesViewModel.isIconTile(this)) 1 else largeSize } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index 5bebdbc7a13e..9bbf290a53f0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -156,6 +156,14 @@ fun Tile( bounceEnd = currentBounceableInfo.bounceEnd, ), ) { expandable -> + val longClick: (() -> Unit)? = + { + hapticsViewModel?.setTileInteractionState( + TileHapticsViewModel.TileInteractionState.LONG_CLICKED + ) + tile.onLongClick(expandable) + } + .takeIf { uiState.handlesLongClick } TileContainer( onClick = { tile.onClick(expandable) @@ -166,12 +174,7 @@ fun Tile( coroutineScope.launch { currentBounceableInfo.bounceable.animateBounce() } } }, - onLongClick = { - hapticsViewModel?.setTileInteractionState( - TileHapticsViewModel.TileInteractionState.LONG_CLICKED - ) - tile.onLongClick(expandable) - }, + onLongClick = longClick, uiState = uiState, iconOnly = iconOnly, ) { @@ -192,14 +195,6 @@ fun Tile( tile.onSecondaryClick() } .takeIf { uiState.handlesSecondaryClick } - val longClick: (() -> Unit)? = - { - hapticsViewModel?.setTileInteractionState( - TileHapticsViewModel.TileInteractionState.LONG_CLICKED - ) - tile.onLongClick(expandable) - } - .takeIf { uiState.handlesLongClick } LargeTileContent( label = uiState.label, secondaryLabel = uiState.secondaryLabel, @@ -237,7 +232,7 @@ private fun TileExpandable( @Composable fun TileContainer( onClick: () -> Unit, - onLongClick: () -> Unit, + onLongClick: (() -> Unit)?, uiState: TileUiState, iconOnly: Boolean, content: @Composable BoxScope.() -> Unit, @@ -281,7 +276,7 @@ fun Modifier.tilePadding(): Modifier { @Composable fun Modifier.tileCombinedClickable( onClick: () -> Unit, - onLongClick: () -> Unit, + onLongClick: (() -> Unit)?, uiState: TileUiState, iconOnly: Boolean, ): Modifier { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt index 9552aa935bbf..41c3de55af70 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt @@ -22,7 +22,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.setValue import com.android.systemui.qs.panels.ui.compose.selection.ResizingDefaults.RESIZING_THRESHOLD -class ResizingState(private val widths: TileWidths, private val onResize: () -> Unit) { +class ResizingState(val widths: TileWidths, private val onResize: () -> Unit) { /** Total drag offset of this resize operation. */ private var totalOffset by mutableFloatStateOf(0f) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModel.kt index 9feaab83cc1f..a9d673aa7400 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModel.kt @@ -17,9 +17,13 @@ package com.android.systemui.qs.panels.ui.viewmodel import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.qs.panels.domain.interactor.DynamicIconTilesInteractor import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch /** View model to resize QS tiles down to icons when removed from the current tiles. */ class DynamicIconTilesViewModel @@ -28,10 +32,21 @@ constructor( interactorFactory: DynamicIconTilesInteractor.Factory, iconTilesViewModel: IconTilesViewModel, ) : IconTilesViewModel by iconTilesViewModel, ExclusiveActivatable() { + private val hydrator = Hydrator("DynamicIconTilesViewModel") private val interactor = interactorFactory.create() + val largeTilesSpanState = + hydrator.hydratedStateOf( + traceName = "largeTilesSpan", + source = iconTilesViewModel.largeTilesSpan, + ) + override suspend fun onActivated(): Nothing { - interactor.activate() + coroutineScope { + launch { hydrator.activate() } + launch { interactor.activate() } + awaitCancellation() + } } @AssistedFactory diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt index 4e698edf4e34..b8c5fbb72614 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt @@ -25,6 +25,8 @@ import kotlinx.coroutines.flow.StateFlow interface IconTilesViewModel { val largeTiles: StateFlow<Set<TileSpec>> + val largeTilesSpan: StateFlow<Int> + fun isIconTile(spec: TileSpec): Boolean fun resize(spec: TileSpec, toIcon: Boolean) @@ -34,6 +36,7 @@ interface IconTilesViewModel { class IconTilesViewModelImpl @Inject constructor(private val interactor: IconTilesInteractor) : IconTilesViewModel { override val largeTiles = interactor.largeTilesSpecs + override val largeTilesSpan = interactor.largeTilesSpan override fun isIconTile(spec: TileSpec): Boolean = interactor.isIconTile(spec) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt index 33ce5519b68c..adc4e4bf0870 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt @@ -70,18 +70,21 @@ constructor( source = quickQuickSettingsRowInteractor.rows, ) + private val largeTilesSpan by + hydrator.hydratedStateOf( + traceName = "largeTilesSpan", + source = iconTilesViewModel.largeTilesSpan, + ) + private val currentTiles by hydrator.hydratedStateOf(traceName = "currentTiles", source = tilesInteractor.currentTiles) val tileViewModels by derivedStateOf { currentTiles - .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width) } + .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width()) } .let { splitInRowsSequence(it, columns).take(rows).toList().flatten() } } - private val TileSpec.width: Int - get() = if (largeTiles.contains(this)) 2 else 1 - override suspend fun onActivated(): Nothing { coroutineScope { launch { hydrator.activate() } @@ -95,4 +98,6 @@ constructor( interface Factory { fun create(): QuickQuickSettingsViewModel } + + private fun TileSpec.width(): Int = if (largeTiles.contains(this)) largeTilesSpan else 1 } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java index f218d86a5aa1..37d24debe958 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java @@ -243,15 +243,15 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> { } mSelectedCard = cards.get(selectedIndex); switch (mSelectedCard.getCardImage().getType()) { + case TYPE_BITMAP: + case TYPE_ADAPTIVE_BITMAP: + mCardViewDrawable = mSelectedCard.getCardImage().loadDrawable(mContext); + break; case TYPE_URI: case TYPE_URI_ADAPTIVE_BITMAP: - mCardViewDrawable = null; - break; case TYPE_RESOURCE: - case TYPE_BITMAP: - case TYPE_ADAPTIVE_BITMAP: case TYPE_DATA: - mCardViewDrawable = mSelectedCard.getCardImage().loadDrawable(mContext); + mCardViewDrawable = null; break; default: Log.e(TAG, "Unknown icon type: " + mSelectedCard.getCardImage().getType()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt index f33b76b17f96..ff4760fd2837 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt @@ -18,8 +18,10 @@ package com.android.systemui.statusbar.core import android.view.Display import android.view.View +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.demomode.DemoModeController import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.DarkIconDispatcher @@ -46,12 +48,12 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.io.PrintWriter import java.util.Optional +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.filterNotNull -import com.android.app.tracing.coroutines.launchTraced as launch /** * Class responsible for managing the lifecycle and state of the status bar. @@ -68,6 +70,7 @@ constructor( @Assisted private val statusBarModeRepository: StatusBarModePerDisplayRepository, @Assisted private val statusBarInitializer: StatusBarInitializer, @Assisted private val statusBarWindowController: StatusBarWindowController, + @Main private val mainContext: CoroutineContext, private val demoModeController: DemoModeController, private val pluginDependencyProvider: PluginDependencyProvider, private val autoHideController: AutoHideController, @@ -141,7 +144,8 @@ constructor( override fun start() { StatusBarConnectedDisplays.assertInNewMode() coroutineScope - .launch { + // Perform animations on the main thread to prevent crashes. + .launch(context = mainContext) { dumpManager.registerCriticalDumpable(dumpableName, this@StatusBarOrchestrator) launch { controllerAndBouncerShowing.collect { (controller, bouncerShowing) -> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt index 958001625a07..1f8d365cfdad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt @@ -102,6 +102,10 @@ class NotifCollectionCache<V>( return --lives <= 0 } } + + override fun toString(): String { + return "$key = $value" + } } /** @@ -174,7 +178,10 @@ class NotifCollectionCache<V>( pw.println("$TAG(retainCount = $retainCount, purgeTimeoutMillis = $purgeTimeoutMillis)") pw.withIncreasedIndent { - pw.printCollection("keys present in cache", cache.keys.stream().sorted().toList()) + pw.printCollection( + "entries present in cache", + cache.values.stream().map { it.toString() }.sorted().toList(), + ) val misses = misses.get() val hits = hits.get() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt index 9cbfc440ab16..94e9d26c9dc8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt @@ -21,6 +21,7 @@ import android.telephony.ServiceState import android.telephony.SignalStrength import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyManager +import android.telephony.satellite.NtnSignalStrength import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.MobileMappings import com.android.systemui.dagger.SysUISingleton @@ -31,11 +32,7 @@ import javax.inject.Inject /** Logs for inputs into the mobile pipeline. */ @SysUISingleton -class MobileInputLogger -@Inject -constructor( - @MobileInputLog private val buffer: LogBuffer, -) { +class MobileInputLogger @Inject constructor(@MobileInputLog private val buffer: LogBuffer) { fun logOnServiceStateChanged(serviceState: ServiceState, subId: Int) { buffer.log( TAG, @@ -49,7 +46,7 @@ constructor( { "onServiceStateChanged: subId=$int1 emergencyOnly=$bool1 roaming=$bool2" + " operator=$str1" - } + }, ) } @@ -61,7 +58,7 @@ constructor( int1 = subId bool1 = serviceState.isEmergencyOnly }, - { "ACTION_SERVICE_STATE for subId=$int1. ServiceState.isEmergencyOnly=$bool1" } + { "ACTION_SERVICE_STATE for subId=$int1. ServiceState.isEmergencyOnly=$bool1" }, ) } @@ -70,7 +67,7 @@ constructor( TAG, LogLevel.INFO, { int1 = subId }, - { "ACTION_SERVICE_STATE for subId=$int1. Intent is missing extras. Ignoring" } + { "ACTION_SERVICE_STATE for subId=$int1. Intent is missing extras. Ignoring" }, ) } @@ -82,7 +79,16 @@ constructor( int1 = subId str1 = signalStrength.toString() }, - { "onSignalStrengthsChanged: subId=$int1 strengths=$str1" } + { "onSignalStrengthsChanged: subId=$int1 strengths=$str1" }, + ) + } + + fun logNtnSignalStrengthChanged(signalStrength: NtnSignalStrength) { + buffer.log( + TAG, + LogLevel.INFO, + { int1 = signalStrength.level }, + { "onCarrierRoamingNtnSignalStrengthChanged: level=$int1" }, ) } @@ -128,7 +134,7 @@ constructor( TAG, LogLevel.INFO, { bool1 = active }, - { "onCarrierRoamingNtnModeChanged: $bool1" } + { "onCarrierRoamingNtnModeChanged: $bool1" }, ) } @@ -146,12 +152,7 @@ constructor( } fun logCarrierConfigChanged(subId: Int) { - buffer.log( - TAG, - LogLevel.INFO, - { int1 = subId }, - { "onCarrierConfigChanged: subId=$int1" }, - ) + buffer.log(TAG, LogLevel.INFO, { int1 = subId }, { "onCarrierConfigChanged: subId=$int1" }) } fun logOnDataEnabledChanged(enabled: Boolean, subId: Int) { @@ -175,7 +176,7 @@ constructor( TAG, LogLevel.INFO, { str1 = config.toString() }, - { "defaultDataSubRatConfig: $str1" } + { "defaultDataSubRatConfig: $str1" }, ) } @@ -184,7 +185,7 @@ constructor( TAG, LogLevel.INFO, { str1 = mapping.toString() }, - { "defaultMobileIconMapping: $str1" } + { "defaultMobileIconMapping: $str1" }, ) } @@ -216,7 +217,7 @@ constructor( { "Intent: ACTION_SERVICE_PROVIDERS_UPDATED." + " showSpn=$bool1 spn=$str1 dataSpn=$str2 showPlmn=$bool2 plmn=$str3" - } + }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index 205205eac210..07843f1ef041 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -107,6 +107,12 @@ interface MobileConnectionRepository { // @IntRange(from = 0, to = 4) val primaryLevel: StateFlow<Int> + /** + * This level can be used to reflect the signal strength when in carrier roaming NTN mode + * (carrier-based satellite) + */ + val satelliteLevel: StateFlow<Int> + /** The current data connection state. See [DataConnectionState] */ val dataConnectionState: StateFlow<DataConnectionState> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt index 3261b71ece3c..be3977ecd4ba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt @@ -37,12 +37,14 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullM import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_OPERATOR import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_PRIMARY_LEVEL import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_ROAMING +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_SATELLITE_LEVEL import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -75,7 +77,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = "inflate", - _inflateSignalStrength.value + _inflateSignalStrength.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _inflateSignalStrength.value) @@ -89,7 +91,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_EMERGENCY, - _isEmergencyOnly.value + _isEmergencyOnly.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _isEmergencyOnly.value) @@ -100,7 +102,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_ROAMING, - _isRoaming.value + _isRoaming.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _isRoaming.value) @@ -111,7 +113,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_OPERATOR, - _operatorAlphaShort.value + _operatorAlphaShort.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _operatorAlphaShort.value) @@ -122,7 +124,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_IS_IN_SERVICE, - _isInService.value + _isInService.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _isInService.value) @@ -133,7 +135,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_IS_NTN, - _isNonTerrestrial.value + _isNonTerrestrial.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _isNonTerrestrial.value) @@ -144,7 +146,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_IS_GSM, - _isGsm.value + _isGsm.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _isGsm.value) @@ -155,7 +157,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_CDMA_LEVEL, - _cdmaLevel.value + _cdmaLevel.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _cdmaLevel.value) @@ -166,10 +168,21 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_PRIMARY_LEVEL, - _primaryLevel.value + _primaryLevel.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _primaryLevel.value) + private val _satelliteLevel = MutableStateFlow(0) + override val satelliteLevel: StateFlow<Int> = + _satelliteLevel + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_SATELLITE_LEVEL, + _satelliteLevel.value, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _satelliteLevel.value) + private val _dataConnectionState = MutableStateFlow(DataConnectionState.Disconnected) override val dataConnectionState = _dataConnectionState @@ -177,12 +190,7 @@ class DemoMobileConnectionRepository( .stateIn(scope, SharingStarted.WhileSubscribed(), _dataConnectionState.value) private val _dataActivityDirection = - MutableStateFlow( - DataActivityModel( - hasActivityIn = false, - hasActivityOut = false, - ) - ) + MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) override val dataActivityDirection = _dataActivityDirection .logDiffsForTable(tableLogBuffer, columnPrefix = "", _dataActivityDirection.value) @@ -195,7 +203,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_CARRIER_NETWORK_CHANGE, - _carrierNetworkChangeActive.value + _carrierNetworkChangeActive.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _carrierNetworkChangeActive.value) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt index 2e4767893c3d..75f613d7e3c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt @@ -90,7 +90,7 @@ class CarrierMergedConnectionRepository( TAG, "Connection repo subId=$subId " + "does not equal wifi repo subId=${network.subscriptionId}; " + - "not showing carrier merged" + "not showing carrier merged", ) null } @@ -149,7 +149,7 @@ class CarrierMergedConnectionRepository( .stateIn( scope, SharingStarted.WhileSubscribed(), - ResolvedNetworkType.UnknownNetworkType + ResolvedNetworkType.UnknownNetworkType, ) override val dataConnectionState = @@ -173,6 +173,7 @@ class CarrierMergedConnectionRepository( override val isNonTerrestrial = MutableStateFlow(false).asStateFlow() override val isGsm = MutableStateFlow(false).asStateFlow() override val carrierNetworkChangeActive = MutableStateFlow(false).asStateFlow() + override val satelliteLevel = MutableStateFlow(0) /** * Carrier merged connections happen over wifi but are displayed as a mobile triangle. Because @@ -207,10 +208,7 @@ class CarrierMergedConnectionRepository( @Application private val scope: CoroutineScope, private val wifiRepository: WifiRepository, ) { - fun build( - subId: Int, - mobileLogger: TableLogBuffer, - ): MobileConnectionRepository { + fun build(subId: Int, mobileLogger: TableLogBuffer): MobileConnectionRepository { return CarrierMergedConnectionRepository( subId, mobileLogger, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt index a5e47a6e68cd..fae9be083e12 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt @@ -132,12 +132,12 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_EMERGENCY, - activeRepo.value.isEmergencyOnly.value + activeRepo.value.isEmergencyOnly.value, ) .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.isEmergencyOnly.value + activeRepo.value.isEmergencyOnly.value, ) override val isRoaming = @@ -147,7 +147,7 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_ROAMING, - activeRepo.value.isRoaming.value + activeRepo.value.isRoaming.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isRoaming.value) @@ -158,12 +158,12 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_OPERATOR, - activeRepo.value.operatorAlphaShort.value + activeRepo.value.operatorAlphaShort.value, ) .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.operatorAlphaShort.value + activeRepo.value.operatorAlphaShort.value, ) override val isInService = @@ -173,7 +173,7 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_IS_IN_SERVICE, - activeRepo.value.isInService.value + activeRepo.value.isInService.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isInService.value) @@ -184,12 +184,12 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_IS_NTN, - activeRepo.value.isNonTerrestrial.value + activeRepo.value.isNonTerrestrial.value, ) .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.isNonTerrestrial.value + activeRepo.value.isNonTerrestrial.value, ) override val isGsm = @@ -199,7 +199,7 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_IS_GSM, - activeRepo.value.isGsm.value + activeRepo.value.isGsm.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isGsm.value) @@ -210,7 +210,7 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_CDMA_LEVEL, - activeRepo.value.cdmaLevel.value + activeRepo.value.cdmaLevel.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaLevel.value) @@ -221,22 +221,33 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_PRIMARY_LEVEL, - activeRepo.value.primaryLevel.value + activeRepo.value.primaryLevel.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.primaryLevel.value) + override val satelliteLevel: StateFlow<Int> = + activeRepo + .flatMapLatest { it.satelliteLevel } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_SATELLITE_LEVEL, + activeRepo.value.satelliteLevel.value, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.satelliteLevel.value) + override val dataConnectionState = activeRepo .flatMapLatest { it.dataConnectionState } .logDiffsForTable( tableLogBuffer, columnPrefix = "", - activeRepo.value.dataConnectionState.value + activeRepo.value.dataConnectionState.value, ) .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.dataConnectionState.value + activeRepo.value.dataConnectionState.value, ) override val dataActivityDirection = @@ -245,12 +256,12 @@ class FullMobileConnectionRepository( .logDiffsForTable( tableLogBuffer, columnPrefix = "", - activeRepo.value.dataActivityDirection.value + activeRepo.value.dataActivityDirection.value, ) .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.dataActivityDirection.value + activeRepo.value.dataActivityDirection.value, ) override val carrierNetworkChangeActive = @@ -260,12 +271,12 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_CARRIER_NETWORK_CHANGE, - activeRepo.value.carrierNetworkChangeActive.value + activeRepo.value.carrierNetworkChangeActive.value, ) .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.carrierNetworkChangeActive.value + activeRepo.value.carrierNetworkChangeActive.value, ) override val resolvedNetworkType = @@ -274,12 +285,12 @@ class FullMobileConnectionRepository( .logDiffsForTable( tableLogBuffer, columnPrefix = "", - activeRepo.value.resolvedNetworkType.value + activeRepo.value.resolvedNetworkType.value, ) .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.resolvedNetworkType.value + activeRepo.value.resolvedNetworkType.value, ) override val dataEnabled = @@ -305,7 +316,7 @@ class FullMobileConnectionRepository( .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.inflateSignalStrength.value + activeRepo.value.inflateSignalStrength.value, ) override val allowNetworkSliceIndicator = @@ -320,7 +331,7 @@ class FullMobileConnectionRepository( .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.allowNetworkSliceIndicator.value + activeRepo.value.allowNetworkSliceIndicator.value, ) override val numberOfLevels = @@ -439,6 +450,7 @@ class FullMobileConnectionRepository( const val COL_IS_IN_SERVICE = "isInService" const val COL_OPERATOR = "operatorName" const val COL_PRIMARY_LEVEL = "primaryLevel" + const val COL_SATELLITE_LEVEL = "satelliteLevel" const val COL_ROAMING = "roaming" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt index 62bd8ad4317c..8a1e7f9a0096 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt @@ -41,6 +41,7 @@ import android.telephony.TelephonyManager.ERI_ON import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID +import android.telephony.satellite.NtnSignalStrength import com.android.settingslib.Utils import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow @@ -173,7 +174,7 @@ class MobileConnectionRepositoryImpl( override fun onDataConnectionStateChanged( dataState: Int, - networkType: Int + networkType: Int, ) { logger.logOnDataConnectionStateChanged(dataState, networkType, subId) trySend(CallbackEvent.OnDataConnectionStateChanged(dataState)) @@ -195,6 +196,17 @@ class MobileConnectionRepositoryImpl( logger.logOnSignalStrengthsChanged(signalStrength, subId) trySend(CallbackEvent.OnSignalStrengthChanged(signalStrength)) } + + override fun onCarrierRoamingNtnSignalStrengthChanged( + signalStrength: NtnSignalStrength + ) { + logger.logNtnSignalStrengthChanged(signalStrength) + trySend( + CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged( + signalStrength + ) + ) + } } telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } @@ -267,6 +279,12 @@ class MobileConnectionRepositoryImpl( .map { it.signalStrength.level } .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + override val satelliteLevel: StateFlow<Int> = + callbackEvents + .mapNotNull { it.onCarrierRoamingNtnSignalStrengthChanged } + .map { it.signalStrength.level } + .stateIn(scope, SharingStarted.WhileSubscribed(), 0) + override val dataConnectionState = callbackEvents .mapNotNull { it.onDataConnectionStateChanged } @@ -280,7 +298,7 @@ class MobileConnectionRepositoryImpl( .stateIn( scope, SharingStarted.WhileSubscribed(), - DataActivityModel(hasActivityIn = false, hasActivityOut = false) + DataActivityModel(hasActivityIn = false, hasActivityOut = false), ) override val carrierNetworkChangeActive = @@ -385,7 +403,7 @@ class MobileConnectionRepositoryImpl( if ( intent.getIntExtra( EXTRA_SUBSCRIPTION_INDEX, - INVALID_SUBSCRIPTION_ID + INVALID_SUBSCRIPTION_ID, ) == subId ) { logger.logServiceProvidersUpdatedBroadcast(intent) @@ -399,7 +417,7 @@ class MobileConnectionRepositoryImpl( context.registerReceiver( receiver, - IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED) + IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED), ) awaitClose { context.unregisterReceiver(receiver) } @@ -524,6 +542,9 @@ sealed interface CallbackEvent { data class OnServiceStateChanged(val serviceState: ServiceState) : CallbackEvent data class OnSignalStrengthChanged(val signalStrength: SignalStrength) : CallbackEvent + + data class OnCarrierRoamingNtnSignalStrengthChanged(val signalStrength: NtnSignalStrength) : + CallbackEvent } /** @@ -539,6 +560,9 @@ data class TelephonyCallbackState( val onDisplayInfoChanged: CallbackEvent.OnDisplayInfoChanged? = null, val onServiceStateChanged: CallbackEvent.OnServiceStateChanged? = null, val onSignalStrengthChanged: CallbackEvent.OnSignalStrengthChanged? = null, + val onCarrierRoamingNtnSignalStrengthChanged: + CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged? = + null, ) { fun applyEvent(event: CallbackEvent): TelephonyCallbackState { return when (event) { @@ -555,6 +579,8 @@ data class TelephonyCallbackState( copy(onServiceStateChanged = event) } is CallbackEvent.OnSignalStrengthChanged -> copy(onSignalStrengthChanged = event) + is CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged -> + copy(onCarrierRoamingNtnSignalStrengthChanged = event) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 4ef328cf1623..1bf14af7ea6e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -335,7 +335,11 @@ class MobileIconInteractorImpl( // Satellite level is unaffected by the inflateSignalStrength property // See b/346904529 for details private val satelliteShownLevel: StateFlow<Int> = - combine(level, isInService) { level, isInService -> if (isInService) level else 0 } + if (Flags.carrierRoamingNbIotNtn()) { + connectionRepository.satelliteLevel + } else { + combine(level, isInService) { level, isInService -> if (isInService) level else 0 } + } .stateIn(scope, SharingStarted.WhileSubscribed(), 0) private val cellularIcon: Flow<SignalIconModel.Cellular> = diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java index 53e6b4f82b7e..761993b3cda7 100644 --- a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java @@ -330,13 +330,19 @@ public class WalletScreenController implements QAWalletCardViewInfo(Context context, WalletCard walletCard) { mWalletCard = walletCard; Icon cardImageIcon = mWalletCard.getCardImage(); - if (cardImageIcon.getType() == Icon.TYPE_URI) { - mCardDrawable = null; - } else { + if (cardImageIcon.getType() == Icon.TYPE_BITMAP + || cardImageIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) { mCardDrawable = mWalletCard.getCardImage().loadDrawable(context); + } else { + mCardDrawable = null; } Icon icon = mWalletCard.getCardIcon(); - mIconDrawable = icon == null ? null : icon.loadDrawable(context); + if (icon != null && (icon.getType() == Icon.TYPE_BITMAP + || icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP)) { + mIconDrawable = icon.loadDrawable(context); + } else { + mIconDrawable = null; + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java index 1a399341f12c..ca9b866e2d18 100644 --- a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java @@ -283,6 +283,11 @@ public class WalletView extends FrameLayout implements WalletCardCarousel.OnCard return mCardLabel; } + @VisibleForTesting + ImageView getIcon() { + return mIcon; + } + @Nullable private static Drawable getHeaderIcon(Context context, WalletCardViewInfo walletCard) { Drawable icon = walletCard.getIcon(); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 2b167e4c5da4..65b62737b692 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -56,6 +56,7 @@ import com.android.systemui.plugins.clocks.ThemeConfig import com.android.systemui.plugins.clocks.ZenData import com.android.systemui.plugins.clocks.ZenData.ZenMode import com.android.systemui.res.R +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ZenModeController @@ -128,6 +129,7 @@ class ClockEventControllerTest : SysuiTestCase() { @Mock private lateinit var largeClockEvents: ClockFaceEvents @Mock private lateinit var parentView: View @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor + @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var zenModeController: ZenModeController private var zenModeControllerCallback: ZenModeController.Callback? = null @@ -153,6 +155,7 @@ class ClockEventControllerTest : SysuiTestCase() { .thenReturn(ClockFaceConfig(tickRate = ClockTickRate.PER_MINUTE)) whenever(smallClockController.theme).thenReturn(ThemeConfig(true, null)) whenever(largeClockController.theme).thenReturn(ThemeConfig(true, null)) + whenever(userTracker.userId).thenReturn(1) zenModeRepository.addMode(MANUAL_DND_INACTIVE) @@ -177,6 +180,7 @@ class ClockEventControllerTest : SysuiTestCase() { withDeps.featureFlags, zenModeController, kosmos.zenModeInteractor, + userTracker, ) underTest.clock = clock diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java index 96f4a60271d2..b4c69529741e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java @@ -46,7 +46,6 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.InstanceId; import com.android.internal.logging.UiEventLogger; -import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -222,7 +221,6 @@ public class DozeTriggersTest extends SysuiTestCase { } @Test - @EnableFlags(Flags.FLAG_NOTIFICATION_PULSING_FIX) public void testOnNotification_alreadyPulsing_notificationNotSuppressed() { // GIVEN device is pulsing Runnable pulseSuppressListener = mock(Runnable.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt index fb376ce3ca40..3ddd4b58211d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt @@ -289,6 +289,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa } verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor)) mediaControllerFactory.setControllerForToken(session.sessionToken, controller) + whenever(controller.sessionToken).thenReturn(session.sessionToken) whenever(controller.transportControls).thenReturn(transportControls) whenever(controller.playbackInfo).thenReturn(playbackInfo) whenever(controller.metadata).thenReturn(metadataBuilder.build()) @@ -1599,6 +1600,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testTooManyCompactActions_isTruncated() { // GIVEN a notification where too many compact actions were specified @@ -1635,6 +1637,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa .isEqualTo(LegacyMediaDataManagerImpl.MAX_COMPACT_ACTIONS) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testTooManyNotificationActions_isTruncated() { // GIVEN a notification where too many notification actions are added @@ -1670,6 +1673,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa .isEqualTo(LegacyMediaDataManagerImpl.MAX_NOTIFICATION_ACTIONS) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_noState_usesNotification() { val desc = "Notification Action" @@ -1703,6 +1707,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa assertThat(mediaDataCaptor.value!!.actions[0]!!.contentDescription).isEqualTo(desc) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_hasPrevNext() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") @@ -1746,6 +1751,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1]) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_noPrevNext_usesCustom() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5") @@ -1778,6 +1784,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[3]) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_connecting() { val stateActions = PlaybackState.ACTION_PLAY @@ -1797,6 +1804,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa .isEqualTo(context.getString(R.string.controls_media_button_connecting)) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_reservedSpace() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") @@ -1835,6 +1843,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa assertThat(actions.reservePrev).isTrue() } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_playPause_hasButton() { val stateActions = PlaybackState.ACTION_PLAY_PAUSE @@ -1998,6 +2007,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa assertThat(mediaDataCaptor.value.semanticActions).isNotNull() } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackStateNull_Pause_keyExists_callsListener() { whenever(controller.playbackState).thenReturn(null) @@ -2056,6 +2066,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa assertThat(mediaDataCaptor.value.isClearable).isFalse() } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testRetain_notifPlayer_notifRemoved_setToResume() { fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) @@ -2086,6 +2097,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa ) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() { fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) @@ -2104,6 +2116,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean()) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() { fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt index 7d364bd832f2..e5483c0980c0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt @@ -103,7 +103,6 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito -import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify @@ -113,6 +112,7 @@ import org.mockito.junit.MockitoJUnit import org.mockito.kotlin.any import org.mockito.kotlin.capture import org.mockito.kotlin.eq +import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import platform.test.runner.parameterized.ParameterizedAndroidJunit4 @@ -312,6 +312,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { } verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor)) mediaControllerFactory.setControllerForToken(session.sessionToken, controller) + whenever(controller.sessionToken).thenReturn(session.sessionToken) whenever(controller.transportControls).thenReturn(transportControls) whenever(controller.playbackInfo).thenReturn(playbackInfo) whenever(controller.metadata).thenReturn(metadataBuilder.build()) @@ -596,7 +597,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { fun testOnNotificationAdded_emptyTitle_hasPlaceholder() { // When the manager has a notification with an empty title, and the app is not // required to include a non-empty title - val mockPackageManager = mock(PackageManager::class.java) + val mockPackageManager = mock<PackageManager>() context.setMockPackageManager(mockPackageManager) whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME) whenever(controller.metadata) @@ -626,7 +627,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { fun testOnNotificationAdded_blankTitle_hasPlaceholder() { // GIVEN that the manager has a notification with a blank title, and the app is not // required to include a non-empty title - val mockPackageManager = mock(PackageManager::class.java) + val mockPackageManager = mock<PackageManager>() context.setMockPackageManager(mockPackageManager) whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME) whenever(controller.metadata) @@ -656,7 +657,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { fun testOnNotificationAdded_emptyMetadata_usesNotificationTitle() { // When the app sets the metadata title fields to empty strings, but does include a // non-blank notification title - val mockPackageManager = mock(PackageManager::class.java) + val mockPackageManager = mock<PackageManager>() context.setMockPackageManager(mockPackageManager) whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME) whenever(controller.metadata) @@ -1610,6 +1611,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testTooManyCompactActions_isTruncated() { // GIVEN a notification where too many compact actions were specified @@ -1646,6 +1648,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { .isEqualTo(MediaDataProcessor.MAX_COMPACT_ACTIONS) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testTooManyNotificationActions_isTruncated() { // GIVEN a notification where too many notification actions are added @@ -1681,6 +1684,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { .isEqualTo(MediaDataProcessor.MAX_NOTIFICATION_ACTIONS) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_noState_usesNotification() { val desc = "Notification Action" @@ -1714,6 +1718,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(mediaDataCaptor.value!!.actions[0]!!.contentDescription).isEqualTo(desc) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_hasPrevNext() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") @@ -1757,6 +1762,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1]) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_noPrevNext_usesCustom() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5") @@ -1789,6 +1795,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[3]) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_connecting() { val stateActions = PlaybackState.ACTION_PLAY @@ -1874,6 +1881,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { .isNotEqualTo(firstSemanticActions.prevOrCustom?.icon) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_reservedSpace() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") @@ -1912,6 +1920,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(actions.reservePrev).isTrue() } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_playPause_hasButton() { val stateActions = PlaybackState.ACTION_PLAY_PAUSE @@ -2074,6 +2083,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(mediaDataCaptor.value.semanticActions).isNotNull() } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackStateNull_Pause_keyExists_callsListener() { whenever(controller.playbackState).thenReturn(null) @@ -2132,6 +2142,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(mediaDataCaptor.value.isClearable).isFalse() } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testRetain_notifPlayer_notifRemoved_setToResume() { fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) @@ -2162,6 +2173,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { ) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() { fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) @@ -2180,6 +2192,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean()) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() { fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt index 8a6df1cbb4de..d88d69da5e59 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt @@ -63,6 +63,7 @@ class DragAndDropTest : SysuiTestCase() { listState = listState, otherTiles = listOf(), columns = 4, + largeTilesSpan = 4, modifier = Modifier.fillMaxSize(), onRemoveTile = {}, onSetTiles = onSetTiles, @@ -75,7 +76,7 @@ class DragAndDropTest : SysuiTestCase() { @Test fun draggedTile_shouldDisappear() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) } @@ -101,7 +102,7 @@ class DragAndDropTest : SysuiTestCase() { @Test fun draggedTile_shouldChangePosition() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) } @@ -128,7 +129,7 @@ class DragAndDropTest : SysuiTestCase() { @Test fun draggedTileOut_shouldBeRemoved() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) } @@ -153,7 +154,7 @@ class DragAndDropTest : SysuiTestCase() { @Test fun draggedNewTileIn_shouldBeAdded() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt index d9c1d998798c..fac5ecb49027 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt @@ -62,6 +62,7 @@ class ResizingTest : SysuiTestCase() { listState = listState, otherTiles = listOf(), columns = 4, + largeTilesSpan = 4, modifier = Modifier.fillMaxSize(), onRemoveTile = {}, onSetTiles = {}, @@ -74,7 +75,7 @@ class ResizingTest : SysuiTestCase() { @Test fun toggleIconTile_shouldBeLarge() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } } @@ -90,7 +91,7 @@ class ResizingTest : SysuiTestCase() { @Test fun toggleLargeTile_shouldBeIcon() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } } @@ -106,7 +107,7 @@ class ResizingTest : SysuiTestCase() { @Test fun resizedLarge_shouldBeIcon() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } } @@ -126,7 +127,7 @@ class ResizingTest : SysuiTestCase() { @Test fun resizedIcon_shouldBeLarge() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java index 38a61fecdc8a..21adeb01487b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java @@ -316,6 +316,31 @@ public class WalletScreenControllerTest extends SysuiTestCase { } @Test + public void queryCards_hasCards_showCarousel_invalidIconSource_noIcon() { + GetWalletCardsResponse response = + new GetWalletCardsResponse( + Collections.singletonList(createWalletCardWithInvalidIcon(mContext)), 0); + + mController.queryWalletCards(); + mTestableLooper.processAllMessages(); + + verify(mWalletClient).getWalletCards(any(), any(), mCallbackCaptor.capture()); + + QuickAccessWalletClient.OnWalletCardsRetrievedCallback callback = + mCallbackCaptor.getValue(); + + assertEquals(mController, callback); + + callback.onWalletCardsRetrieved(response); + mTestableLooper.processAllMessages(); + + assertEquals(VISIBLE, mWalletView.getCardCarousel().getVisibility()); + assertEquals(GONE, mWalletView.getEmptyStateView().getVisibility()); + assertEquals(GONE, mWalletView.getErrorView().getVisibility()); + assertEquals(null, mWalletView.getIcon().getDrawable()); + } + + @Test public void queryCards_noCards_showEmptyState() { GetWalletCardsResponse response = new GetWalletCardsResponse(Collections.EMPTY_LIST, 0); @@ -507,6 +532,16 @@ public class WalletScreenControllerTest extends SysuiTestCase { .build(); } + private WalletCard createWalletCardWithInvalidIcon(Context context) { + PendingIntent pendingIntent = + PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE); + return new WalletCard.Builder( + CARD_ID_1, createIconWithInvalidSource(), "•••• 1234", pendingIntent) + .setCardIcon(createIconWithInvalidSource()) + .setCardLabel("Hold to reader") + .build(); + } + private WalletCard createCrazyWalletCard(Context context, boolean hasLabel) { PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE); @@ -520,6 +555,10 @@ public class WalletScreenControllerTest extends SysuiTestCase { return Icon.createWithBitmap(Bitmap.createBitmap(70, 44, Bitmap.Config.ARGB_8888)); } + private static Icon createIconWithInvalidSource() { + return Icon.createWithContentUri("content://media/external/images/media"); + } + private WalletCardViewInfo createCardViewInfo(WalletCard walletCard) { return new WalletScreenController.QAWalletCardViewInfo( mContext, walletCard); diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 48106de5225b..fc318d56a8d5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -2395,7 +2395,8 @@ public class BubblesTest extends SysuiTestCase { FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); - mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT); + mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT, + BubbleBarLocation.UpdateSource.DRAG_EXP_VIEW); assertThat(bubbleStateListener.mLastUpdate).isNotNull(); assertThat(bubbleStateListener.mLastUpdate.bubbleBarLocation).isEqualTo( BubbleBarLocation.LEFT); @@ -2408,7 +2409,8 @@ public class BubblesTest extends SysuiTestCase { FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); - mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT); + mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT, + BubbleBarLocation.UpdateSource.DRAG_EXP_VIEW); assertThat(bubbleStateListener.mStateChangeCalls).isEqualTo(0); } @@ -2535,6 +2537,78 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test + public void testEventLogging_bubbleBar_dragBarLeft() { + mBubbleProperties.mIsBubbleBarEnabled = true; + mPositioner.setIsLargeScreen(true); + mPositioner.setBubbleBarLocation(BubbleBarLocation.RIGHT); + FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); + mBubbleController.registerBubbleStateListener(bubbleStateListener); + + mEntryListener.onEntryAdded(mRow); + assertBarMode(); + + mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT, + BubbleBarLocation.UpdateSource.DRAG_BAR); + + verify(mBubbleLogger).log(BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_BAR); + } + + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) + @Test + public void testEventLogging_bubbleBar_dragBarRight() { + mBubbleProperties.mIsBubbleBarEnabled = true; + mPositioner.setIsLargeScreen(true); + mPositioner.setBubbleBarLocation(BubbleBarLocation.LEFT); + FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); + mBubbleController.registerBubbleStateListener(bubbleStateListener); + + mEntryListener.onEntryAdded(mRow); + assertBarMode(); + + mBubbleController.setBubbleBarLocation(BubbleBarLocation.RIGHT, + BubbleBarLocation.UpdateSource.DRAG_BAR); + + verify(mBubbleLogger).log(BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_DRAG_BAR); + } + + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) + @Test + public void testEventLogging_bubbleBar_dragBubbleLeft() { + mBubbleProperties.mIsBubbleBarEnabled = true; + mPositioner.setIsLargeScreen(true); + mPositioner.setBubbleBarLocation(BubbleBarLocation.RIGHT); + FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); + mBubbleController.registerBubbleStateListener(bubbleStateListener); + + mEntryListener.onEntryAdded(mRow); + assertBarMode(); + + mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT, + BubbleBarLocation.UpdateSource.DRAG_BUBBLE); + + verify(mBubbleLogger).log(BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_BUBBLE); + } + + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) + @Test + public void testEventLogging_bubbleBar_dragBubbleRight() { + mBubbleProperties.mIsBubbleBarEnabled = true; + mPositioner.setIsLargeScreen(true); + mPositioner.setBubbleBarLocation(BubbleBarLocation.LEFT); + FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); + mBubbleController.registerBubbleStateListener(bubbleStateListener); + + mEntryListener.onEntryAdded(mRow); + assertBarMode(); + + mBubbleController.setBubbleBarLocation(BubbleBarLocation.RIGHT, + BubbleBarLocation.UpdateSource.DRAG_BUBBLE); + + verify(mBubbleLogger).log(BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_DRAG_BUBBLE); + } + + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) + @Test public void testEventLogging_bubbleBar_expandAndCollapse() { mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt index 219794f3ad18..a7917a0866bb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt @@ -37,9 +37,7 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.currentTime -class FakeAuthenticationRepository( - private val currentTime: () -> Long, -) : AuthenticationRepository { +class FakeAuthenticationRepository(private val currentTime: () -> Long) : AuthenticationRepository { override val hintedPinLength: Int = HINTING_PIN_LENGTH @@ -72,6 +70,9 @@ class FakeAuthenticationRepository( private val credentialCheckingMutex = Mutex(locked = false) + var maximumTimeToLock: Long = 0 + var powerButtonInstantlyLocks: Boolean = true + override suspend fun getAuthenticationMethod(): AuthenticationMethodModel { return authenticationMethod.value } @@ -114,6 +115,7 @@ class FakeAuthenticationRepository( MAX_FAILED_AUTH_TRIES_BEFORE_WIPE var profileWithMinFailedUnlockAttemptsForWipe: Int = UserHandle.USER_SYSTEM + override suspend fun getProfileWithMinFailedUnlockAttemptsForWipe(): Int = profileWithMinFailedUnlockAttemptsForWipe @@ -144,10 +146,7 @@ class FakeAuthenticationRepository( val failedAttempts = _failedAuthenticationAttempts.value if (isSuccessful || failedAttempts < MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) { - AuthenticationResultModel( - isSuccessful = isSuccessful, - lockoutDurationMs = 0, - ) + AuthenticationResultModel(isSuccessful = isSuccessful, lockoutDurationMs = 0) } else { AuthenticationResultModel( isSuccessful = false, @@ -178,6 +177,14 @@ class FakeAuthenticationRepository( credentialCheckingMutex.unlock() } + override suspend fun getMaximumTimeToLock(): Long { + return maximumTimeToLock + } + + override suspend fun getPowerButtonInstantlyLocks(): Boolean { + return powerButtonInstantlyLocks + } + private fun getExpectedCredential(securityMode: SecurityMode): List<Any> { return when (val credentialType = getCurrentCredentialType(securityMode)) { LockPatternUtils.CREDENTIAL_TYPE_PIN -> credentialOverride ?: DEFAULT_PIN @@ -219,9 +226,7 @@ class FakeAuthenticationRepository( } @LockPatternUtils.CredentialType - private fun getCurrentCredentialType( - securityMode: SecurityMode, - ): Int { + private fun getCurrentCredentialType(securityMode: SecurityMode): Int { return when (securityMode) { SecurityMode.PIN, SecurityMode.SimPin, @@ -260,9 +265,8 @@ class FakeAuthenticationRepository( object FakeAuthenticationRepositoryModule { @Provides @SysUISingleton - fun provideFake( - scope: TestScope, - ) = FakeAuthenticationRepository(currentTime = { scope.currentTime }) + fun provideFake(scope: TestScope) = + FakeAuthenticationRepository(currentTime = { scope.currentTime }) @Module interface Bindings { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt index be84e3316f5b..e4c7df64fdc6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt @@ -19,12 +19,14 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.deviceentry.data.repository.deviceEntryRepository import com.android.systemui.flags.fakeSystemPropertiesHelper +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.trustInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository val Kosmos.deviceUnlockedInteractor by Fixture { DeviceUnlockedInteractor( @@ -36,6 +38,8 @@ val Kosmos.deviceUnlockedInteractor by Fixture { powerInteractor = powerInteractor, biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor, systemPropertiesHelper = fakeSystemPropertiesHelper, + userAwareSecureSettingsRepository = userAwareSecureSettingsRepository, + keyguardInteractor = keyguardInteractor, ) .apply { activateIn(testScope) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt index 19e077c57de0..8209ee12ad9a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt @@ -87,7 +87,7 @@ class FakeKeyguardTransitionRepository( ) : this( initInLockscreen = true, initiallySendTransitionStepsOnStartTransition = true, - testScope + testScope, ) private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> = @@ -191,12 +191,12 @@ class FakeKeyguardTransitionRepository( if (lastStep != null && lastStep.transitionState != TransitionState.FINISHED) { sendTransitionStep( step = - TransitionStep( - transitionState = TransitionState.CANCELED, - from = lastStep.from, - to = lastStep.to, - value = 0f, - ) + TransitionStep( + transitionState = TransitionState.CANCELED, + from = lastStep.from, + to = lastStep.to, + value = 0f, + ) ) testScheduler.runCurrent() } @@ -390,6 +390,18 @@ class FakeKeyguardTransitionRepository( @FloatRange(from = 0.0, to = 1.0) value: Float, state: TransitionState, ) = Unit + + override suspend fun forceFinishCurrentTransition() { + _transitions.tryEmit( + TransitionStep( + _currentTransitionInfo.value.from, + _currentTransitionInfo.value.to, + 1f, + TransitionState.FINISHED, + ownerName = _currentTransitionInfo.value.ownerName, + ) + ) + } } @Module diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt index aa94c368e8f1..b9a831f11d23 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor val Kosmos.keyguardTransitionInteractor: KeyguardTransitionInteractor by @@ -26,6 +27,7 @@ val Kosmos.keyguardTransitionInteractor: KeyguardTransitionInteractor by KeyguardTransitionInteractor( scope = applicationCoroutineScope, repository = keyguardTransitionRepository, - sceneInteractor = sceneInteractor + sceneInteractor = sceneInteractor, + powerInteractor = powerInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt index f49e3771763a..b3be2c09c6f8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt @@ -22,6 +22,7 @@ import android.os.Handler import android.os.looper import androidx.media3.session.CommandButton import androidx.media3.session.MediaController +import androidx.media3.session.SessionCommand import androidx.media3.session.SessionToken import com.android.systemui.Flags import com.android.systemui.graphics.imageLoader @@ -30,7 +31,11 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.shared.mediaLogger import com.android.systemui.media.controls.util.fakeMediaControllerFactory import com.android.systemui.media.controls.util.fakeSessionTokenFactory +import com.android.systemui.util.concurrency.execution import com.google.common.collect.ImmutableList +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -46,10 +51,22 @@ var Kosmos.media3ActionFactory: Media3ActionFactory by mock<MediaController>().also { whenever(it.customLayout).thenReturn(customLayout) whenever(it.sessionExtras).thenReturn(Bundle()) + whenever(it.isCommandAvailable(any())).thenReturn(true) + whenever(it.isSessionCommandAvailable(any<SessionCommand>())).thenReturn(true) } fakeMediaControllerFactory.setMedia3Controller(media3Controller) fakeSessionTokenFactory.setMedia3SessionToken(mock<SessionToken>()) } + + val runnableCaptor = argumentCaptor<Runnable>() + val handler = + mock<Handler> { + on { post(runnableCaptor.capture()) } doAnswer + { + runnableCaptor.lastValue.run() + true + } + } Media3ActionFactory( context = applicationContext, imageLoader = imageLoader, @@ -57,7 +74,8 @@ var Kosmos.media3ActionFactory: Media3ActionFactory by tokenFactory = fakeSessionTokenFactory, logger = mediaLogger, looper = looper, - handler = Handler(looper), + handler = handler, bgScope = testScope, + execution = execution, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepositoryKosmos.kt new file mode 100644 index 000000000000..a977121b3803 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepositoryKosmos.kt @@ -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.systemui.qs.panels.data.repository + +import android.content.res.mainResources +import com.android.systemui.common.ui.data.repository.configurationRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope + +val Kosmos.largeTileSpanRepository by + Kosmos.Fixture { + LargeTileSpanRepository(applicationCoroutineScope, mainResources, configurationRepository) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt index 0c62d0e85ce1..8d4db8b74061 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt @@ -20,6 +20,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.log.core.FakeLogBuffer import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository +import com.android.systemui.qs.panels.data.repository.largeTileSpanRepository import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor val Kosmos.iconTilesInteractor by @@ -28,7 +29,8 @@ val Kosmos.iconTilesInteractor by defaultLargeTilesRepository, currentTilesInteractor, qsPreferencesInteractor, + largeTileSpanRepository, FakeLogBuffer.Factory.create(), - applicationCoroutineScope + applicationCoroutineScope, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt index 45aab860cde7..28edae7c3689 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt @@ -49,6 +49,7 @@ val Kosmos.statusBarOrchestrator by fakeStatusBarModePerDisplayRepository, fakeStatusBarInitializer, fakeStatusBarWindowController, + applicationCoroutineScope.coroutineContext, mockDemoModeController, mockPluginDependencyProvider, mockAutoHideController, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt index c3c3cce5cf68..dae66d42b2bc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -41,6 +41,7 @@ class FakeMobileConnectionRepository( override val isGsm = MutableStateFlow(false) override val cdmaLevel = MutableStateFlow(0) override val primaryLevel = MutableStateFlow(0) + override val satelliteLevel = MutableStateFlow(0) override val dataConnectionState = MutableStateFlow(DataConnectionState.Disconnected) override val dataActivityDirection = MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py index cee29dcd1d59..7a7de3553829 100755 --- a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py +++ b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py @@ -47,6 +47,7 @@ class TestWithGoldenOutput(unittest.TestCase): # Test to check the generated jar files to the golden output. def test_compare_to_golden(self): + self.skipTest("test cannot handle multiple images (see b/378470825)") files = os.listdir(GOLDEN_DIR) files.sort() diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 86d3ee6d1257..d4af7b765254 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -42,9 +42,11 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATIN import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE; import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR; import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED; +import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; import static android.view.accessibility.AccessibilityManager.FlashNotificationReason; +import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures; import static com.android.hardware.input.Flags.keyboardA11yMouseKeys; import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME; @@ -55,6 +57,7 @@ import static com.android.internal.accessibility.common.ShortcutConstants.UserSh import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.ALL; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP; @@ -111,6 +114,8 @@ import android.graphics.Rect; import android.graphics.Region; import android.hardware.display.DisplayManager; import android.hardware.fingerprint.IFingerprintService; +import android.hardware.input.InputManager; +import android.hardware.input.KeyGestureEvent; import android.media.AudioManagerInternal; import android.net.Uri; import android.os.Binder; @@ -338,6 +343,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private AlertDialog mEnableTouchExplorationDialog; + private final InputManager mInputManager; + private AccessibilityInputFilter mInputFilter; private boolean mHasInputFilter; @@ -503,6 +510,25 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } + private InputManager.KeyGestureEventHandler mKeyGestureEventHandler = + new InputManager.KeyGestureEventHandler() { + @Override + public boolean handleKeyGestureEvent( + @NonNull KeyGestureEvent event, + @Nullable IBinder focusedToken) { + return AccessibilityManagerService.this.handleKeyGestureEvent(event); + } + + @Override + public boolean isKeyGestureSupported(int gestureType) { + return switch (gestureType) { + case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION, + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK -> true; + default -> false; + }; + } + }; + @VisibleForTesting AccessibilityManagerService( Context context, @@ -542,6 +568,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mUmi = LocalServices.getService(UserManagerInternal.class); // TODO(b/255426725): not used on tests mVisibleBgUserIds = null; + mInputManager = context.getSystemService(InputManager.class); init(); } @@ -583,6 +610,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mUiAutomationManager, this); mFlashNotificationsController = new FlashNotificationsController(mContext); mUmi = LocalServices.getService(UserManagerInternal.class); + mInputManager = context.getSystemService(InputManager.class); if (UserManager.isVisibleBackgroundUsersEnabled()) { mVisibleBgUserIds = new SparseBooleanArray(); @@ -599,6 +627,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub registerBroadcastReceivers(); new AccessibilityContentObserver(mMainHandler).register( mContext.getContentResolver()); + if (enableTalkbackAndMagnifierKeyGestures()) { + mInputManager.registerKeyGestureEventHandler(mKeyGestureEventHandler); + } disableAccessibilityMenuToMigrateIfNeeded(); } @@ -640,6 +671,79 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return mIsAccessibilityButtonShown; } + @VisibleForTesting + boolean handleKeyGestureEvent(KeyGestureEvent event) { + final boolean complete = + event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE + && !event.isCancelled(); + final int gestureType = event.getKeyGestureType(); + if (!complete) { + return false; + } + + String targetName; + switch (gestureType) { + case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION: + targetName = MAGNIFICATION_CONTROLLER_NAME; + break; + case KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK: + targetName = mContext.getString(R.string.config_defaultSelectToSpeakService); + if (targetName.isEmpty()) { + return false; + } + + final ComponentName targetServiceComponent = TextUtils.isEmpty(targetName) + ? null : ComponentName.unflattenFromString(targetName); + AccessibilityServiceInfo accessibilityServiceInfo; + synchronized (mLock) { + AccessibilityUserState userState = getCurrentUserStateLocked(); + accessibilityServiceInfo = + userState.getInstalledServiceInfoLocked(targetServiceComponent); + } + if (accessibilityServiceInfo == null) { + return false; + } + + // Skip enabling if a warning dialog is required for the feature. + // TODO(b/377752960): Explore better options to instead show the warning dialog + // in this scenario. + if (isAccessibilityServiceWarningRequired(accessibilityServiceInfo)) { + Slog.w(LOG_TAG, + "Accessibility warning is required before this service can be " + + "activated automatically via KEY_GESTURE shortcut."); + return false; + } + break; + default: + return false; + } + + List<String> shortcutTargets = getAccessibilityShortcutTargets( + KEY_GESTURE); + if (!shortcutTargets.contains(targetName)) { + int userId; + synchronized (mLock) { + userId = mCurrentUserId; + } + // TODO(b/377752960): Add dialog to confirm enabling the service and to + // activate the first time. + enableShortcutForTargets(true, UserShortcutType.KEY_GESTURE, + List.of(targetName), userId); + + // Do not perform action on first press since it was just registered. Eventually, + // this will be a separate dialog that appears that requires the user to confirm + // which will resolve this race condition. For now, just require two presses the + // first time it is activated. + return true; + } + + final int displayId = event.getDisplayId() != INVALID_DISPLAY + ? event.getDisplayId() : getLastNonProxyTopFocusedDisplayId(); + performAccessibilityShortcutInternal(displayId, KEY_GESTURE, targetName); + + return true; + } + @Override public Pair<float[], MagnificationSpec> getWindowTransformationMatrixAndMagnificationSpec( int windowId) { @@ -1224,14 +1328,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub int displayId = event.getDisplayId(); final int windowId = event.getWindowId(); if (windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID - && displayId == Display.INVALID_DISPLAY) { + && displayId == INVALID_DISPLAY) { displayId = mA11yWindowManager.getDisplayIdByUserIdAndWindowId( resolvedUserId, windowId); event.setDisplayId(displayId); } synchronized (mLock) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED - && displayId != Display.INVALID_DISPLAY + && displayId != INVALID_DISPLAY && mA11yWindowManager.isTrackingWindowsLocked(displayId)) { shouldComputeWindows = true; } @@ -3257,6 +3361,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub updateAccessibilityShortcutTargetsLocked(userState, SOFTWARE); updateAccessibilityShortcutTargetsLocked(userState, GESTURE); updateAccessibilityShortcutTargetsLocked(userState, QUICK_SETTINGS); + updateAccessibilityShortcutTargetsLocked(userState, KEY_GESTURE); // Update the capabilities before the mode because we will check the current mode is // invalid or not.. updateMagnificationCapabilitiesSettingsChangeLocked(userState); @@ -3387,6 +3492,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, QUICK_SETTINGS); somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, SOFTWARE); somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, GESTURE); + somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, KEY_GESTURE); somethingChanged |= readAccessibilityButtonTargetComponentLocked(userState); somethingChanged |= readUserRecommendedUiTimeoutSettingsLocked(userState); somethingChanged |= readMagnificationModeForDefaultDisplayLocked(userState); @@ -3968,6 +4074,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (android.provider.Flags.a11yStandaloneGestureEnabled()) { shortcutTypes.add(GESTURE); } + shortcutTypes.add(KEY_GESTURE); final ComponentName serviceName = service.getComponentName(); for (Integer shortcutType: shortcutTypes) { @@ -4078,13 +4185,15 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub */ private void performAccessibilityShortcutInternal(int displayId, @UserShortcutType int shortcutType, @Nullable String targetName) { - final List<String> shortcutTargets = getAccessibilityShortcutTargetsInternal(shortcutType); + final List<String> shortcutTargets = getAccessibilityShortcutTargetsInternal( + shortcutType); if (shortcutTargets.isEmpty()) { Slog.d(LOG_TAG, "No target to perform shortcut, shortcutType=" + shortcutType); return; } // In case the caller specified a target name - if (targetName != null && !doesShortcutTargetsStringContain(shortcutTargets, targetName)) { + if (targetName != null && !doesShortcutTargetsStringContain(shortcutTargets, + targetName)) { Slog.v(LOG_TAG, "Perform shortcut failed, invalid target name:" + targetName); targetName = null; } @@ -4306,6 +4415,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return; } + if (shortcutType == UserShortcutType.KEY_GESTURE + && !enableTalkbackAndMagnifierKeyGestures()) { + Slog.w(LOG_TAG, + "KEY_GESTURE type shortcuts are disabled by feature flag"); + return; + } + final String shortcutTypeSettingKey = ShortcutUtils.convertToKey(shortcutType); if (shortcutType == UserShortcutType.TRIPLETAP || shortcutType == UserShortcutType.TWOFINGER_DOUBLETAP) { @@ -5683,6 +5799,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private final Uri mAccessibilityGestureTargetsUri = Settings.Secure.getUriFor( Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS); + private final Uri mAccessibilityKeyGestureTargetsUri = Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS); + private final Uri mUserNonInteractiveUiTimeoutUri = Settings.Secure.getUriFor( Settings.Secure.ACCESSIBILITY_NON_INTERACTIVE_UI_TIMEOUT_MS); @@ -5747,6 +5866,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub contentResolver.registerContentObserver( mAccessibilityGestureTargetsUri, false, this, UserHandle.USER_ALL); contentResolver.registerContentObserver( + mAccessibilityKeyGestureTargetsUri, false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver( mUserNonInteractiveUiTimeoutUri, false, this, UserHandle.USER_ALL); contentResolver.registerContentObserver( mUserInteractiveUiTimeoutUri, false, this, UserHandle.USER_ALL); @@ -5828,6 +5949,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (readAccessibilityShortcutTargetsLocked(userState, GESTURE)) { onUserStateChangedLocked(userState); } + } else if (mAccessibilityKeyGestureTargetsUri.equals(uri)) { + if (readAccessibilityShortcutTargetsLocked(userState, KEY_GESTURE)) { + onUserStateChangedLocked(userState); + } } else if (mUserNonInteractiveUiTimeoutUri.equals(uri) || mUserInteractiveUiTimeoutUri.equals(uri)) { readUserRecommendedUiTimeoutSettingsLocked(userState); diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index 67b40632dde8..8b3e63d0dc5e 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java @@ -29,6 +29,7 @@ import static com.android.internal.accessibility.AccessibilityShortcutController import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP; @@ -209,6 +210,7 @@ class AccessibilityUserState { mShortcutTargets.put(SOFTWARE, new ArraySet<>()); mShortcutTargets.put(GESTURE, new ArraySet<>()); mShortcutTargets.put(QUICK_SETTINGS, new ArraySet<>()); + mShortcutTargets.put(KEY_GESTURE, new ArraySet<>()); } boolean isHandlingAccessibilityEventsLocked() { diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java index 23cee9db2138..1588e0421675 100644 --- a/services/core/java/com/android/server/BootReceiver.java +++ b/services/core/java/com/android/server/BootReceiver.java @@ -53,6 +53,7 @@ import com.android.server.am.DropboxRateLimiter; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; @@ -400,9 +401,18 @@ public class BootReceiver extends BroadcastReceiver { Slog.w(TAG, "Tombstone too large to add to DropBox: " + tombstone.toPath()); return; } - // Read the proto tombstone file as bytes. - final byte[] tombstoneBytes = Files.readAllBytes(tombstone.toPath()); + // Read the proto tombstone file as bytes. + // Previously used Files.readAllBytes() which internally creates a ThreadLocal BufferCache + // via ChannelInputStream that isn't properly released. Switched to + // FileInputStream.transferTo() which avoids the NIO channels completely, + // preventing the memory leak while maintaining the same functionality. + final byte[] tombstoneBytes; + try (FileInputStream fis = new FileInputStream(tombstone); + ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + fis.transferTo(baos); + tombstoneBytes = baos.toByteArray(); + } final File tombstoneProtoWithHeaders = File.createTempFile( tombstone.getName(), ".tmp", TOMBSTONE_TMP_DIR); Files.setPosixFilePermissions( diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java index b0f880710eb6..8a128582c507 100644 --- a/services/core/java/com/android/server/am/BroadcastController.java +++ b/services/core/java/com/android/server/am/BroadcastController.java @@ -593,7 +593,7 @@ class BroadcastController { originalStickyCallingUid, BackgroundStartPrivileges.NONE, false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */, null /* filterExtrasForReceiver */, - broadcast.originalCallingAppProcessState); + broadcast.originalCallingAppProcessState, mService.mPlatformCompat); queue.enqueueBroadcastLocked(r); } } @@ -1631,7 +1631,7 @@ class BroadcastController { receivers, resultToApp, resultTo, resultCode, resultData, resultExtras, ordered, sticky, false, userId, backgroundStartPrivileges, timeoutExempt, filterExtrasForReceiver, - callerAppProcessState); + callerAppProcessState, mService.mPlatformCompat); broadcastSentEventRecord.setBroadcastRecord(r); if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing ordered broadcast " + r); diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index f908c67d7ec9..38df10a0bc8c 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -42,6 +42,8 @@ import android.app.AppOpsManager; import android.app.BackgroundStartPrivileges; import android.app.BroadcastOptions; import android.app.BroadcastOptions.DeliveryGroupPolicy; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; import android.content.ComponentName; import android.content.IIntentReceiver; import android.content.Intent; @@ -55,10 +57,12 @@ import android.os.UserHandle; import android.util.ArrayMap; import android.util.IntArray; import android.util.PrintWriterPrinter; +import android.util.SparseBooleanArray; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.compat.PlatformCompat; import dalvik.annotation.optimization.NeverCompile; @@ -77,6 +81,15 @@ import java.util.function.BiFunction; * An active intent broadcast. */ final class BroadcastRecord extends Binder { + /** + * Limit the scope of the priority values to the process level. This means that priority values + * will only influence the order of broadcast delivery within the same process. + */ + @ChangeId + @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE) + @VisibleForTesting + static final long CHANGE_LIMIT_PRIORITY_SCOPE = 371307720L; + final @NonNull Intent intent; // the original intent that generated us final @Nullable ComponentName targetComp; // original component name set on the intent final @Nullable ProcessRecord callerApp; // process that sent this @@ -417,13 +430,13 @@ final class BroadcastRecord extends Binder { @NonNull BackgroundStartPrivileges backgroundStartPrivileges, boolean timeoutExempt, @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver, - int callerAppProcessState) { + int callerAppProcessState, PlatformCompat platformCompat) { this(queue, intent, callerApp, callerPackage, callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType, requiredPermissions, excludedPermissions, excludedPackages, appOp, options, receivers, resultToApp, resultTo, resultCode, resultData, resultExtras, serialized, sticky, initialSticky, userId, -1, backgroundStartPrivileges, timeoutExempt, - filterExtrasForReceiver, callerAppProcessState); + filterExtrasForReceiver, callerAppProcessState, platformCompat); } BroadcastRecord(BroadcastQueue _queue, @@ -439,7 +452,7 @@ final class BroadcastRecord extends Binder { @NonNull BackgroundStartPrivileges backgroundStartPrivileges, boolean timeoutExempt, @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver, - int callerAppProcessState) { + int callerAppProcessState, PlatformCompat platformCompat) { if (_intent == null) { throw new NullPointerException("Can't construct with a null intent"); } @@ -466,7 +479,8 @@ final class BroadcastRecord extends Binder { urgent = calculateUrgent(_intent, _options); deferUntilActive = calculateDeferUntilActive(_callingUid, _options, _resultTo, _serialized, urgent); - blockedUntilBeyondCount = calculateBlockedUntilBeyondCount(receivers, _serialized); + blockedUntilBeyondCount = calculateBlockedUntilBeyondCount( + receivers, _serialized, platformCompat); scheduledTime = new long[delivery.length]; terminalTime = new long[delivery.length]; resultToApp = _resultToApp; @@ -730,7 +744,8 @@ final class BroadcastRecord extends Binder { } /** - * Determine if the result of {@link #calculateBlockedUntilBeyondCount(List, boolean)} + * Determine if the result of + * {@link #calculateBlockedUntilBeyondCount(List, boolean, PlatformCompat)} * has prioritized tranches of receivers. */ @VisibleForTesting @@ -754,37 +769,121 @@ final class BroadcastRecord extends Binder { */ @VisibleForTesting static @NonNull int[] calculateBlockedUntilBeyondCount( - @NonNull List<Object> receivers, boolean ordered) { + @NonNull List<Object> receivers, boolean ordered, PlatformCompat platformCompat) { final int N = receivers.size(); final int[] blockedUntilBeyondCount = new int[N]; - int lastPriority = 0; - int lastPriorityIndex = 0; - for (int i = 0; i < N; i++) { - if (ordered) { - // When sending an ordered broadcast, we need to block this - // receiver until all previous receivers have terminated + if (ordered) { + // When sending an ordered broadcast, we need to block this + // receiver until all previous receivers have terminated + for (int i = 0; i < N; i++) { blockedUntilBeyondCount[i] = i; + } + } else { + if (Flags.limitPriorityScope()) { + final boolean[] changeEnabled = calculateChangeStateForReceivers( + receivers, CHANGE_LIMIT_PRIORITY_SCOPE, platformCompat); + + // Priority of the previous tranche + int lastTranchePriority = 0; + // Priority of the current tranche + int currentTranchePriority = 0; + // Index of the last receiver in the previous tranche + int lastTranchePriorityIndex = -1; + // Index of the last receiver with change disabled in the previous tranche + int lastTrancheChangeDisabledIndex = -1; + // Index of the last receiver with change disabled in the current tranche + int currentTrancheChangeDisabledIndex = -1; + + for (int i = 0; i < N; i++) { + final int thisPriority = getReceiverPriority(receivers.get(i)); + if (i == 0) { + currentTranchePriority = thisPriority; + if (!changeEnabled[i]) { + currentTrancheChangeDisabledIndex = i; + } + continue; + } + + // Check if a new priority tranche has started + if (thisPriority != currentTranchePriority) { + // Update tranche boundaries and reset the disabled index. + if (currentTrancheChangeDisabledIndex != -1) { + lastTrancheChangeDisabledIndex = currentTrancheChangeDisabledIndex; + } + lastTranchePriority = currentTranchePriority; + lastTranchePriorityIndex = i - 1; + currentTranchePriority = thisPriority; + currentTrancheChangeDisabledIndex = -1; + } + if (!changeEnabled[i]) { + currentTrancheChangeDisabledIndex = i; + + // Since the change is disabled, block the current receiver until the + // last receiver in the previous tranche. + blockedUntilBeyondCount[i] = lastTranchePriorityIndex + 1; + } else if (thisPriority != lastTranchePriority) { + // If the changeId was disabled for an earlier receiver and the current + // receiver has a different priority, block the current receiver + // until that earlier receiver. + if (lastTrancheChangeDisabledIndex != -1) { + blockedUntilBeyondCount[i] = lastTrancheChangeDisabledIndex + 1; + } + } + } + // If the entire list is in the same priority tranche or no receivers had + // changeId disabled, mark as -1 to indicate that none of them need to wait + if (N > 0 && (lastTranchePriorityIndex == -1 + || (lastTrancheChangeDisabledIndex == -1 + && currentTrancheChangeDisabledIndex == -1))) { + Arrays.fill(blockedUntilBeyondCount, -1); + } } else { // When sending a prioritized broadcast, we only need to wait // for the previous tranche of receivers to be terminated - final int thisPriority = getReceiverPriority(receivers.get(i)); - if ((i == 0) || (thisPriority != lastPriority)) { - lastPriority = thisPriority; - lastPriorityIndex = i; - blockedUntilBeyondCount[i] = i; - } else { - blockedUntilBeyondCount[i] = lastPriorityIndex; + int lastPriority = 0; + int lastPriorityIndex = 0; + for (int i = 0; i < N; i++) { + final int thisPriority = getReceiverPriority(receivers.get(i)); + if ((i == 0) || (thisPriority != lastPriority)) { + lastPriority = thisPriority; + lastPriorityIndex = i; + blockedUntilBeyondCount[i] = i; + } else { + blockedUntilBeyondCount[i] = lastPriorityIndex; + } + } + // If the entire list is in the same priority tranche, mark as -1 to + // indicate that none of them need to wait + if (N > 0 && blockedUntilBeyondCount[N - 1] == 0) { + Arrays.fill(blockedUntilBeyondCount, -1); } } } - // If the entire list is in the same priority tranche, mark as -1 to - // indicate that none of them need to wait - if (N > 0 && blockedUntilBeyondCount[N - 1] == 0) { - Arrays.fill(blockedUntilBeyondCount, -1); - } return blockedUntilBeyondCount; } + @VisibleForTesting + static @NonNull boolean[] calculateChangeStateForReceivers(@NonNull List<Object> receivers, + long changeId, PlatformCompat platformCompat) { + final SparseBooleanArray changeStateForUids = new SparseBooleanArray(); + final int count = receivers.size(); + final boolean[] changeStateForReceivers = new boolean[count]; + for (int i = 0; i < count; ++i) { + final int receiverUid = getReceiverUid(receivers.get(i)); + final boolean isChangeEnabled; + final int idx = changeStateForUids.indexOfKey(receiverUid); + if (idx >= 0) { + isChangeEnabled = changeStateForUids.valueAt(idx); + } else { + isChangeEnabled = platformCompat.isChangeEnabledByUidInternalNoLogging( + changeId, receiverUid); + changeStateForUids.put(receiverUid, isChangeEnabled); + } + changeStateForReceivers[i] = isChangeEnabled; + } + return changeStateForReceivers; + } + static int getReceiverUid(@NonNull Object receiver) { if (receiver instanceof BroadcastFilter) { return ((BroadcastFilter) receiver).owningUid; diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java index f4a931f89551..d2af84cf3d30 100644 --- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java +++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java @@ -19,7 +19,6 @@ package com.android.server.am; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW; import static com.android.server.am.ActivityManagerService.checkComponentPermission; import static com.android.server.am.BroadcastQueue.TAG; -import static com.android.server.am.Flags.usePermissionManagerForBroadcastDeliveryCheck; import android.annotation.NonNull; import android.annotation.Nullable; @@ -289,33 +288,16 @@ public class BroadcastSkipPolicy { if (info.activityInfo.applicationInfo.uid != Process.SYSTEM_UID && r.requiredPermissions != null && r.requiredPermissions.length > 0) { - final AttributionSource[] attributionSources; - if (usePermissionManagerForBroadcastDeliveryCheck()) { - attributionSources = createAttributionSourcesForResolveInfo(info); - } else { - attributionSources = null; - } + final AttributionSource[] attributionSources = + createAttributionSourcesForResolveInfo(info); for (int i = 0; i < r.requiredPermissions.length; i++) { String requiredPermission = r.requiredPermissions[i]; - try { - if (usePermissionManagerForBroadcastDeliveryCheck()) { - perm = hasPermissionForDataDelivery( - requiredPermission, - "Broadcast delivered to " + info.activityInfo.name, - attributionSources) - ? PackageManager.PERMISSION_GRANTED - : PackageManager.PERMISSION_DENIED; - } else { - perm = AppGlobals.getPackageManager() - .checkPermission( - requiredPermission, - info.activityInfo.applicationInfo.packageName, - UserHandle - .getUserId(info.activityInfo.applicationInfo.uid)); - } - } catch (RemoteException e) { - perm = PackageManager.PERMISSION_DENIED; - } + perm = hasPermissionForDataDelivery( + requiredPermission, + "Broadcast delivered to " + info.activityInfo.name, + attributionSources) + ? PackageManager.PERMISSION_GRANTED + : PackageManager.PERMISSION_DENIED; if (perm != PackageManager.PERMISSION_GRANTED) { return "Permission Denial: receiving " + r.intent + " to " @@ -324,15 +306,6 @@ public class BroadcastSkipPolicy { + " due to sender " + r.callerPackage + " (uid " + r.callingUid + ")"; } - if (!usePermissionManagerForBroadcastDeliveryCheck()) { - int appOp = AppOpsManager.permissionToOpCode(requiredPermission); - if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp) { - if (!noteOpForManifestReceiver(appOp, r, info, component)) { - return "Skipping delivery to " + info.activityInfo.packageName - + " due to required appop " + appOp; - } - } - } } } if (r.appOp != AppOpsManager.OP_NONE) { @@ -452,35 +425,20 @@ public class BroadcastSkipPolicy { // Check that the receiver has the required permission(s) to receive this broadcast. if (r.requiredPermissions != null && r.requiredPermissions.length > 0) { - final AttributionSource attributionSource; - if (usePermissionManagerForBroadcastDeliveryCheck()) { - attributionSource = - new AttributionSource.Builder(filter.receiverList.uid) - .setPid(filter.receiverList.pid) - .setPackageName(filter.packageName) - .setAttributionTag(filter.featureId) - .build(); - } else { - attributionSource = null; - } + final AttributionSource attributionSource = + new AttributionSource.Builder(filter.receiverList.uid) + .setPid(filter.receiverList.pid) + .setPackageName(filter.packageName) + .setAttributionTag(filter.featureId) + .build(); for (int i = 0; i < r.requiredPermissions.length; i++) { String requiredPermission = r.requiredPermissions[i]; - final int perm; - if (usePermissionManagerForBroadcastDeliveryCheck()) { - perm = hasPermissionForDataDelivery( - requiredPermission, - "Broadcast delivered to registered receiver " + filter.receiverId, - attributionSource) - ? PackageManager.PERMISSION_GRANTED - : PackageManager.PERMISSION_DENIED; - } else { - perm = checkComponentPermission( - requiredPermission, - filter.receiverList.pid, - filter.receiverList.uid, - -1 /* owningUid */, - true /* exported */); - } + final int perm = hasPermissionForDataDelivery( + requiredPermission, + "Broadcast delivered to registered receiver " + filter.receiverId, + attributionSource) + ? PackageManager.PERMISSION_GRANTED + : PackageManager.PERMISSION_DENIED; if (perm != PackageManager.PERMISSION_GRANTED) { return "Permission Denial: receiving " + r.intent.toString() @@ -491,24 +449,6 @@ public class BroadcastSkipPolicy { + " due to sender " + r.callerPackage + " (uid " + r.callingUid + ")"; } - if (!usePermissionManagerForBroadcastDeliveryCheck()) { - int appOp = AppOpsManager.permissionToOpCode(requiredPermission); - if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp - && mService.getAppOpsManager().noteOpNoThrow(appOp, - filter.receiverList.uid, filter.packageName, filter.featureId, - "Broadcast delivered to registered receiver " + filter.receiverId) - != AppOpsManager.MODE_ALLOWED) { - return "Appop Denial: receiving " - + r.intent.toString() - + " to " + filter.receiverList.app - + " (pid=" + filter.receiverList.pid - + ", uid=" + filter.receiverList.uid + ")" - + " requires appop " + AppOpsManager.permissionToOp( - requiredPermission) - + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"; - } - } } } if ((r.requiredPermissions == null || r.requiredPermissions.length == 0)) { diff --git a/services/core/java/com/android/server/am/broadcasts_flags.aconfig b/services/core/java/com/android/server/am/broadcasts_flags.aconfig index b1185d552941..7f169db7dcec 100644 --- a/services/core/java/com/android/server/am/broadcasts_flags.aconfig +++ b/services/core/java/com/android/server/am/broadcasts_flags.aconfig @@ -7,4 +7,12 @@ flag { description: "Restrict priority values defined by non-system apps" is_fixed_read_only: true bug: "369487976" +} + +flag { + name: "limit_priority_scope" + namespace: "backstage_power" + description: "Limit the scope of receiver priorities to within a process" + is_fixed_read_only: true + bug: "369487976" }
\ No newline at end of file diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 09de89445122..34d4fb02ad99 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -40,6 +40,7 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeAudio; import android.bluetooth.BluetoothProfile; import android.content.Intent; +import android.media.AudioDescriptor; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioDevicePort; @@ -47,6 +48,7 @@ import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioManager.AudioDeviceCategory; import android.media.AudioPort; +import android.media.AudioProfile; import android.media.AudioRoutesInfo; import android.media.AudioSystem; import android.media.IAudioRoutesObserver; @@ -619,6 +621,8 @@ public class AudioDeviceInventory { final int mGroupId; @NonNull String mPeerDeviceAddress; @NonNull String mPeerIdentityDeviceAddress; + @NonNull List<AudioProfile> mAudioProfiles; + @NonNull List<AudioDescriptor> mAudioDescriptors; /** Disabled operating modes for this device. Use a negative logic so that by default * an empty list means all modes are allowed. @@ -627,7 +631,8 @@ public class AudioDeviceInventory { DeviceInfo(int deviceType, String deviceName, String address, String identityAddress, int codecFormat, - int groupId, String peerAddress, String peerIdentityAddress) { + int groupId, String peerAddress, String peerIdentityAddress, + List<AudioProfile> profiles, List<AudioDescriptor> descriptors) { mDeviceType = deviceType; mDeviceName = TextUtils.emptyIfNull(deviceName); mDeviceAddress = TextUtils.emptyIfNull(address); @@ -639,6 +644,16 @@ public class AudioDeviceInventory { mGroupId = groupId; mPeerDeviceAddress = TextUtils.emptyIfNull(peerAddress); mPeerIdentityDeviceAddress = TextUtils.emptyIfNull(peerIdentityAddress); + mAudioProfiles = profiles; + mAudioDescriptors = descriptors; + } + + DeviceInfo(int deviceType, String deviceName, String address, + String identityAddress, int codecFormat, + int groupId, String peerAddress, String peerIdentityAddress) { + this(deviceType, deviceName, address, identityAddress, codecFormat, + groupId, peerAddress, peerIdentityAddress, + new ArrayList<>(), new ArrayList<>()); } /** Constructor for all devices except A2DP sink and LE Audio */ @@ -646,6 +661,13 @@ public class AudioDeviceInventory { this(deviceType, deviceName, address, null, AudioSystem.AUDIO_FORMAT_DEFAULT); } + /** Constructor for HDMI OUT, HDMI ARC/EARC sink devices */ + DeviceInfo(int deviceType, String deviceName, String address, + List<AudioProfile> profiles, List<AudioDescriptor> descriptors) { + this(deviceType, deviceName, address, null, AudioSystem.AUDIO_FORMAT_DEFAULT, + BluetoothLeAudio.GROUP_ID_INVALID, null, null, profiles, descriptors); + } + /** Constructor for A2DP sink devices */ DeviceInfo(int deviceType, String deviceName, String address, String identityAddress, int codecFormat) { @@ -1194,27 +1216,31 @@ public class AudioDeviceInventory { } /*package*/ void onToggleHdmi() { - MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "onToggleHdmi") - .set(MediaMetrics.Property.DEVICE, - AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HDMI)); + final int[] hdmiDevices = { AudioSystem.DEVICE_OUT_HDMI, + AudioSystem.DEVICE_OUT_HDMI_ARC, AudioSystem.DEVICE_OUT_HDMI_EARC }; + synchronized (mDevicesLock) { - // Is HDMI connected? - final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HDMI, ""); - final DeviceInfo di = mConnectedDevices.get(key); - if (di == null) { - Log.e(TAG, "invalid null DeviceInfo in onToggleHdmi"); - mmi.set(MediaMetrics.Property.EARLY_RETURN, "invalid null DeviceInfo").record(); - return; + for (DeviceInfo di : mConnectedDevices.values()) { + boolean isHdmiDevice = Arrays.stream(hdmiDevices).anyMatch(device -> + device == di.mDeviceType); + if (isHdmiDevice) { + MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "onToggleHdmi") + .set(MediaMetrics.Property.DEVICE, + AudioSystem.getDeviceName(di.mDeviceType)); + AudioDeviceAttributes ada = new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.convertInternalDeviceToDeviceType(di.mDeviceType), + di.mDeviceAddress, di.mDeviceName, di.mAudioProfiles, + di.mAudioDescriptors); + // Toggle HDMI to retrigger broadcast with proper formats. + setWiredDeviceConnectionState(ada, + AudioSystem.DEVICE_STATE_UNAVAILABLE, "onToggleHdmi"); // disconnect + setWiredDeviceConnectionState(ada, + AudioSystem.DEVICE_STATE_AVAILABLE, "onToggleHdmi"); // reconnect + mmi.record(); + } } - // Toggle HDMI to retrigger broadcast with proper formats. - setWiredDeviceConnectionState( - new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_HDMI, ""), - AudioSystem.DEVICE_STATE_UNAVAILABLE, "android"); // disconnect - setWiredDeviceConnectionState( - new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_HDMI, ""), - AudioSystem.DEVICE_STATE_AVAILABLE, "android"); // reconnect } - mmi.record(); } @GuardedBy("mDevicesLock") @@ -1818,7 +1844,15 @@ public class AudioDeviceInventory { .printSlog(EventLogger.Event.ALOGE, TAG)); return false; } - mConnectedDevices.put(deviceKey, new DeviceInfo(device, deviceName, address)); + + if (device == AudioSystem.DEVICE_OUT_HDMI || + device == AudioSystem.DEVICE_OUT_HDMI_ARC || + device == AudioSystem.DEVICE_OUT_HDMI_EARC) { + mConnectedDevices.put(deviceKey, new DeviceInfo(device, deviceName, + address, attributes.getAudioProfiles(), attributes.getAudioDescriptors())); + } else { + mConnectedDevices.put(deviceKey, new DeviceInfo(device, deviceName, address)); + } mDeviceBroker.postAccessoryPlugMediaUnmute(device); status = true; } else if (!connect && isConnected) { diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index c0aa4cc6fa24..71f17d1f411e 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -242,6 +242,11 @@ public class DisplayManagerFlags { Flags::autoBrightnessModeBedtimeWear ); + private final FlagState mEnablePluginManagerFlagState = new FlagState( + Flags.FLAG_ENABLE_PLUGIN_MANAGER, + Flags::enablePluginManager + ); + /** * @return {@code true} if 'port' is allowed in display layout configuration file. */ @@ -517,6 +522,10 @@ public class DisplayManagerFlags { return mAutoBrightnessModeBedtimeWearFlagState.isEnabled(); } + public boolean isPluginManagerEnabled() { + return mEnablePluginManagerFlagState.isEnabled(); + } + /** * dumps all flagstates * @param pw printWriter @@ -568,6 +577,7 @@ public class DisplayManagerFlags { pw.println(" " + mIsUserRefreshRateForExternalDisplayEnabled); pw.println(" " + mHasArrSupport); pw.println(" " + mAutoBrightnessModeBedtimeWearFlagState); + pw.println(" " + mEnablePluginManagerFlagState); } private static class FlagState { diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index a9bdccef2300..7850360c7dbf 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -446,3 +446,11 @@ flag { bug: "365163968" is_fixed_read_only: true } + +flag { + name: "enable_plugin_manager" + namespace: "display_manager" + description: "Flag to enable DisplayManager plugins" + bug: "354059797" + is_fixed_read_only: true +} diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index 2e7f5c083ac3..6f3540221b63 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -221,6 +221,12 @@ final class InputGestureManager { systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_EQUALS, KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN)); + systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_M, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION)); + systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_S, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK)); } if (keyboardA11yShortcutControl()) { if (InputSettings.isAccessibilityBounceKeysFeatureEnabled()) { diff --git a/services/core/java/com/android/server/pm/InstallDependencyHelper.java b/services/core/java/com/android/server/pm/InstallDependencyHelper.java new file mode 100644 index 000000000000..745665bab5b3 --- /dev/null +++ b/services/core/java/com/android/server/pm/InstallDependencyHelper.java @@ -0,0 +1,67 @@ +/* + * 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.pm; + +import static android.content.pm.PackageManager.INSTALL_FAILED_MISSING_SHARED_LIBRARY; + +import android.content.pm.SharedLibraryInfo; +import android.content.pm.parsing.PackageLite; +import android.os.OutcomeReceiver; + +import java.util.List; + +/** + * Helper class to interact with SDK Dependency Installer service. + */ +public class InstallDependencyHelper { + private final SharedLibrariesImpl mSharedLibraries; + + InstallDependencyHelper(SharedLibrariesImpl sharedLibraries) { + mSharedLibraries = sharedLibraries; + } + + void resolveLibraryDependenciesIfNeeded(PackageLite pkg, + OutcomeReceiver<Void, PackageManagerException> callback) { + final List<SharedLibraryInfo> missing; + try { + missing = mSharedLibraries.collectMissingSharedLibraryInfos(pkg); + } catch (PackageManagerException e) { + callback.onError(e); + return; + } + + if (missing.isEmpty()) { + // No need for dependency resolution. Move to installation directly. + callback.onResult(null); + return; + } + + try { + bindToDependencyInstaller(); + } catch (Exception e) { + PackageManagerException pe = new PackageManagerException( + INSTALL_FAILED_MISSING_SHARED_LIBRARY, e.getMessage()); + callback.onError(pe); + } + } + + private void bindToDependencyInstaller() { + throw new IllegalStateException("Failed to bind to Dependency Installer"); + } + + +} diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index ef0997696cd7..eb70748918b6 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -220,6 +220,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements private AppOpsManager mAppOps; private final VerifierController mVerifierController; + private final InstallDependencyHelper mInstallDependencyHelper; private final HandlerThread mInstallThread; private final Handler mInstallHandler; @@ -346,6 +347,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements synchronized (mVerificationPolicyPerUser) { mVerificationPolicyPerUser.put(USER_SYSTEM, DEFAULT_VERIFICATION_POLICY); } + mInstallDependencyHelper = new InstallDependencyHelper( + mPm.mInjector.getSharedLibrariesImpl()); LocalServices.getService(SystemServiceManager.class).startService( new Lifecycle(context, this)); @@ -543,7 +546,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements session = PackageInstallerSession.readFromXml(in, mInternalCallback, mContext, mPm, mInstallThread.getLooper(), mStagingManager, mSessionsDir, this, mSilentUpdatePolicy, - mVerifierController); + mVerifierController, mInstallDependencyHelper); } catch (Exception e) { Slog.e(TAG, "Could not read session", e); continue; @@ -1065,7 +1068,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements userId, callingUid, installSource, params, createdMillis, 0L, stageDir, stageCid, null, null, false, false, false, false, null, SessionInfo.INVALID_ID, false, false, false, PackageManager.INSTALL_UNKNOWN, "", null, - mVerifierController, verificationPolicy, verificationPolicy); + mVerifierController, verificationPolicy, verificationPolicy, + mInstallDependencyHelper); synchronized (mSessions) { mSessions.put(sessionId, session); diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 2a92de57446d..bad12016dca7 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -145,6 +145,7 @@ import android.os.FileUtils; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.OutcomeReceiver; import android.os.ParcelFileDescriptor; import android.os.ParcelableException; import android.os.PersistableBundle; @@ -433,6 +434,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private final StagingManager mStagingManager; @NonNull private final VerifierController mVerifierController; + private final InstallDependencyHelper mInstallDependencyHelper; + final int sessionId; final int userId; final SessionParams params; @@ -1188,7 +1191,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { String sessionErrorMessage, DomainSet preVerifiedDomains, @NonNull VerifierController verifierController, @PackageInstaller.VerificationPolicy int initialVerificationPolicy, - @PackageInstaller.VerificationPolicy int currentVerificationPolicy) { + @PackageInstaller.VerificationPolicy int currentVerificationPolicy, + InstallDependencyHelper installDependencyHelper) { mCallback = callback; mContext = context; mPm = pm; @@ -1200,6 +1204,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mVerifierController = verifierController; mInitialVerificationPolicy = initialVerificationPolicy; mCurrentVerificationPolicy = new AtomicInteger(currentVerificationPolicy); + mInstallDependencyHelper = installDependencyHelper; this.sessionId = sessionId; this.userId = userId; @@ -2611,6 +2616,13 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { maybeFinishChildSessions(error, msg); } + private void onSessionDependencyResolveFailure(int error, String msg) { + Slog.e(TAG, "Failed to resolve dependency for session " + sessionId); + // Dispatch message to remove session from PackageInstallerService. + dispatchSessionFinished(error, msg, null); + maybeFinishChildSessions(error, msg); + } + private void onSystemDataLoaderUnrecoverable() { final String packageName = getPackageName(); if (TextUtils.isEmpty(packageName)) { @@ -3402,7 +3414,34 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { /* extras= */ null, /* forPreapproval= */ false); return; } - install(); + + if (Flags.sdkDependencyInstaller() && !isMultiPackage()) { + resolveLibraryDependenciesIfNeeded(); + } else { + install(); + } + } + + + private void resolveLibraryDependenciesIfNeeded() { + synchronized (mLock) { + // TODO(b/372862145): Callback should be called on a handler passed as parameter + mInstallDependencyHelper.resolveLibraryDependenciesIfNeeded(mPackageLite, + new OutcomeReceiver<>() { + + @Override + public void onResult(Void result) { + install(); + } + + @Override + public void onError(@NonNull PackageManagerException e) { + final String completeMsg = ExceptionUtils.getCompleteMessage(e); + setSessionFailed(e.error, completeMsg); + onSessionDependencyResolveFailure(e.error, completeMsg); + } + }); + } } /** @@ -6048,7 +6087,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @NonNull StagingManager stagingManager, @NonNull File sessionsDir, @NonNull PackageSessionProvider sessionProvider, @NonNull SilentUpdatePolicy silentUpdatePolicy, - @NonNull VerifierController verifierController) + @NonNull VerifierController verifierController, + @NonNull InstallDependencyHelper installDependencyHelper) throws IOException, XmlPullParserException { final int sessionId = in.getAttributeInt(null, ATTR_SESSION_ID); final int userId = in.getAttributeInt(null, ATTR_USER_ID); @@ -6257,6 +6297,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { stageCid, fileArray, checksumsMap, prepared, committed, destroyed, sealed, childSessionIdsArray, parentSessionId, isReady, isFailed, isApplied, sessionErrorCode, sessionErrorMessage, preVerifiedDomains, verifierController, - initialVerificationPolicy, currentVerificationPolicy); + initialVerificationPolicy, currentVerificationPolicy, installDependencyHelper); } } diff --git a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java index a28e3c142220..52e8c52fe6af 100644 --- a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java +++ b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java @@ -38,6 +38,7 @@ import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import java.util.ArrayList; @@ -45,7 +46,8 @@ import java.util.function.BiFunction; /** Helper class to handle PackageMonitorCallback and notify the registered client. This is mainly * used by PackageMonitor to improve the broadcast latency. */ -class PackageMonitorCallbackHelper { +@VisibleForTesting +public class PackageMonitorCallbackHelper { private static final boolean DEBUG = false; private static final String TAG = "PackageMonitorCallbackHelper"; diff --git a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java index 929fccce5265..fc54f6864db0 100644 --- a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java +++ b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java @@ -33,6 +33,7 @@ import android.content.pm.SharedLibraryInfo; import android.content.pm.Signature; import android.content.pm.SigningDetails; import android.content.pm.VersionedPackage; +import android.content.pm.parsing.PackageLite; import android.os.Build; import android.os.Process; import android.os.UserHandle; @@ -83,6 +84,7 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable private static final boolean DEBUG_SHARED_LIBRARIES = false; private static final String LIBRARY_TYPE_SDK = "sdk"; + private static final String LIBRARY_TYPE_STATIC = "static shared"; /** * Apps targeting Android S and above need to declare dependencies to the public native @@ -926,18 +928,19 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable if (!pkg.getUsesLibraries().isEmpty()) { usesLibraryInfos = collectSharedLibraryInfos(pkg.getUsesLibraries(), null, null, null, pkg.getPackageName(), "shared", true, pkg.getTargetSdkVersion(), null, - availablePackages, newLibraries); + availablePackages, newLibraries, null); } if (!pkg.getUsesStaticLibraries().isEmpty()) { usesLibraryInfos = collectSharedLibraryInfos(pkg.getUsesStaticLibraries(), pkg.getUsesStaticLibrariesVersions(), pkg.getUsesStaticLibrariesCertDigests(), - null, pkg.getPackageName(), "static shared", true, - pkg.getTargetSdkVersion(), usesLibraryInfos, availablePackages, newLibraries); + null, pkg.getPackageName(), LIBRARY_TYPE_STATIC, true, + pkg.getTargetSdkVersion(), usesLibraryInfos, availablePackages, newLibraries, + null); } if (!pkg.getUsesOptionalLibraries().isEmpty()) { usesLibraryInfos = collectSharedLibraryInfos(pkg.getUsesOptionalLibraries(), null, null, null, pkg.getPackageName(), "shared", false, pkg.getTargetSdkVersion(), - usesLibraryInfos, availablePackages, newLibraries); + usesLibraryInfos, availablePackages, newLibraries, null); } if (platformCompat.isChangeEnabledInternal(ENFORCE_NATIVE_SHARED_LIBRARY_DEPENDENCIES, pkg.getPackageName(), pkg.getTargetSdkVersion())) { @@ -945,13 +948,13 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable usesLibraryInfos = collectSharedLibraryInfos(pkg.getUsesNativeLibraries(), null, null, null, pkg.getPackageName(), "native shared", true, pkg.getTargetSdkVersion(), usesLibraryInfos, availablePackages, - newLibraries); + newLibraries, null); } if (!pkg.getUsesOptionalNativeLibraries().isEmpty()) { usesLibraryInfos = collectSharedLibraryInfos(pkg.getUsesOptionalNativeLibraries(), null, null, null, pkg.getPackageName(), "native shared", false, pkg.getTargetSdkVersion(), usesLibraryInfos, availablePackages, - newLibraries); + newLibraries, null); } } if (!pkg.getUsesSdkLibraries().isEmpty()) { @@ -961,11 +964,34 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable pkg.getUsesSdkLibrariesVersionsMajor(), pkg.getUsesSdkLibrariesCertDigests(), pkg.getUsesSdkLibrariesOptional(), pkg.getPackageName(), LIBRARY_TYPE_SDK, required, pkg.getTargetSdkVersion(), - usesLibraryInfos, availablePackages, newLibraries); + usesLibraryInfos, availablePackages, newLibraries, null); } return usesLibraryInfos; } + List<SharedLibraryInfo> collectMissingSharedLibraryInfos(PackageLite pkgLite) + throws PackageManagerException { + ArrayList<SharedLibraryInfo> missingSharedLibrary = new ArrayList<>(); + synchronized (mPm.mLock) { + collectSharedLibraryInfos(pkgLite.getUsesSdkLibraries(), + pkgLite.getUsesSdkLibrariesVersionsMajor(), + pkgLite.getUsesSdkLibrariesCertDigests(), + /*libsOptional=*/ null, pkgLite.getPackageName(), LIBRARY_TYPE_SDK, + /*required=*/ true, pkgLite.getTargetSdk(), + /*outUsedLibraries=*/ null, mPm.mPackages, /*newLibraries=*/ null, + missingSharedLibrary); + + collectSharedLibraryInfos(pkgLite.getUsesStaticLibraries(), + pkgLite.getUsesStaticLibrariesVersions(), + pkgLite.getUsesStaticLibrariesCertDigests(), + /*libsOptional=*/ null, pkgLite.getPackageName(), LIBRARY_TYPE_STATIC, + /*required=*/ true, pkgLite.getTargetSdk(), + /*outUsedLibraries=*/ null, mPm.mPackages, /*newLibraries=*/ null, + missingSharedLibrary); + } + return missingSharedLibrary; + } + private ArrayList<SharedLibraryInfo> collectSharedLibraryInfos( @NonNull List<String> requestedLibraries, @Nullable long[] requiredVersions, @Nullable String[][] requiredCertDigests, @@ -973,7 +999,8 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable @NonNull String packageName, @NonNull String libraryType, boolean required, int targetSdk, @Nullable ArrayList<SharedLibraryInfo> outUsedLibraries, @NonNull final Map<String, AndroidPackage> availablePackages, - @Nullable final Map<String, WatchedLongSparseArray<SharedLibraryInfo>> newLibraries) + @Nullable final Map<String, WatchedLongSparseArray<SharedLibraryInfo>> newLibraries, + @Nullable final List<SharedLibraryInfo> outMissingSharedLibraryInfos) throws PackageManagerException { final int libCount = requestedLibraries.size(); for (int i = 0; i < libCount; i++) { @@ -986,16 +1013,33 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable libName, libVersion, mSharedLibraries, newLibraries); } if (libraryInfo == null) { - // Only allow app be installed if the app specifies the sdk-library dependency is - // optional - if (required || (LIBRARY_TYPE_SDK.equals(libraryType) && (libsOptional != null - && !libsOptional[i]))) { - throw new PackageManagerException(INSTALL_FAILED_MISSING_SHARED_LIBRARY, - "Package " + packageName + " requires unavailable " + libraryType - + " library " + libName + "; failing!"); - } else if (DEBUG_SHARED_LIBRARIES) { - Slog.i(TAG, "Package " + packageName + " desires unavailable " + libraryType - + " library " + libName + "; ignoring!"); + if (required) { + boolean isSdkOrStatic = libraryType.equals(LIBRARY_TYPE_SDK) + || libraryType.equals(LIBRARY_TYPE_STATIC); + if (isSdkOrStatic && outMissingSharedLibraryInfos != null) { + // TODO(b/372862145): Pass the CertDigest too + // If Dependency Installation is supported, try that instead of failing. + SharedLibraryInfo missingLibrary = new SharedLibraryInfo( + libName, libVersion, SharedLibraryInfo.TYPE_SDK_PACKAGE + ); + outMissingSharedLibraryInfos.add(missingLibrary); + } else { + throw new PackageManagerException(INSTALL_FAILED_MISSING_SHARED_LIBRARY, + "Package " + packageName + " requires unavailable " + libraryType + + " library " + libName + "; failing!"); + } + } else { + // Only allow app be installed if the app specifies the sdk-library + // dependency is optional + boolean isOptional = libsOptional != null && libsOptional[i]; + if (LIBRARY_TYPE_SDK.equals(libraryType) && !isOptional) { + throw new PackageManagerException(INSTALL_FAILED_MISSING_SHARED_LIBRARY, + "Package " + packageName + " requires unavailable " + libraryType + + " library " + libName + "; failing!"); + } else if (DEBUG_SHARED_LIBRARIES) { + Slog.i(TAG, "Package " + packageName + " desires unavailable " + libraryType + + " library " + libName + "; ignoring!"); + } } } else { if (requiredVersions != null && requiredCertDigests != null) { diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java index d5bea4adaf8c..b3e68a35764b 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java @@ -19,6 +19,7 @@ package com.android.server.wallpaper; import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; import static android.app.WallpaperManager.getOrientation; import static android.app.WallpaperManager.getRotatedOrientation; +import static android.app.Flags.accurateWallpaperDownsampling; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.server.wallpaper.WallpaperUtils.RECORD_FILE; @@ -378,7 +379,14 @@ public class WallpaperCropper { for (int i = 0; i < wallpaper.mCropHints.size(); i++) { Rect adjustedRect = new Rect(wallpaper.mCropHints.valueAt(i)); adjustedRect.offset(-wallpaper.cropHint.left, -wallpaper.cropHint.top); - adjustedRect.scale(1f / wallpaper.mSampleSize); + if (accurateWallpaperDownsampling()) { + adjustedRect.left = (int) (0.5f + adjustedRect.left / wallpaper.mSampleSize); + adjustedRect.top = (int) (0.5f + adjustedRect.top / wallpaper.mSampleSize); + adjustedRect.right = (int) Math.floor(adjustedRect.right / wallpaper.mSampleSize); + adjustedRect.bottom = (int) Math.floor(adjustedRect.bottom / wallpaper.mSampleSize); + } else { + adjustedRect.scale(1f / wallpaper.mSampleSize); + } result.put(wallpaper.mCropHints.keyAt(i), adjustedRect); } return result; @@ -603,6 +611,11 @@ public class WallpaperCropper { float sampleSizeForThisOrientation = Math.max(1f, Math.min( crop.width() / displayForThisOrientation.x, crop.height() / displayForThisOrientation.y)); + if (accurateWallpaperDownsampling()) { + sampleSizeForThisOrientation = Math.max(1f, Math.min( + (float) crop.width() / displayForThisOrientation.x, + (float) crop.height() / displayForThisOrientation.y)); + } sampleSize = Math.min(sampleSize, sampleSizeForThisOrientation); } // If the total crop has more width or height than either the max texture size @@ -746,8 +759,8 @@ public class WallpaperCropper { final ImageDecoder.Source srcData = ImageDecoder.createSource(wallpaper.getWallpaperFile()); final int finalScale = scale; - final int rescaledBitmapWidth = (int) (0.5f + bitmapSize.x / sampleSize); - final int rescaledBitmapHeight = (int) (0.5f + bitmapSize.y / sampleSize); + final int rescaledBitmapWidth = (int) Math.ceil(bitmapSize.x / sampleSize); + final int rescaledBitmapHeight = (int) Math.ceil(bitmapSize.y / sampleSize); Bitmap cropped = ImageDecoder.decodeBitmap(srcData, (decoder, info, src) -> { if (!multiCrop()) decoder.setTargetSampleSize(finalScale); if (multiCrop()) { diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 10f096c9031b..d019516cd069 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -2380,8 +2380,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub SparseArray<Rect> relativeSuggestedCrops = mWallpaperCropper.getRelativeCropHints(wallpaper); Point croppedBitmapSize = new Point( - (int) (0.5f + wallpaper.cropHint.width() / wallpaper.mSampleSize), - (int) (0.5f + wallpaper.cropHint.height() / wallpaper.mSampleSize)); + (int) Math.ceil(wallpaper.cropHint.width() / wallpaper.mSampleSize), + (int) Math.ceil(wallpaper.cropHint.height() / wallpaper.mSampleSize)); if (croppedBitmapSize.equals(0, 0)) { // There is an ImageWallpaper, but there are no crop hints and the bitmap size is // unknown (e.g. the default wallpaper). Return a special "null" value that will be @@ -2410,6 +2410,27 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } @Override + public Bundle getCurrentBitmapCrops(int which, int userId) { + userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), + Binder.getCallingUid(), userId, false, true, "getBitmapCrop", null); + synchronized (mLock) { + checkPermission(READ_WALLPAPER_INTERNAL); + WallpaperData wallpaper = (which == FLAG_LOCK) ? mLockWallpaperMap.get(userId) + : mWallpaperMap.get(userId); + if (wallpaper == null || !mImageWallpaper.equals(wallpaper.getComponent())) { + return null; + } + Bundle bundle = new Bundle(); + for (int i = 0; i < wallpaper.mCropHints.size(); i++) { + String key = String.valueOf(wallpaper.mCropHints.keyAt(i)); + Rect rect = wallpaper.mCropHints.valueAt(i); + bundle.putParcelable(key, rect); + } + return bundle; + } + } + + @Override public List<Rect> getFutureBitmapCrops(Point bitmapSize, List<Point> displaySizes, int[] screenOrientations, List<Rect> crops) { SparseArray<Rect> cropMap = getCropMap(screenOrientations, crops); diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java index 3f6e91590cce..9a48d5b8880d 100644 --- a/services/core/java/com/android/server/wm/SurfaceAnimator.java +++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java @@ -18,7 +18,6 @@ package com.android.server.wm; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM; import static com.android.server.wm.SurfaceAnimatorProto.ANIMATION_ADAPTER; -import static com.android.server.wm.SurfaceAnimatorProto.ANIMATION_START_DELAYED; import static com.android.server.wm.SurfaceAnimatorProto.LEASH; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; @@ -90,8 +89,6 @@ public class SurfaceAnimator { @Nullable private Runnable mAnimationCancelledCallback; - private boolean mAnimationStartDelayed; - private boolean mAnimationFinished; /** @@ -188,10 +185,6 @@ public class SurfaceAnimator { mAnimatable.onAnimationLeashCreated(t, mLeash); } mAnimatable.onLeashAnimationStarting(t, mLeash); - if (mAnimationStartDelayed) { - ProtoLog.i(WM_DEBUG_ANIM, "Animation start delayed for %s", mAnimatable); - return; - } mAnimation.startAnimation(mLeash, t, type, mInnerAnimationFinishedCallback); if (ProtoLog.isEnabled(WM_DEBUG_ANIM, LogLevel.DEBUG)) { StringWriter sw = new StringWriter(); @@ -215,36 +208,7 @@ public class SurfaceAnimator { null /* animationCancelledCallback */, null /* snapshotAnim */, null /* freezer */); } - /** - * Begins with delaying all animations to start. Any subsequent call to {@link #startAnimation} - * will not start the animation until {@link #endDelayingAnimationStart} is called. When an - * animation start is being delayed, the animator is considered animating already. - */ - void startDelayingAnimationStart() { - - // We only allow delaying animation start we are not currently animating - if (!isAnimating()) { - mAnimationStartDelayed = true; - } - } - - /** - * See {@link #startDelayingAnimationStart}. - */ - void endDelayingAnimationStart() { - final boolean delayed = mAnimationStartDelayed; - mAnimationStartDelayed = false; - if (delayed && mAnimation != null) { - mAnimation.startAnimation(mLeash, mAnimatable.getSyncTransaction(), - mAnimationType, mInnerAnimationFinishedCallback); - mAnimatable.commitPendingTransaction(); - } - } - - /** - * @return Whether we are currently running an animation, or we have a pending animation that - * is waiting to be started with {@link #endDelayingAnimationStart} - */ + /** Returns whether it is currently running an animation. */ boolean isAnimating() { return mAnimation != null; } @@ -290,15 +254,6 @@ public class SurfaceAnimator { } /** - * Reparents the surface. - * - * @see #setLayer - */ - void reparent(Transaction t, SurfaceControl newParent) { - t.reparent(mLeash != null ? mLeash : mAnimatable.getSurfaceControl(), newParent); - } - - /** * @return True if the surface is attached to the leash; false otherwise. */ boolean hasLeash() { @@ -319,7 +274,6 @@ public class SurfaceAnimator { Slog.w(TAG, "Unable to transfer animation, because " + from + " animation is finished"); return; } - endDelayingAnimationStart(); final Transaction t = mAnimatable.getSyncTransaction(); cancelAnimation(t, true /* restarting */, true /* forwardCancel */); mLeash = from.mLeash; @@ -336,10 +290,6 @@ public class SurfaceAnimator { mService.mAnimationTransferMap.put(mAnimation, this); } - boolean isAnimationStartDelayed() { - return mAnimationStartDelayed; - } - /** * Cancels the animation, and resets the leash. * @@ -361,7 +311,7 @@ public class SurfaceAnimator { final SurfaceFreezer.Snapshot snapshot = mSnapshot; reset(t, false); if (animation != null) { - if (!mAnimationStartDelayed && forwardCancel) { + if (forwardCancel) { animation.onAnimationCancelled(leash); if (animationCancelledCallback != null) { animationCancelledCallback.run(); @@ -386,10 +336,6 @@ public class SurfaceAnimator { mService.scheduleAnimationLocked(); } } - - if (!restarting) { - mAnimationStartDelayed = false; - } } private void reset(Transaction t, boolean destroyLeash) { @@ -495,14 +441,12 @@ public class SurfaceAnimator { if (mLeash != null) { mLeash.dumpDebug(proto, LEASH); } - proto.write(ANIMATION_START_DELAYED, mAnimationStartDelayed); proto.end(token); } void dump(PrintWriter pw, String prefix) { pw.print(prefix); pw.print("mLeash="); pw.print(mLeash); - pw.print(" mAnimationType=" + animationTypeToString(mAnimationType)); - pw.println(mAnimationStartDelayed ? " mAnimationStartDelayed=true" : ""); + pw.print(" mAnimationType="); pw.println(animationTypeToString(mAnimationType)); pw.print(prefix); pw.print("Animation: "); pw.println(mAnimation); if (mAnimation != null) { mAnimation.dump(pw, prefix + " "); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index a6034664af5a..20481f25fa5c 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -465,6 +465,31 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return false; } + /** + * This ensures that all changes for previously transient-hide containers are flagged such that + * they will report changes and be included in this transition. + */ + void updateChangesForRestoreTransientHideTasks(Transition transientLaunchTransition) { + if (transientLaunchTransition.mTransientHideTasks == null) { + // Skip if the transient-launch transition has no transient-hide tasks + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + "Skipping update changes for restore transient hide tasks"); + return; + } + + // For each change, if it was previously transient-hidden, then we should force a flag to + // ensure that it is included in the next transition + for (int i = 0; i < mChanges.size(); i++) { + final WindowContainer container = mChanges.keyAt(i); + if (transientLaunchTransition.isInTransientHide(container)) { + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + "Force update transient hide task for restore %d: %s", mSyncId, container); + final ChangeInfo info = mChanges.valueAt(i); + info.mRestoringTransientHide = true; + } + } + } + /** Returns {@code true} if the task should keep visible if this is a transient transition. */ boolean isTransientVisible(@NonNull Task task) { if (mTransientLaunches == null) return false; @@ -3478,6 +3503,10 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // State tracking boolean mExistenceChanged = false; + // This state indicates that we are restoring transient order as a part of an + // end-transition. Because the visibility for transient hide containers has not actually + // changed, we need to ensure that hasChanged() still reports the relevant changes + boolean mRestoringTransientHide = false; // before change state boolean mVisible; int mWindowingMode; @@ -3552,7 +3581,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { || !mContainer.getBounds().equals(mAbsoluteBounds) || mRotation != mContainer.getWindowConfiguration().getRotation() || mDisplayId != getDisplayId(mContainer) - || (mFlags & ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP) != 0; + || (mFlags & ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP) != 0 + // If we are restoring transient-hide containers, then we should consider them + // important for the transition as well (their requested visibilities would not + // have changed for the checks below to consider it). + || mRestoringTransientHide; } @TransitionInfo.TransitionMode @@ -3565,6 +3598,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } final boolean nowVisible = wc.isVisibleRequested(); if (nowVisible == mVisible) { + if (mRestoringTransientHide) { + // The requested visibility has not changed for transient-hide containers, but + // we are restoring them so we should considering them moving to front again + return TRANSIT_TO_FRONT; + } return TRANSIT_CHANGE; } if (mExistenceChanged) { diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 87bdfa4f5d75..143d1b72fff9 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -37,6 +37,7 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; import android.util.ArrayMap; +import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; @@ -524,6 +525,23 @@ class TransitionController { return false; } + /** + * @return A pair of the transition and restore-behind target for the given {@param container}. + * @param container An ancestor of a transient-launch activity + */ + @Nullable + Pair<Transition, Task> getTransientLaunchTransitionAndTarget( + @NonNull WindowContainer container) { + for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { + final Transition transition = mPlayingTransitions.get(i); + final Task restoreBehindTask = transition.getTransientLaunchRestoreTarget(container); + if (restoreBehindTask != null) { + return new Pair<>(transition, restoreBehindTask); + } + } + return null; + } + /** Returns {@code true} if the display contains a transient-launch transition. */ boolean hasTransientLaunch(@NonNull DisplayContent dc) { if (mCollectingTransition != null && mCollectingTransition.hasTransientLaunch() diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index e0c473de0f33..5f92bb626154 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -3215,8 +3215,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< final boolean isChanging = AppTransition.isChangeTransitOld(transit) && enter && isChangingAppTransition(); - // Delaying animation start isn't compatible with remote animations at all. - if (controller != null && !mSurfaceAnimator.isAnimationStartDelayed()) { + if (controller != null) { // Here we load App XML in order to read com.android.R.styleable#Animation_showBackdrop. boolean showBackdrop = false; // Optionally set backdrop color if App explicitly provides it through @@ -3639,20 +3638,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return getAnimatingContainer(PARENTS, ANIMATION_TYPE_ALL); } - /** - * @see SurfaceAnimator#startDelayingAnimationStart - */ - void startDelayingAnimationStart() { - mSurfaceAnimator.startDelayingAnimationStart(); - } - - /** - * @see SurfaceAnimator#endDelayingAnimationStart - */ - void endDelayingAnimationStart() { - mSurfaceAnimator.endDelayingAnimationStart(); - } - @Override public int getSurfaceWidth() { return mSurfaceControl.getWidth(); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index dac8f69a4cae..ead12826c263 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -111,6 +111,7 @@ import android.os.RemoteException; import android.util.AndroidRuntimeException; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.Pair; import android.util.Slog; import android.view.RemoteAnimationAdapter; import android.view.SurfaceControl; @@ -1375,16 +1376,56 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub break; } case HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER: { - if (!chain.isFinishing()) break; + if (!com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + // Only allow restoring transient order when finishing a transition + if (!chain.isFinishing()) break; + } + // Validate the container final WindowContainer container = WindowContainer.fromBinder(hop.getContainer()); - if (container == null) break; + if (container == null) { + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + "Restoring transient order: invalid container"); + break; + } final Task thisTask = container.asActivityRecord() != null ? container.asActivityRecord().getTask() : container.asTask(); - if (thisTask == null) break; - final Task restoreAt = chain.mTransition.getTransientLaunchRestoreTarget(container); - if (restoreAt == null) break; + if (thisTask == null) { + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + "Restoring transient order: invalid task"); + break; + } + + // Find the task to restore behind + final Pair<Transition, Task> transientRestore = + mTransitionController.getTransientLaunchTransitionAndTarget(container); + if (transientRestore == null) { + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + "Restoring transient order: no restore task"); + break; + } + final Transition transientLaunchTransition = transientRestore.first; + final Task restoreAt = transientRestore.second; + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + "Restoring transient order: restoring behind task=%d", restoreAt.mTaskId); + + // Restore the position of the given container behind the target task final TaskDisplayArea taskDisplayArea = thisTask.getTaskDisplayArea(); taskDisplayArea.moveRootTaskBehindRootTask(thisTask.getRootTask(), restoreAt); + + if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + // Because we are in a transient launch transition, the requested visibility of + // tasks does not actually change for the transient-hide tasks, but we do want + // the restoration of these transient-hide tasks to top to be a part of this + // finish transition + final Transition collectingTransition = + mTransitionController.getCollectingTransition(); + if (collectingTransition != null) { + collectingTransition.updateChangesForRestoreTransientHideTasks( + transientLaunchTransition); + } + } + + effects |= TRANSACT_EFFECTS_LIFECYCLE; break; } case HIERARCHY_OP_TYPE_ADD_INSETS_FRAME_PROVIDER: { diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java index cc5573bb01d8..f34ec72d7e27 100644 --- a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java @@ -19,6 +19,7 @@ package com.android.server.policy; import static android.hardware.devicestate.DeviceState.PROPERTY_EMULATED_ONLY; import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT; import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY; +import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT; import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY; import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY; import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED; @@ -71,6 +72,7 @@ public class BookStyleDeviceStatePolicy extends DeviceStatePolicy implements private static final int DEVICE_STATE_OPENED = 2; private static final int DEVICE_STATE_REAR_DISPLAY = 3; private static final int DEVICE_STATE_CONCURRENT_INNER_DEFAULT = 4; + private static final int DEVICE_STATE_REAR_DISPLAY_OUTER_DEFAULT = 5; private static final int TENT_MODE_SWITCH_ANGLE_DEGREES = 90; private static final int TABLE_TOP_MODE_SWITCH_ANGLE_DEGREES = 125; private static final int MIN_CLOSED_ANGLE_DEGREES = 0; @@ -130,14 +132,17 @@ public class BookStyleDeviceStatePolicy extends DeviceStatePolicy implements return hingeAngle >= MAX_CLOSED_ANGLE_DEGREES && hingeAngle <= TABLE_TOP_MODE_SWITCH_ANGLE_DEGREES; }), - createConfig(getOpenedDeviceState(), /* activeStatePredicate= */ - ALLOWED), - createConfig(getRearDisplayDeviceState(), /* activeStatePredicate= */ - NOT_ALLOWED), - createConfig(getDualDisplayDeviceState(), /* activeStatePredicate= */ - NOT_ALLOWED, /* availabilityPredicate= */ - provider -> !mIsDualDisplayBlockingEnabled - || provider.hasNoConnectedExternalDisplay())}; + createConfig(getOpenedDeviceState(), + /* activeStatePredicate= */ ALLOWED), + createConfig(getRearDisplayDeviceState(), + /* activeStatePredicate= */ NOT_ALLOWED), + createConfig(getDualDisplayDeviceState(), + /* activeStatePredicate= */ NOT_ALLOWED, + /* availabilityPredicate= */ provider -> !mIsDualDisplayBlockingEnabled + || provider.hasNoConnectedExternalDisplay()), + createConfig(getRearDisplayOuterDefaultState(), + /* activeStatePredicate= */ NOT_ALLOWED) + }; } private DeviceStatePredicateWrapper createClosedConfiguration( @@ -266,4 +271,24 @@ public class BookStyleDeviceStatePolicy extends DeviceStatePolicy implements .setSystemProperties(systemProperties) .build()); } + + /** + * Returns the {link DeviceState.Configuration} that represents the new rear display state + * where the inner display is also enabled, showing a system affordance to exit the state. + */ + @NonNull + private DeviceState getRearDisplayOuterDefaultState() { + Set<@DeviceState.SystemDeviceStateProperties Integer> systemProperties = new HashSet<>( + List.of(PROPERTY_EMULATED_ONLY, + PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY, + PROPERTY_POLICY_AVAILABLE_FOR_APP_REQUEST, + PROPERTY_FEATURE_REAR_DISPLAY, + PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT)); + + return new DeviceState(new DeviceState.Configuration.Builder( + DEVICE_STATE_REAR_DISPLAY_OUTER_DEFAULT, + "REAR_DISPLAY_OUTER_DEFAULT") + .setSystemProperties(systemProperties) + .build()); + } } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 9759772ae8bd..19b03437292f 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1621,7 +1621,8 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(ROLE_SERVICE_CLASS); t.traceEnd(); - if (!isWatch && android.app.supervision.flags.Flags.supervisionApi()) { + if (android.app.supervision.flags.Flags.supervisionApi() + && (!isWatch || android.app.supervision.flags.Flags.supervisionApiOnWear())) { t.traceBegin("StartSupervisionService"); mSystemServiceManager.startService(SupervisionService.Lifecycle.class); t.traceEnd(); diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java new file mode 100644 index 000000000000..1be5cef28244 --- /dev/null +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import static android.content.pm.Flags.FLAG_REDUCE_BROADCASTS_FOR_COMPONENT_STATE_CHANGES; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Message; +import android.os.UserHandle; +import android.platform.test.annotations.AppModeFull; +import android.platform.test.annotations.AppModeNonSdkSandbox; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.internal.pm.parsing.pkg.AndroidPackageInternal; +import com.android.internal.pm.pkg.component.ParsedActivity; +import com.android.server.pm.pkg.PackageStateInternal; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; + +@AppModeFull +@AppModeNonSdkSandbox +@RunWith(AndroidJUnit4.class) +public class BroadcastHelperTest { + private static final String TAG = "BroadcastHelperTest"; + private static final String PACKAGE_CHANGED_TEST_PACKAGE_NAME = "testpackagename"; + private static final String PACKAGE_CHANGED_TEST_MAIN_ACTIVITY = + PACKAGE_CHANGED_TEST_PACKAGE_NAME + ".MainActivity"; + private static final String PERMISSION_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED = + "android.permission.INTERNAL_RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED"; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Mock + ActivityManagerInternal mMockActivityManagerInternal; + @Mock + AndroidPackageInternal mMockAndroidPackageInternal; + @Mock + Computer mMockSnapshot; + @Mock + Handler mMockHandler; + @Mock + PackageManagerServiceInjector mMockPackageManagerServiceInjector; + @Mock + PackageMonitorCallbackHelper mMockPackageMonitorCallbackHelper; + @Mock + PackageStateInternal mMockPackageStateInternal; + @Mock + ParsedActivity mMockParsedActivity; + @Mock + UserManagerInternal mMockUserManagerInternal; + + private Context mContext; + private BroadcastHelper mBroadcastHelper; + + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + + when(mMockHandler.sendMessageAtTime(any(Message.class), anyLong())).thenAnswer( + i -> { + ((Message) i.getArguments()[0]).getCallback().run(); + return true; + }); + when(mMockPackageManagerServiceInjector.getActivityManagerInternal()).thenReturn( + mMockActivityManagerInternal); + when(mMockPackageManagerServiceInjector.getContext()).thenReturn(mContext); + when(mMockPackageManagerServiceInjector.getHandler()).thenReturn(mMockHandler); + when(mMockPackageManagerServiceInjector.getPackageMonitorCallbackHelper()).thenReturn( + mMockPackageMonitorCallbackHelper); + when(mMockPackageManagerServiceInjector.getUserManagerInternal()).thenReturn( + mMockUserManagerInternal); + + mBroadcastHelper = new BroadcastHelper(mMockPackageManagerServiceInjector); + } + + @RequiresFlagsEnabled(FLAG_REDUCE_BROADCASTS_FOR_COMPONENT_STATE_CHANGES) + @Test + public void changeNonExportedComponent_sendPackageChangedBroadcastToSystem_withPermission() + throws Exception { + changeComponentAndSendPackageChangedBroadcast(false /* changeExportedComponent */); + + ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); + verify(mMockActivityManagerInternal).broadcastIntentWithCallback( + captor.capture(), eq(null), + eq(new String[]{PERMISSION_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED}), + anyInt(), eq(null), eq(null), eq(null)); + Intent intent = captor.getValue(); + assertNotNull(intent); + assertThat(intent.getPackage()).isEqualTo("android"); + } + + @RequiresFlagsEnabled(FLAG_REDUCE_BROADCASTS_FOR_COMPONENT_STATE_CHANGES) + @Test + public void changeNonExportedComponent_sendPackageChangedBroadcastToApplicationItself() + throws Exception { + changeComponentAndSendPackageChangedBroadcast(false /* changeExportedComponent */); + + ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); + verify(mMockActivityManagerInternal).broadcastIntentWithCallback(captor.capture(), eq(null), + eq(null), anyInt(), eq(null), eq(null), eq(null)); + Intent intent = captor.getValue(); + assertNotNull(intent); + assertThat(intent.getPackage()).isEqualTo(PACKAGE_CHANGED_TEST_PACKAGE_NAME); + } + + @Test + public void changeExportedComponent_sendPackageChangedBroadcastToAll() throws Exception { + changeComponentAndSendPackageChangedBroadcast(true /* changeExportedComponent */); + + ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); + verify(mMockActivityManagerInternal).broadcastIntentWithCallback(captor.capture(), eq(null), + eq(null), anyInt(), eq(null), eq(null), eq(null)); + Intent intent = captor.getValue(); + assertNotNull(intent); + assertNull(intent.getPackage()); + } + + private void changeComponentAndSendPackageChangedBroadcast(boolean changeExportedComponent) { + when(mMockSnapshot.getPackageStateInternal(eq(PACKAGE_CHANGED_TEST_PACKAGE_NAME), + anyInt())).thenReturn(mMockPackageStateInternal); + when(mMockSnapshot.isInstantAppInternal(any(), anyInt(), anyInt())).thenReturn(false); + when(mMockSnapshot.getVisibilityAllowLists(any(), any())).thenReturn(null); + when(mMockPackageStateInternal.getPkg()).thenReturn(mMockAndroidPackageInternal); + + when(mMockParsedActivity.getClassName()).thenReturn( + PACKAGE_CHANGED_TEST_MAIN_ACTIVITY); + when(mMockParsedActivity.isExported()).thenReturn(changeExportedComponent); + ArrayList<ParsedActivity> parsedActivities = new ArrayList<>(); + parsedActivities.add(mMockParsedActivity); + + when(mMockAndroidPackageInternal.getReceivers()).thenReturn(new ArrayList<>()); + when(mMockAndroidPackageInternal.getProviders()).thenReturn(new ArrayList<>()); + when(mMockAndroidPackageInternal.getServices()).thenReturn(new ArrayList<>()); + when(mMockAndroidPackageInternal.getActivities()).thenReturn(parsedActivities); + + doNothing().when(mMockPackageMonitorCallbackHelper).notifyPackageChanged(any(), + anyBoolean(), any(), anyInt(), any(), any(), any(), any(), any()); + when(mMockActivityManagerInternal.broadcastIntentWithCallback(any(), any(), any(), anyInt(), + any(), any(), any())).thenReturn(ActivityManager.BROADCAST_SUCCESS); + + ArrayList<String> componentNames = new ArrayList<>(); + componentNames.add(PACKAGE_CHANGED_TEST_MAIN_ACTIVITY); + + mBroadcastHelper.sendPackageChangedBroadcast(mMockSnapshot, + PACKAGE_CHANGED_TEST_PACKAGE_NAME, true /* dontKillApp */, componentNames, + UserHandle.USER_SYSTEM, "test" /* reason */); + } +} diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt index 09d0e4a82f7f..5a59c57ddf28 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt @@ -201,7 +201,8 @@ class PackageInstallerSessionTest { /* preVerifiedDomains */ DomainSet(setOf("com.foo", "com.bar")), /* VerifierController */ mock(VerifierController::class.java), /* initialVerificationPolicy */ VERIFICATION_POLICY_BLOCK_FAIL_OPEN, - /* currentVerificationPolicy */ VERIFICATION_POLICY_BLOCK_FAIL_CLOSED + /* currentVerificationPolicy */ VERIFICATION_POLICY_BLOCK_FAIL_CLOSED, + /* installDependencyHelper */ null ) } @@ -256,7 +257,8 @@ class PackageInstallerSessionTest { mTmpDir, mock(PackageSessionProvider::class.java), mock(SilentUpdatePolicy::class.java), - mock(VerifierController::class.java) + mock(VerifierController::class.java), + mock(InstallDependencyHelper::class.java) ) ret.add(session) } catch (e: Exception) { diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp index be698b2673ad..6acf2421ba75 100644 --- a/services/tests/mockingservicestests/Android.bp +++ b/services/tests/mockingservicestests/Android.bp @@ -109,6 +109,10 @@ android_test { optimize: { enabled: false, }, + + data: [ + ":HelloWorldUsingSdk1And2", + ], } java_library { @@ -134,6 +138,7 @@ android_ravenwood_test { "androidx.annotation_annotation", "androidx.test.rules", "services.core", + "servicestests-utils-mockito-extended", ], srcs: [ "src/com/android/server/am/BroadcastRecordTest.java", diff --git a/services/tests/mockingservicestests/AndroidTest.xml b/services/tests/mockingservicestests/AndroidTest.xml index 7782d570856f..2b90119145bd 100644 --- a/services/tests/mockingservicestests/AndroidTest.xml +++ b/services/tests/mockingservicestests/AndroidTest.xml @@ -23,6 +23,12 @@ <option name="test-file-name" value="FrameworksMockingServicesTests.apk" /> </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="cleanup" value="true"/> + <option name="push-file" key="HelloWorldUsingSdk1And2.apk" + value="/data/local/tmp/tests/smockingservicestest/pm/HelloWorldUsingSdk1And2.apk"/> + </target_preparer> + <option name="test-tag" value="FrameworksMockingServicesTests" /> <test class="com.android.tradefed.testtype.AndroidJUnitTest" > diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java index d602660597ff..a1a8b0ec7d2f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java @@ -41,6 +41,7 @@ import android.os.TestLooperManager; import android.os.UserHandle; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.util.SparseArray; @@ -97,6 +98,9 @@ public abstract class BaseBroadcastQueueTest { .spyStatic(ProcessList.class) .build(); + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @@ -112,6 +116,8 @@ public abstract class BaseBroadcastQueueTest { AlarmManagerInternal mAlarmManagerInt; @Mock ProcessList mProcessList; + @Mock + PlatformCompat mPlatformCompat; @Mock AppStartInfoTracker mAppStartInfoTracker; @@ -178,6 +184,11 @@ public abstract class BaseBroadcastQueueTest { doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any()); doReturn(mAppStartInfoTracker).when(mProcessList).getAppStartInfoTracker(); + + doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastFilter.CHANGE_RESTRICT_PRIORITY_VALUES), anyInt()); + doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), anyInt()); } public void tearDown() throws Exception { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index 100b54897573..1481085c5f71 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -18,6 +18,7 @@ package com.android.server.am; import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED; import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD; @@ -49,7 +50,6 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -65,7 +65,6 @@ import android.content.IIntentReceiver; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; -import android.content.pm.ResolveInfo; import android.media.AudioManager; import android.os.Bundle; import android.os.BundleMerger; @@ -73,6 +72,8 @@ import android.os.DropBoxManager; import android.os.Process; import android.os.SystemClock; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.util.IndentingPrintWriter; import android.util.Pair; @@ -182,10 +183,6 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { return mock(Intent.class); } - private static ResolveInfo makeMockManifestReceiver() { - return mock(ResolveInfo.class); - } - private static BroadcastFilter makeMockRegisteredReceiver() { return mock(BroadcastFilter.class); } @@ -214,7 +211,8 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { return new BroadcastRecord(mImpl, intent, mProcess, PACKAGE_RED, null, 21, TEST_UID, false, null, null, null, null, AppOpsManager.OP_NONE, options, receivers, null, resultTo, Activity.RESULT_OK, null, null, ordered, false, false, UserHandle.USER_SYSTEM, - BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN); + BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN, + mPlatformCompat); } private void enqueueOrReplaceBroadcast(BroadcastProcessQueue queue, @@ -646,7 +644,8 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { @Test public void testRunnableAt_Cached_Manifest() { doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), null, - List.of(makeMockManifestReceiver()), null, false), REASON_CONTAINS_MANIFEST); + List.of(makeManifestReceiver(PACKAGE_RED, CLASS_RED)), null, false), + REASON_CONTAINS_MANIFEST); } @Test @@ -679,6 +678,19 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { List.of(makeMockRegisteredReceiver()), null, false), REASON_CONTAINS_ALARM); } + @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testRunnableAt_Cached_Prioritized_NonDeferrable_flagDisabled() { + final List receivers = List.of( + withPriority(makeManifestReceiver(PACKAGE_RED, PACKAGE_RED), 10), + withPriority(makeManifestReceiver(PACKAGE_GREEN, PACKAGE_GREEN), -10)); + final BroadcastOptions options = BroadcastOptions.makeBasic() + .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE); + doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options, + receivers, null, false), REASON_CONTAINS_PRIORITIZED); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) @Test public void testRunnableAt_Cached_Prioritized_NonDeferrable() { final List receivers = List.of( @@ -687,6 +699,32 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { final BroadcastOptions options = BroadcastOptions.makeBasic() .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE); doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options, + receivers, null, false), REASON_CONTAINS_MANIFEST); + } + + @Test + public void testRunnableAt_Cached_Ordered_NonDeferrable() { + final List receivers = List.of( + withPriority(makeManifestReceiver(PACKAGE_RED, PACKAGE_RED), 10), + withPriority(makeManifestReceiver(PACKAGE_GREEN, PACKAGE_GREEN), -10)); + final BroadcastOptions options = BroadcastOptions.makeBasic() + .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE); + doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options, + receivers, mock(IIntentReceiver.class), true), REASON_CONTAINS_ORDERED); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testRunnableAt_Cached_Prioritized_NonDeferrable_changeIdDisabled() { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), + eq(getUidForPackage(PACKAGE_GREEN))); + final List receivers = List.of( + withPriority(makeManifestReceiver(PACKAGE_RED, PACKAGE_RED), 10), + withPriority(makeManifestReceiver(PACKAGE_GREEN, PACKAGE_GREEN), -10)); + final BroadcastOptions options = BroadcastOptions.makeBasic() + .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE); + doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options, receivers, null, false), REASON_CONTAINS_PRIORITIZED); } @@ -1136,6 +1174,63 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { verifyPendingRecords(blueQueue, List.of(screenOn)); } + @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @SuppressWarnings("GuardedBy") + @Test + public void testDeliveryGroupPolicy_prioritized_diffReceivers_flagDisabled() { + final Intent screenOn = new Intent(Intent.ACTION_SCREEN_ON); + final Intent screenOff = new Intent(Intent.ACTION_SCREEN_OFF); + final BroadcastOptions screenOnOffOptions = BroadcastOptions.makeBasic() + .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) + .setDeliveryGroupMatchingKey("screenOnOff", Intent.ACTION_SCREEN_ON); + + final Object greenReceiver = withPriority( + makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10); + final Object redReceiver = withPriority( + makeManifestReceiver(PACKAGE_RED, CLASS_RED), 5); + final Object blueReceiver = withPriority( + makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE), 0); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions, + List.of(greenReceiver, blueReceiver), false)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions, + List.of(greenReceiver, redReceiver, blueReceiver), false)); + final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED, + getUidForPackage(PACKAGE_RED)); + final BroadcastProcessQueue blueQueue = mImpl.getProcessQueue(PACKAGE_BLUE, + getUidForPackage(PACKAGE_BLUE)); + verifyPendingRecords(greenQueue, List.of(screenOff)); + verifyPendingRecords(redQueue, List.of(screenOff)); + verifyPendingRecords(blueQueue, List.of(screenOff)); + + assertTrue(greenQueue.isEmpty()); + assertTrue(redQueue.isEmpty()); + assertTrue(blueQueue.isEmpty()); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions, + List.of(greenReceiver, redReceiver, blueReceiver), false)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions, + List.of(greenReceiver, blueReceiver), false)); + verifyPendingRecords(greenQueue, List.of(screenOff, screenOn)); + verifyPendingRecords(redQueue, List.of(screenOff)); + verifyPendingRecords(blueQueue, List.of(screenOff, screenOn)); + + final BroadcastRecord screenOffRecord = makeBroadcastRecord(screenOff, screenOnOffOptions, + List.of(greenReceiver, redReceiver, blueReceiver), false); + screenOffRecord.setDeliveryState(2, BroadcastRecord.DELIVERY_DEFERRED, + "testDeliveryGroupPolicy_prioritized_diffReceivers_flagDisabled"); + mImpl.enqueueBroadcastLocked(screenOffRecord); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions, + List.of(greenReceiver, blueReceiver), false)); + verifyPendingRecords(greenQueue, List.of(screenOff, screenOn)); + verifyPendingRecords(redQueue, List.of(screenOff)); + verifyPendingRecords(blueQueue, List.of(screenOn)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @SuppressWarnings("GuardedBy") @Test public void testDeliveryGroupPolicy_prioritized_diffReceivers() { final Intent screenOn = new Intent(Intent.ACTION_SCREEN_ON); @@ -1173,6 +1268,65 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { List.of(greenReceiver, redReceiver, blueReceiver), false)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions, List.of(greenReceiver, blueReceiver), false)); + verifyPendingRecords(greenQueue, List.of(screenOn)); + verifyPendingRecords(redQueue, List.of(screenOff)); + verifyPendingRecords(blueQueue, List.of(screenOn)); + + final BroadcastRecord screenOffRecord = makeBroadcastRecord(screenOff, screenOnOffOptions, + List.of(greenReceiver, redReceiver, blueReceiver), false); + screenOffRecord.setDeliveryState(2, BroadcastRecord.DELIVERY_DEFERRED, + "testDeliveryGroupPolicy_prioritized_diffReceivers"); + mImpl.enqueueBroadcastLocked(screenOffRecord); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions, + List.of(greenReceiver, blueReceiver), false)); + verifyPendingRecords(greenQueue, List.of(screenOn)); + verifyPendingRecords(redQueue, List.of(screenOff)); + verifyPendingRecords(blueQueue, List.of(screenOn)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @SuppressWarnings("GuardedBy") + @Test + public void testDeliveryGroupPolicy_prioritized_diffReceivers_changeIdDisabled() { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), + eq(getUidForPackage(PACKAGE_GREEN))); + + final Intent screenOn = new Intent(Intent.ACTION_SCREEN_ON); + final Intent screenOff = new Intent(Intent.ACTION_SCREEN_OFF); + final BroadcastOptions screenOnOffOptions = BroadcastOptions.makeBasic() + .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) + .setDeliveryGroupMatchingKey("screenOnOff", Intent.ACTION_SCREEN_ON); + + final Object greenReceiver = withPriority( + makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10); + final Object redReceiver = withPriority( + makeManifestReceiver(PACKAGE_RED, CLASS_RED), 5); + final Object blueReceiver = withPriority( + makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE), 0); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions, + List.of(greenReceiver, blueReceiver), false)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions, + List.of(greenReceiver, redReceiver, blueReceiver), false)); + final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED, + getUidForPackage(PACKAGE_RED)); + final BroadcastProcessQueue blueQueue = mImpl.getProcessQueue(PACKAGE_BLUE, + getUidForPackage(PACKAGE_BLUE)); + verifyPendingRecords(greenQueue, List.of(screenOff)); + verifyPendingRecords(redQueue, List.of(screenOff)); + verifyPendingRecords(blueQueue, List.of(screenOff)); + + assertTrue(greenQueue.isEmpty()); + assertTrue(redQueue.isEmpty()); + assertTrue(blueQueue.isEmpty()); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions, + List.of(greenReceiver, redReceiver, blueReceiver), false)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions, + List.of(greenReceiver, blueReceiver), false)); verifyPendingRecords(greenQueue, List.of(screenOff, screenOn)); verifyPendingRecords(redQueue, List.of(screenOff)); verifyPendingRecords(blueQueue, List.of(screenOff, screenOn)); @@ -1569,8 +1723,9 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { verifyPendingRecords(redQueue, List.of(userPresent, timeTick)); } + @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) @Test - public void testDeliveryDeferredForCached() throws Exception { + public void testDeliveryDeferredForCached_flagDisabled() throws Exception { final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED)); @@ -1664,8 +1819,217 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { }, false /* andRemove */); } + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @SuppressWarnings("GuardedBy") + @Test + public void testDeliveryDeferredForCached_changeIdDisabled() throws Exception { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), + eq(getUidForPackage(PACKAGE_GREEN))); + + final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); + final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED)); + + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); + final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick, + List.of(makeRegisteredReceiver(greenProcess, 0))); + + final Intent batteryChanged = new Intent(Intent.ACTION_BATTERY_CHANGED); + final BroadcastOptions optionsBatteryChanged = + BroadcastOptions.makeWithDeferUntilActive(true); + final BroadcastRecord batteryChangedRecord = makeBroadcastRecord(batteryChanged, + optionsBatteryChanged, + List.of(makeRegisteredReceiver(greenProcess, 10), + makeRegisteredReceiver(redProcess, 0)), + false /* ordered */); + + mImpl.enqueueBroadcastLocked(timeTickRecord); + mImpl.enqueueBroadcastLocked(batteryChangedRecord); + + final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED, + getUidForPackage(PACKAGE_RED)); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason()); + assertFalse(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_BLOCKED, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // Simulate process state change + greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */, + true /* processFreezable */); + greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply, + mImpl.mBroadcastConsumerDeferClear); + + assertEquals(BroadcastProcessQueue.REASON_CACHED, greenQueue.getRunnableAtReason()); + assertTrue(greenQueue.shouldBeDeferred()); + // Once the broadcasts to green process are deferred, broadcasts to red process + // shouldn't be blocked anymore. + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // All broadcasts to green process should be deferred. + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + + final Intent packageChanged = new Intent(Intent.ACTION_PACKAGE_CHANGED); + final BroadcastRecord packageChangedRecord = makeBroadcastRecord(packageChanged, + List.of(makeRegisteredReceiver(greenProcess, 0))); + mImpl.enqueueBroadcastLocked(packageChangedRecord); + + assertEquals(BroadcastProcessQueue.REASON_CACHED, greenQueue.getRunnableAtReason()); + assertTrue(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // All broadcasts to the green process, including the newly enqueued one, should be + // deferred. + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + + // Simulate process state change + greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */, + false /* processFreezable */); + greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply, + mImpl.mBroadcastConsumerDeferClear); + + assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason()); + assertFalse(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + } + + @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @SuppressWarnings("GuardedBy") + @Test + public void testDeliveryDeferredForCached_withInfiniteDeferred_flagDisabled() throws Exception { + final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); + final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED)); + + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); + final BroadcastOptions optionsTimeTick = BroadcastOptions.makeWithDeferUntilActive(true); + final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick, optionsTimeTick, + List.of(makeRegisteredReceiver(greenProcess, 0)), false /* ordered */); + + final Intent batteryChanged = new Intent(Intent.ACTION_BATTERY_CHANGED); + final BroadcastOptions optionsBatteryChanged = + BroadcastOptions.makeWithDeferUntilActive(true); + final BroadcastRecord batteryChangedRecord = makeBroadcastRecord(batteryChanged, + optionsBatteryChanged, + List.of(makeRegisteredReceiver(greenProcess, 10), + makeRegisteredReceiver(redProcess, 0)), + false /* ordered */); + + mImpl.enqueueBroadcastLocked(timeTickRecord); + mImpl.enqueueBroadcastLocked(batteryChangedRecord); + + final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED, + getUidForPackage(PACKAGE_RED)); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason()); + assertFalse(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_BLOCKED, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // Simulate process state change + greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */, + true /* processFreezable */); + greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply, + mImpl.mBroadcastConsumerDeferClear); + + assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER, + greenQueue.getRunnableAtReason()); + assertTrue(greenQueue.shouldBeDeferred()); + // Once the broadcasts to green process are deferred, broadcasts to red process + // shouldn't be blocked anymore. + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // All broadcasts to green process should be deferred. + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + + final Intent packageChanged = new Intent(Intent.ACTION_PACKAGE_CHANGED); + final BroadcastOptions optionsPackageChanged = + BroadcastOptions.makeWithDeferUntilActive(true); + final BroadcastRecord packageChangedRecord = makeBroadcastRecord(packageChanged, + optionsPackageChanged, + List.of(makeRegisteredReceiver(greenProcess, 0)), false /* ordered */); + mImpl.enqueueBroadcastLocked(packageChangedRecord); + + assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER, + greenQueue.getRunnableAtReason()); + assertTrue(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // All broadcasts to the green process, including the newly enqueued one, should be + // deferred. + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + + // Simulate process state change + greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */, + false /* processFreezable */); + greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply, + mImpl.mBroadcastConsumerDeferClear); + + assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason()); + assertFalse(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) @Test - public void testDeliveryDeferredForCached_withInfiniteDeferred() throws Exception { + public void testDeliveryDeferredForCached_withInfiniteDeferred_changeIdDisabled() + throws Exception { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), + eq(getUidForPackage(PACKAGE_GREEN))); final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED)); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index 3aaf2e5c61a6..9d92d5fe4f60 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -21,6 +21,7 @@ import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_FINISH_RECEIVER import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_START_RECEIVER; import static android.os.UserHandle.USER_SYSTEM; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.server.am.ActivityManagerDebugConfig.LOG_WRITER_INFO; import static com.android.server.am.BroadcastProcessQueue.reasonToString; import static com.android.server.am.BroadcastRecord.deliveryStateToString; @@ -45,7 +46,6 @@ import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -77,6 +77,8 @@ import android.os.IBinder; import android.os.PowerExemptionManager; import android.os.SystemClock; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.RequiresFlagsEnabled; import android.util.ArrayMap; import android.util.Log; @@ -446,7 +448,8 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { callerApp.getPid(), callerApp.info.uid, false, null, null, null, null, AppOpsManager.OP_NONE, options, receivers, callerApp, resultTo, Activity.RESULT_OK, null, resultExtras, ordered, false, false, userId, - BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN); + BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN, + mPlatformCompat); } private void assertHealth() { @@ -1495,7 +1498,7 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { null, null, null, null, AppOpsManager.OP_NONE, BroadcastOptions.makeBasic(), List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), null, null, Activity.RESULT_OK, null, null, false, false, false, UserHandle.USER_SYSTEM, - backgroundStartPrivileges, false, null, PROCESS_STATE_UNKNOWN); + backgroundStartPrivileges, false, null, PROCESS_STATE_UNKNOWN, mPlatformCompat); enqueueBroadcast(r); waitForIdle(); @@ -1550,8 +1553,10 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { /** * Verify that when dispatching we respect tranches of priority. */ + @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @SuppressWarnings("DistinctVarargsChecker") @Test - public void testPriority() throws Exception { + public void testPriority_flagDisabled() throws Exception { final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); @@ -1594,6 +1599,106 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { } /** + * Verify that when dispatching we respect tranches of priority. + */ + @SuppressWarnings("DistinctVarargsChecker") + @Test + public void testOrdered_withPriorities() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); + final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); + final ProcessRecord receiverYellowApp = makeActiveProcessRecord(PACKAGE_YELLOW); + + // Enqueue a normal broadcast that will go to several processes, and + // then enqueue a foreground broadcast that risks reordering + final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED); + final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + final IIntentReceiver orderedResultTo = mock(IIntentReceiver.class); + enqueueBroadcast(makeOrderedBroadcastRecord(timezone, callerApp, + List.of(makeRegisteredReceiver(receiverBlueApp, 10), + makeRegisteredReceiver(receiverGreenApp, 10), + makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE), + makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW), + makeRegisteredReceiver(receiverYellowApp, -10)), + orderedResultTo, null)); + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, + List.of(makeRegisteredReceiver(receiverBlueApp)))); + waitForIdle(); + + // Ignore the final foreground broadcast + mScheduledBroadcasts.remove(makeScheduledBroadcast(receiverBlueApp, airplane)); + assertEquals(6, mScheduledBroadcasts.size()); + + // We're only concerned about enforcing ordering between tranches; + // within a tranche we're okay with reordering + assertEquals( + Set.of(makeScheduledBroadcast(receiverBlueApp, timezone), + makeScheduledBroadcast(receiverGreenApp, timezone)), + Set.of(mScheduledBroadcasts.remove(0), + mScheduledBroadcasts.remove(0))); + assertEquals( + Set.of(makeScheduledBroadcast(receiverBlueApp, timezone), + makeScheduledBroadcast(receiverYellowApp, timezone)), + Set.of(mScheduledBroadcasts.remove(0), + mScheduledBroadcasts.remove(0))); + assertEquals( + Set.of(makeScheduledBroadcast(receiverYellowApp, timezone)), + Set.of(mScheduledBroadcasts.remove(0))); + } + + /** + * Verify that when dispatching we respect tranches of priority. + */ + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @SuppressWarnings("DistinctVarargsChecker") + @Test + public void testPriority_changeIdDisabled() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); + final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); + final ProcessRecord receiverYellowApp = makeActiveProcessRecord(PACKAGE_YELLOW); + + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid)); + + // Enqueue a normal broadcast that will go to several processes, and + // then enqueue a foreground broadcast that risks reordering + final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED); + final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + enqueueBroadcast(makeBroadcastRecord(timezone, callerApp, + List.of(makeRegisteredReceiver(receiverBlueApp, 10), + makeRegisteredReceiver(receiverGreenApp, 10), + makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE), + makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW), + makeRegisteredReceiver(receiverYellowApp, -10)))); + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, + List.of(makeRegisteredReceiver(receiverBlueApp)))); + waitForIdle(); + + // Ignore the final foreground broadcast + mScheduledBroadcasts.remove(makeScheduledBroadcast(receiverBlueApp, airplane)); + assertEquals(5, mScheduledBroadcasts.size()); + + // We're only concerned about enforcing ordering between tranches; + // within a tranche we're okay with reordering + assertEquals( + Set.of(makeScheduledBroadcast(receiverBlueApp, timezone), + makeScheduledBroadcast(receiverGreenApp, timezone)), + Set.of(mScheduledBroadcasts.remove(0), + mScheduledBroadcasts.remove(0))); + assertEquals( + Set.of(makeScheduledBroadcast(receiverBlueApp, timezone), + makeScheduledBroadcast(receiverYellowApp, timezone)), + Set.of(mScheduledBroadcasts.remove(0), + mScheduledBroadcasts.remove(0))); + assertEquals( + Set.of(makeScheduledBroadcast(receiverYellowApp, timezone)), + Set.of(mScheduledBroadcasts.remove(0))); + } + + /** * Verify prioritized receivers work as expected with deferrable broadcast - broadcast to * app in cached state should be deferred and the rest should be delivered as per the priority * order. @@ -2305,8 +2410,35 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { .isLessThan(getReceiverScheduledTime(timeTickRecord, receiverBlue)); } + @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testPrioritizedBroadcastDelivery_uidForeground_flagDisabled() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); + final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); + + mUidObserver.onUidStateChanged(receiverGreenApp.info.uid, + ActivityManager.PROCESS_STATE_TOP, 0, ActivityManager.PROCESS_CAPABILITY_NONE); + waitForIdle(); + + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); + + final BroadcastFilter receiverBlue = makeRegisteredReceiver(receiverBlueApp, 10); + final BroadcastFilter receiverGreen = makeRegisteredReceiver(receiverGreenApp, 5); + final BroadcastRecord prioritizedRecord = makeBroadcastRecord(timeTick, callerApp, + List.of(receiverBlue, receiverGreen)); + + enqueueBroadcast(prioritizedRecord); + + waitForIdle(); + // Verify that uid foreground-ness does not impact that delivery of prioritized broadcast. + // That is, broadcast to receiverBlueApp gets scheduled before the one to receiverGreenApp. + assertThat(getReceiverScheduledTime(prioritizedRecord, receiverGreen)) + .isGreaterThan(getReceiverScheduledTime(prioritizedRecord, receiverBlue)); + } + @Test - public void testPrioritizedBroadcastDelivery_uidForeground() throws Exception { + public void testOrderedBroadcastDelivery_uidForeground() throws Exception { final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); @@ -2319,6 +2451,37 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { final BroadcastFilter receiverBlue = makeRegisteredReceiver(receiverBlueApp, 10); final BroadcastFilter receiverGreen = makeRegisteredReceiver(receiverGreenApp, 5); + final IIntentReceiver resultTo = mock(IIntentReceiver.class); + final BroadcastRecord prioritizedRecord = makeOrderedBroadcastRecord(timeTick, callerApp, + List.of(receiverBlue, receiverGreen), resultTo, null); + + enqueueBroadcast(prioritizedRecord); + + waitForIdle(); + // Verify that uid foreground-ness does not impact that delivery of prioritized broadcast. + // That is, broadcast to receiverBlueApp gets scheduled before the one to receiverGreenApp. + assertThat(getReceiverScheduledTime(prioritizedRecord, receiverGreen)) + .isGreaterThan(getReceiverScheduledTime(prioritizedRecord, receiverBlue)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testPrioritizedBroadcastDelivery_uidForeground_changeIdDisabled() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); + final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); + + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid)); + + mUidObserver.onUidStateChanged(receiverGreenApp.info.uid, + ActivityManager.PROCESS_STATE_TOP, 0, ActivityManager.PROCESS_CAPABILITY_NONE); + waitForIdle(); + + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); + + final BroadcastFilter receiverBlue = makeRegisteredReceiver(receiverBlueApp, 10); + final BroadcastFilter receiverGreen = makeRegisteredReceiver(receiverGreenApp, 5); final BroadcastRecord prioritizedRecord = makeBroadcastRecord(timeTick, callerApp, List.of(receiverBlue, receiverGreen)); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java index 8cd0da721364..4a370a3cc431 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java @@ -18,6 +18,8 @@ package com.android.server.am; import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.server.am.BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE; import static com.android.server.am.BroadcastRecord.DELIVERY_DEFERRED; import static com.android.server.am.BroadcastRecord.DELIVERY_DELIVERED; import static com.android.server.am.BroadcastRecord.DELIVERY_PENDING; @@ -33,6 +35,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import android.app.BackgroundStartPrivileges; import android.app.BroadcastOptions; @@ -46,11 +50,17 @@ import android.os.Bundle; import android.os.PersistableBundle; import android.os.Process; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.telephony.SubscriptionManager; import androidx.test.filters.SmallTest; +import com.android.server.compat.PlatformCompat; + import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -73,6 +83,9 @@ import java.util.function.BiFunction; public class BroadcastRecordTest { private static final String TAG = "BroadcastRecordTest"; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private static final int USER0 = UserHandle.USER_SYSTEM; private static final String PACKAGE1 = "pkg1"; private static final String PACKAGE2 = "pkg2"; @@ -89,10 +102,14 @@ public class BroadcastRecordTest { @Mock BroadcastQueue mQueue; @Mock ProcessRecord mProcess; + @Mock PlatformCompat mPlatformCompat; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + + doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), anyInt()); } @Test @@ -108,13 +125,13 @@ public class BroadcastRecordTest { assertArrayEquals(new int[] {-1}, calculateBlockedUntilBeyondCount(List.of( - createResolveInfo(PACKAGE1, getAppId(1), 0)), false)); + createResolveInfo(PACKAGE1, getAppId(1), 0)), false, mPlatformCompat)); assertArrayEquals(new int[] {-1}, calculateBlockedUntilBeyondCount(List.of( - createResolveInfo(PACKAGE1, getAppId(1), -10)), false)); + createResolveInfo(PACKAGE1, getAppId(1), -10)), false, mPlatformCompat)); assertArrayEquals(new int[] {-1}, calculateBlockedUntilBeyondCount(List.of( - createResolveInfo(PACKAGE1, getAppId(1), 10)), false)); + createResolveInfo(PACKAGE1, getAppId(1), 10)), false, mPlatformCompat)); } @Test @@ -128,18 +145,19 @@ public class BroadcastRecordTest { createResolveInfo(PACKAGE2, getAppId(2), 10), createResolveInfo(PACKAGE3, getAppId(3), 10)))); - assertArrayEquals(new int[] {-1,-1,-1}, + assertArrayEquals(new int[] {-1, -1, -1}, calculateBlockedUntilBeyondCount(List.of( createResolveInfo(PACKAGE1, getAppId(1), 0), createResolveInfo(PACKAGE2, getAppId(2), 0), - createResolveInfo(PACKAGE3, getAppId(3), 0)), false)); - assertArrayEquals(new int[] {-1,-1,-1}, + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + assertArrayEquals(new int[] {-1, -1, -1}, calculateBlockedUntilBeyondCount(List.of( createResolveInfo(PACKAGE1, getAppId(1), 10), createResolveInfo(PACKAGE2, getAppId(2), 10), - createResolveInfo(PACKAGE3, getAppId(3), 10)), false)); + createResolveInfo(PACKAGE3, getAppId(3), 10)), false, mPlatformCompat)); } + @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) @Test public void testIsPrioritized_Yes() { assertTrue(isPrioritized(List.of( @@ -151,18 +169,203 @@ public class BroadcastRecordTest { createResolveInfo(PACKAGE2, getAppId(2), 0), createResolveInfo(PACKAGE3, getAppId(3), 0)))); - assertArrayEquals(new int[] {0,1,2}, + assertArrayEquals(new int[] {0, 1, 2}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 2, 3, 3}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 20), + createResolveInfo(PACKAGE2, getAppId(2), 20), + createResolveInfo(PACKAGE3, getAppId(3), 10), + createResolveInfo(PACKAGE3, getAppId(3), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testIsPrioritized_withDifferentPriorities() { + assertFalse(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)))); + assertFalse(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)))); + + assertArrayEquals(new int[] {-1, -1, -1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {-1, -1, -1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 10), + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {-1, -1, -1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + assertArrayEquals(new int[] {-1, -1, -1, -1, -1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 20), + createResolveInfo(PACKAGE2, getAppId(2), 20), + createResolveInfo(PACKAGE3, getAppId(3), 10), + createResolveInfo(PACKAGE3, getAppId(3), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testIsPrioritized_withDifferentPriorities_withFirstUidChangeIdDisabled() { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1))); + + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)))); + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)))); + + assertArrayEquals(new int[] {0, 1, 1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 10), + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 1, 1, 1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 20), + createResolveInfo(PACKAGE2, getAppId(2), 20), + createResolveInfo(PACKAGE3, getAppId(3), 10), + createResolveInfo(PACKAGE3, getAppId(3), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testIsPrioritized_withDifferentPriorities_withLastUidChangeIdDisabled() { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(3))); + + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)))); + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)))); + + assertArrayEquals(new int[] {0, 0, 2}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 2, 3, 3}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 20), + createResolveInfo(PACKAGE2, getAppId(2), 20), + createResolveInfo(PACKAGE3, getAppId(3), 10), + createResolveInfo(PACKAGE3, getAppId(3), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testIsPrioritized_withDifferentPriorities_withUidChangeIdDisabled() { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2))); + + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)))); + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)))); + + assertArrayEquals(new int[] {0, 1, 2}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 1, 0}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 2, 2, 2}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 20), + createResolveInfo(PACKAGE2, getAppId(2), 20), + createResolveInfo(PACKAGE3, getAppId(3), 10), + createResolveInfo(PACKAGE3, getAppId(3), 0), + createResolveInfo(PACKAGE3, getAppId(4), 0)), false, mPlatformCompat)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testIsPrioritized_withDifferentPriorities_withMultipleUidChangeIdDisabled() { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1))); + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2))); + + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)))); + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)))); + + assertArrayEquals(new int[] {0, 1, 2}, calculateBlockedUntilBeyondCount(List.of( createResolveInfo(PACKAGE1, getAppId(1), 10), createResolveInfo(PACKAGE2, getAppId(2), 0), - createResolveInfo(PACKAGE3, getAppId(3), -10)), false)); - assertArrayEquals(new int[] {0,0,2,3,3}, + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 1, 1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 2, 2, 2}, calculateBlockedUntilBeyondCount(List.of( createResolveInfo(PACKAGE1, getAppId(1), 20), createResolveInfo(PACKAGE2, getAppId(2), 20), createResolveInfo(PACKAGE3, getAppId(3), 10), createResolveInfo(PACKAGE3, getAppId(3), 0), - createResolveInfo(PACKAGE3, getAppId(3), 0)), false)); + createResolveInfo(PACKAGE3, getAppId(4), 0)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 1, 1, 3}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 20), + createResolveInfo(PACKAGE2, getAppId(3), 20), + createResolveInfo(PACKAGE3, getAppId(3), 10), + createResolveInfo(PACKAGE3, getAppId(3), 0), + createResolveInfo(PACKAGE3, getAppId(2), 0)), false, mPlatformCompat)); } @Test @@ -602,6 +805,66 @@ public class BroadcastRecordTest { assertTrue(record3.matchesDeliveryGroup(record1)); } + @Test + public void testCalculateChangeStateForReceivers() { + assertArrayEquals(new boolean[] {true, true, true}, calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE3, getAppId(3))))); + assertArrayEquals(new boolean[] {true, true, true, true}, calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE3, getAppId(3))))); + + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1))); + assertArrayEquals(new boolean[] {false, true, true}, calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE3, getAppId(3))))); + assertArrayEquals(new boolean[] {false, true, false, true}, calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE2, getAppId(1)), + createResolveInfo(PACKAGE3, getAppId(3))))); + + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2))); + assertArrayEquals(new boolean[] {false, false, true}, calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE3, getAppId(3))))); + assertArrayEquals(new boolean[] {false, true, false, false, false, true}, + calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE3, getAppId(3)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE2, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE3, getAppId(3))))); + + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(3))); + assertArrayEquals(new boolean[] {false, false, false}, calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE3, getAppId(3))))); + assertArrayEquals(new boolean[] {false, false, false, false, false, false}, + calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE3, getAppId(3)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE2, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE3, getAppId(3))))); + } + + private boolean[] calculateChangeState(List<Object> receivers) { + return BroadcastRecord.calculateChangeStateForReceivers(receivers, + CHANGE_LIMIT_PRIORITY_SCOPE, mPlatformCompat); + } + private static void cleanupDisabledPackageReceivers(BroadcastRecord record, String packageName, int userId) { record.cleanupDisabledPackageReceiversLocked(packageName, null /* filterByClasses */, @@ -753,16 +1016,17 @@ public class BroadcastRecordTest { BackgroundStartPrivileges.NONE, false /* timeoutExempt */, filterExtrasForReceiver, - PROCESS_STATE_UNKNOWN); + PROCESS_STATE_UNKNOWN, + mPlatformCompat); } private static int getAppId(int i) { return Process.FIRST_APPLICATION_UID + i; } - private static boolean isPrioritized(List<Object> receivers) { + private boolean isPrioritized(List<Object> receivers) { return BroadcastRecord.isPrioritized( - calculateBlockedUntilBeyondCount(receivers, false), false); + calculateBlockedUntilBeyondCount(receivers, false, mPlatformCompat), false); } private static void assertBlocked(BroadcastRecord r, boolean... blocked) { diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/InstallDependencyHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/InstallDependencyHelperTest.java new file mode 100644 index 000000000000..f6c644e3d4d4 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/pm/InstallDependencyHelperTest.java @@ -0,0 +1,192 @@ +/* + * 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.pm; + +import static android.content.pm.Flags.FLAG_SDK_DEPENDENCY_INSTALLER; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.pm.SharedLibraryInfo; +import android.content.pm.parsing.ApkLite; +import android.content.pm.parsing.ApkLiteParseUtils; +import android.content.pm.parsing.PackageLite; +import android.content.pm.parsing.result.ParseResult; +import android.content.pm.parsing.result.ParseTypeImpl; +import android.os.FileUtils; +import android.os.OutcomeReceiver; +import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; + +import androidx.annotation.NonNull; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@Presubmit +@RunWith(JUnit4.class) +@RequiresFlagsEnabled(FLAG_SDK_DEPENDENCY_INSTALLER) +public class InstallDependencyHelperTest { + + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule public final CheckFlagsRule checkFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + + private static final String PUSH_FILE_DIR = "/data/local/tmp/tests/smockingservicestest/pm/"; + private static final String TEST_APP_USING_SDK1_AND_SDK2 = "HelloWorldUsingSdk1And2.apk"; + + @Mock private SharedLibrariesImpl mSharedLibraries; + private InstallDependencyHelper mInstallDependencyHelper; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mInstallDependencyHelper = new InstallDependencyHelper(mSharedLibraries); + } + + @Test + public void testResolveLibraryDependenciesIfNeeded_errorInSharedLibrariesImpl() + throws Exception { + doThrow(new PackageManagerException(new Exception("xyz"))) + .when(mSharedLibraries).collectMissingSharedLibraryInfos(any()); + + PackageLite pkg = getPackageLite(TEST_APP_USING_SDK1_AND_SDK2); + CallbackHelper callback = new CallbackHelper(/*expectSuccess=*/ false); + mInstallDependencyHelper.resolveLibraryDependenciesIfNeeded(pkg, callback); + callback.assertFailure(); + + assertThat(callback.error).hasMessageThat().contains("xyz"); + } + + @Test + public void testResolveLibraryDependenciesIfNeeded_failsToBind() throws Exception { + // Return a non-empty list as missing dependency + PackageLite pkg = getPackageLite(TEST_APP_USING_SDK1_AND_SDK2); + List<SharedLibraryInfo> missingDependency = Collections.singletonList( + mock(SharedLibraryInfo.class)); + when(mSharedLibraries.collectMissingSharedLibraryInfos(eq(pkg))) + .thenReturn(missingDependency); + + CallbackHelper callback = new CallbackHelper(/*expectSuccess=*/ false); + mInstallDependencyHelper.resolveLibraryDependenciesIfNeeded(pkg, callback); + callback.assertFailure(); + + assertThat(callback.error).hasMessageThat().contains( + "Failed to bind to Dependency Installer"); + } + + + @Test + public void testResolveLibraryDependenciesIfNeeded_allDependenciesInstalled() throws Exception { + // Return an empty list as missing dependency + PackageLite pkg = getPackageLite(TEST_APP_USING_SDK1_AND_SDK2); + List<SharedLibraryInfo> missingDependency = Collections.emptyList(); + when(mSharedLibraries.collectMissingSharedLibraryInfos(eq(pkg))) + .thenReturn(missingDependency); + + CallbackHelper callback = new CallbackHelper(/*expectSuccess=*/ true); + mInstallDependencyHelper.resolveLibraryDependenciesIfNeeded(pkg, callback); + callback.assertSuccess(); + } + + private static class CallbackHelper implements OutcomeReceiver<Void, PackageManagerException> { + public PackageManagerException error; + + private final CountDownLatch mWait = new CountDownLatch(1); + private final boolean mExpectSuccess; + + CallbackHelper(boolean expectSuccess) { + mExpectSuccess = expectSuccess; + } + + @Override + public void onResult(Void result) { + if (!mExpectSuccess) { + fail("Expected to fail"); + } + mWait.countDown(); + } + + @Override + public void onError(@NonNull PackageManagerException e) { + if (mExpectSuccess) { + fail("Expected success but received: " + e); + } + error = e; + mWait.countDown(); + } + + void assertSuccess() throws Exception { + assertThat(mWait.await(1000, TimeUnit.MILLISECONDS)).isTrue(); + assertThat(error).isNull(); + } + + void assertFailure() throws Exception { + assertThat(mWait.await(1000, TimeUnit.MILLISECONDS)).isTrue(); + assertThat(error).isNotNull(); + } + + } + + private PackageLite getPackageLite(String apkFileName) throws Exception { + File apkFile = copyApkToTmpDir(TEST_APP_USING_SDK1_AND_SDK2); + ParseResult<ApkLite> result = ApkLiteParseUtils.parseApkLite( + ParseTypeImpl.forDefaultParsing().reset(), apkFile, 0); + assertThat(result.isError()).isFalse(); + ApkLite baseApk = result.getResult(); + + return new PackageLite(/*path=*/ null, baseApk.getPath(), baseApk, + /*splitNames=*/ null, /*isFeatureSplits=*/ null, /*usesSplitNames=*/ null, + /*configForSplit=*/ null, /*splitApkPaths=*/ null, + /*splitRevisionCodes=*/ null, baseApk.getTargetSdkVersion(), + /*requiredSplitTypes=*/ null, /*splitTypes=*/ null); + } + + private File copyApkToTmpDir(String apkFileName) throws Exception { + File outFile = temporaryFolder.newFile(apkFileName); + String apkFilePath = PUSH_FILE_DIR + apkFileName; + File apkFile = new File(apkFilePath); + assertThat(apkFile.exists()).isTrue(); + try (InputStream is = new FileInputStream(apkFile)) { + FileUtils.copyToFileOrThrow(is, outFile); + } + return outFile; + } + +} diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java index 591e8df1725b..71c60ad02794 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java @@ -742,10 +742,11 @@ public class StagingManagerTest { /* stagedSessionErrorMessage */ "no error", /* preVerifiedDomains */ null, /* verifierController */ null, - /* initialVerificationPolicy */ + /* initialVerificationPolicy */ PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED, /* currentVerificationPolicy */ - PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED); + PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED, + /* installDependencyHelper */ null); StagingManager.StagedSession stagedSession = spy(session.mStagedSession); doReturn(packageName).when(stagedSession).getPackageName(); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 2edde9b74d0a..d5b930769e43 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -33,6 +33,7 @@ import static com.android.internal.accessibility.AccessibilityShortcutController import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity.EXTRA_TYPE_TO_CHOOSE; @@ -80,6 +81,7 @@ import android.content.pm.ServiceInfo; import android.content.res.XmlResourceParser; import android.graphics.drawable.Icon; import android.hardware.display.DisplayManagerGlobal; +import android.hardware.input.KeyGestureEvent; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -2183,6 +2185,168 @@ public class AccessibilityManagerServiceTest { verify(mockUserContext).getSystemService(EnhancedConfirmationManager.class); } + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_toggleMagnifier() { + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).containsExactly(MAGNIFICATION_CONTROLLER_NAME); + + // The magnifier will only be toggled on the second event received since the first is + // used to toggle the feature on. + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + verify(mInputFilter).notifyMagnificationShortcutTriggered(anyInt()); + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_activateSelectToSpeak_trustedService() { + setupAccessibilityServiceConnection(FLAG_REQUEST_ACCESSIBILITY_BUTTON); + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + + final AccessibilityServiceInfo trustedService = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), + /* isSystemApp= */ true, /* isAlwaysOnService= */ true); + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.add(trustedService); + mTestableContext.getOrCreateTestableResources().addOverride( + R.string.config_defaultSelectToSpeakService, + trustedService.getComponentName().flattenToString()); + mTestableContext.getOrCreateTestableResources().addOverride( + R.array.config_trustedAccessibilityServices, + new String[]{trustedService.getComponentName().flattenToString()}); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).containsExactly( + trustedService.getComponentName().flattenToString()); + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_activateSelectToSpeak_preinstalledService() { + setupAccessibilityServiceConnection(FLAG_REQUEST_ACCESSIBILITY_BUTTON); + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + + final AccessibilityServiceInfo untrustedService = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), + /* isSystemApp= */ true, /* isAlwaysOnService= */ true); + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.add(untrustedService); + mTestableContext.getOrCreateTestableResources().addOverride( + R.string.config_defaultSelectToSpeakService, + untrustedService.getComponentName().flattenToString()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_activateSelectToSpeak_downloadedService() { + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + + final AccessibilityServiceInfo downloadedService = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), + /* isSystemApp= */ false, /* isAlwaysOnService= */ true); + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.add(downloadedService); + mTestableContext.getOrCreateTestableResources().addOverride( + R.string.config_defaultSelectToSpeakService, + downloadedService.getComponentName().flattenToString()); + mTestableContext.getOrCreateTestableResources().addOverride( + R.array.config_trustedAccessibilityServices, + new String[]{downloadedService.getComponentName().flattenToString()}); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_activateSelectToSpeak_defaultNotInstalled() { + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + + final AccessibilityServiceInfo installedService = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), + /* isSystemApp= */ true, /* isAlwaysOnService= */ true); + final AccessibilityServiceInfo defaultService = mockAccessibilityServiceInfo( + new ComponentName("package_b", "class_b"), + /* isSystemApp= */ true, /* isAlwaysOnService= */ true); + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.add(installedService); + mTestableContext.getOrCreateTestableResources().addOverride( + R.string.config_defaultSelectToSpeakService, + defaultService.getComponentName().flattenToString()); + mTestableContext.getOrCreateTestableResources().addOverride( + R.array.config_trustedAccessibilityServices, + new String[]{defaultService.getComponentName().flattenToString()}); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_activateSelectToSpeak_noDefault() { + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + + final AccessibilityServiceInfo installedService = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), + /* isSystemApp= */ true, /* isAlwaysOnService= */ true); + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.add(installedService); + mTestableContext.getOrCreateTestableResources().addOverride( + R.array.config_trustedAccessibilityServices, + new String[]{installedService.getComponentName().flattenToString()}); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + } private Set<String> readStringsFromSetting(String setting) { final Set<String> result = new ArraySet<>(); @@ -2298,6 +2462,10 @@ public class AccessibilityManagerServiceTest { AccessibilityManagerService service) { super(context, service); } + + @Override + void notifyMagnificationShortcutTriggered(int displayId) { + } } private static class A11yTestableContext extends TestableContext { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java index 8c35925debff..cb52eef6adfe 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java @@ -32,6 +32,7 @@ import static com.android.internal.accessibility.AccessibilityShortcutController import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.ALL; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP; @@ -174,6 +175,7 @@ public class AccessibilityUserStateTest { mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), SOFTWARE); mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), GESTURE); mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), QUICK_SETTINGS); + mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), KEY_GESTURE); mUserState.updateA11yTilesInQsPanelLocked( Set.of(AccessibilityShortcutController.COLOR_INVERSION_TILE_COMPONENT_NAME)); mUserState.setTargetAssignedToAccessibilityButton(componentNameString); @@ -201,6 +203,7 @@ public class AccessibilityUserStateTest { assertTrue(mUserState.getShortcutTargetsLocked(SOFTWARE).isEmpty()); assertTrue(mUserState.getShortcutTargetsLocked(GESTURE).isEmpty()); assertTrue(mUserState.getShortcutTargetsLocked(QUICK_SETTINGS).isEmpty()); + assertTrue(mUserState.getShortcutTargetsLocked(KEY_GESTURE).isEmpty()); assertTrue(mUserState.getA11yQsTilesInQsPanel().isEmpty()); assertNull(mUserState.getTargetAssignedToAccessibilityButton()); assertFalse(mUserState.isTouchExplorationEnabledLocked()); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java index 411a6102f45a..361df94e8a90 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java @@ -44,7 +44,7 @@ import android.widget.RemoteViews; import androidx.annotation.NonNull; import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags; import com.android.server.UiServiceTestCase; @@ -89,7 +89,7 @@ import java.util.stream.Stream; import javax.annotation.Nullable; @RunWith(AndroidJUnit4.class) -@EnableFlags({Flags.FLAG_VISIT_PERSON_URI, Flags.FLAG_API_RICH_ONGOING}) +@EnableFlags({Flags.FLAG_API_RICH_ONGOING}) public class NotificationVisitUrisTest extends UiServiceTestCase { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java index 9967ccebeb1f..7dba1422d61d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java @@ -21,7 +21,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static org.junit.Assert.assertEquals; @@ -165,31 +164,6 @@ public class SurfaceAnimatorTest extends WindowTestsBase { } @Test - public void testDelayingAnimationStart() { - mAnimatable.mSurfaceAnimator.startDelayingAnimationStart(); - mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */, - ANIMATION_TYPE_APP_TRANSITION); - verifyZeroInteractions(mSpec); - assertAnimating(mAnimatable); - assertTrue(mAnimatable.mSurfaceAnimator.isAnimationStartDelayed()); - mAnimatable.mSurfaceAnimator.endDelayingAnimationStart(); - verify(mSpec).startAnimation(any(), any(), eq(ANIMATION_TYPE_APP_TRANSITION), any()); - } - - @Test - public void testDelayingAnimationStartAndCancelled() { - mAnimatable.mSurfaceAnimator.startDelayingAnimationStart(); - mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */, - ANIMATION_TYPE_APP_TRANSITION); - mAnimatable.mSurfaceAnimator.cancelAnimation(); - verifyZeroInteractions(mSpec); - assertNotAnimating(mAnimatable); - assertTrue(mAnimatable.mFinishedCallbackCalled); - assertEquals(ANIMATION_TYPE_APP_TRANSITION, mAnimatable.mFinishedAnimationType); - verify(mTransaction).remove(eq(mAnimatable.mLeash)); - } - - @Test public void testTransferAnimation() { mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */, ANIMATION_TYPE_APP_TRANSITION); diff --git a/telephony/java/android/telephony/satellite/EarfcnRange.aidl b/telephony/java/android/telephony/satellite/EarfcnRange.aidl new file mode 100644 index 000000000000..0b224d0b09bd --- /dev/null +++ b/telephony/java/android/telephony/satellite/EarfcnRange.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.telephony.satellite; + +parcelable EarfcnRange; diff --git a/telephony/java/android/telephony/satellite/EarfcnRange.java b/telephony/java/android/telephony/satellite/EarfcnRange.java new file mode 100644 index 000000000000..38043b570c2f --- /dev/null +++ b/telephony/java/android/telephony/satellite/EarfcnRange.java @@ -0,0 +1,124 @@ +/* + * 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.telephony.satellite; + +import android.annotation.FlaggedApi; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.telephony.flags.Flags; + +/** + * EARFCN (E-UTRA Absolute Radio Frequency Channel Number): A number that identifies a + * specific frequency channel in LTE/5G NR, used to define the carrier frequency. + * The range can be [0 ~ 65535] according to the 3GPP TS 36.101 + * + * In satellite communication: + * - Efficient frequency allocation across a wide coverage area. + * - Handles Doppler shift due to satellite movement. + * - Manages interference with terrestrial networks. + * + * See 3GPP TS 36.101 and 38.101-1 for details. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) +public final class EarfcnRange implements Parcelable { + + /** + * The start frequency of the earfcn range and is inclusive in the range + */ + private int mStartEarfcn; + + /** + * The end frequency of the earfcn range and is inclusive in the range. + */ + private int mEndEarfcn; + + private EarfcnRange(@NonNull Parcel in) { + readFromParcel(in); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mStartEarfcn); + dest.writeInt(mEndEarfcn); + } + + private void readFromParcel(Parcel in) { + mStartEarfcn = in.readInt(); + mEndEarfcn = in.readInt(); + } + + /** + * Constructor for the EarfcnRange class. + * The range can be [0 ~ 65535] according to the 3GPP TS 36.101 + * + * @param startEarfcn The starting earfcn value. + * @param endEarfcn The ending earfcn value. + */ + public EarfcnRange(@IntRange(from = 0, to = 65535) int endEarfcn, + @IntRange(from = 0, to = 65535) int startEarfcn) { + mEndEarfcn = endEarfcn; + mStartEarfcn = startEarfcn; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "startEarfcn: " + mStartEarfcn + ", " + "endEarfcn: " + mEndEarfcn; + } + + @NonNull + public static final Creator<EarfcnRange> CREATOR = new Creator<EarfcnRange>() { + @Override + public EarfcnRange createFromParcel(Parcel in) { + return new EarfcnRange(in); + } + + @Override + public EarfcnRange[] newArray(int size) { + return new EarfcnRange[size]; + } + }; + + /** + * Returns the starting earfcn value for this range. + * It can be [0 ~ 65535] according to the 3GPP TS 36.101 + * + * @return The starting earfcn. + */ + public @IntRange(from = 0, to = 65535) int getStartEarfcn() { + return mStartEarfcn; + } + + /** + * Returns the ending earfcn value for this range. + * It can be [0 ~ 65535] according to the 3GPP TS 36.101 + * + * @return The ending earfcn. + */ + public @IntRange(from = 0, to = 65535) int getEndEarfcn() { + return mEndEarfcn; + } +} diff --git a/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl index a7eda482cb76..2730f90c4e5e 100644 --- a/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl +++ b/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl @@ -16,6 +16,8 @@ package android.telephony.satellite; +import android.telephony.satellite.SatelliteAccessConfiguration; + /** * Interface for satellite communication allowed state callback. * @hide @@ -29,4 +31,14 @@ oneway interface ISatelliteCommunicationAllowedStateCallback { * @param allowed whether satellite communication state or not */ void onSatelliteCommunicationAllowedStateChanged(in boolean isAllowed); + + /** + * Callback method invoked when the satellite access configuration changes + * + * @param The satellite access configuration associated with the current location. + * When satellite is not allowed at the current location, + * {@code satelliteRegionalConfiguration} will be null. + */ + void onSatelliteAccessConfigurationChanged(in SatelliteAccessConfiguration + satelliteAccessConfiguration); } diff --git a/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.aidl b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.aidl new file mode 100644 index 000000000000..0214193a654f --- /dev/null +++ b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package android.telephony.satellite; + + parcelable SatelliteAccessConfiguration;
\ No newline at end of file diff --git a/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.java b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.java new file mode 100644 index 000000000000..c3ae70b48854 --- /dev/null +++ b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.java @@ -0,0 +1,122 @@ +/* + * 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.telephony.satellite; + +import android.annotation.FlaggedApi; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import com.android.internal.telephony.flags.Flags; + +import java.util.List; + +/** + * SatelliteAccessConfiguration is used to store satellite access configuration + * that will be applied to the satellite communication at the corresponding region. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) +public final class SatelliteAccessConfiguration implements Parcelable { + /** + * The list of satellites available at the current location. + */ + @NonNull + private List<SatelliteInfo> mSatelliteInfoList; + + /** + * The list of tag IDs associated with the current location + */ + @NonNull + private int[] mTagIds; + + /** + * Constructor for {@link SatelliteAccessConfiguration}. + * + * @param satelliteInfos The list of {@link SatelliteInfo} objects representing the satellites + * accessible with this configuration. + * @param tagIds The list of tag IDs associated with this configuration. + */ + public SatelliteAccessConfiguration(@NonNull List<SatelliteInfo> satelliteInfos, + @NonNull int[] tagIds) { + mSatelliteInfoList = satelliteInfos; + mTagIds = tagIds; + } + + public SatelliteAccessConfiguration(Parcel in) { + mSatelliteInfoList = in.createTypedArrayList(SatelliteInfo.CREATOR); + mTagIds = new int[in.readInt()]; + in.readIntArray(mTagIds); + } + + public static final Creator<SatelliteAccessConfiguration> CREATOR = + new Creator<SatelliteAccessConfiguration>() { + @Override + public SatelliteAccessConfiguration createFromParcel(Parcel in) { + return new SatelliteAccessConfiguration(in); + } + + @Override + public SatelliteAccessConfiguration[] newArray(int size) { + return new SatelliteAccessConfiguration[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + /** + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. + * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}. + */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeTypedList(mSatelliteInfoList); + if (mTagIds != null && mTagIds.length > 0) { + dest.writeInt(mTagIds.length); + dest.writeIntArray(mTagIds); + } else { + dest.writeInt(0); + } + } + + /** + * Returns a list of {@link SatelliteInfo} objects representing the satellites + * associated with this object. + * + * @return The list of {@link SatelliteInfo} objects. + */ + @NonNull + public List<SatelliteInfo> getSatelliteInfos() { + return mSatelliteInfoList; + } + + /** + * Returns a list of tag IDs associated with this object. + * + * @return The list of tag IDs. + */ + @NonNull + public int[] getTagIds() { + return mTagIds; + } +} diff --git a/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java index 1a870202d096..bffb11f23d56 100644 --- a/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java +++ b/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java @@ -17,6 +17,7 @@ package android.telephony.satellite; import android.annotation.FlaggedApi; +import android.annotation.Nullable; import com.android.internal.telephony.flags.Flags; @@ -40,4 +41,17 @@ public interface SatelliteCommunicationAllowedStateCallback { */ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) void onSatelliteCommunicationAllowedStateChanged(boolean isAllowed); + + /** + * Callback method invoked when the satellite access configuration changes + * + * @param satelliteAccessConfiguration The satellite access configuration associated with + * the current location. When satellite is not allowed at + * the current location, + * {@code satelliteRegionalConfiguration} will be null. + * @hide + */ + @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) + default void onSatelliteAccessConfigurationChanged( + @Nullable SatelliteAccessConfiguration satelliteAccessConfiguration) {}; } diff --git a/telephony/java/android/telephony/satellite/SatelliteInfo.aidl b/telephony/java/android/telephony/satellite/SatelliteInfo.aidl new file mode 100644 index 000000000000..fc2303b080a5 --- /dev/null +++ b/telephony/java/android/telephony/satellite/SatelliteInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package android.telephony.satellite; + + parcelable SatelliteInfo;
\ No newline at end of file diff --git a/telephony/java/android/telephony/satellite/SatelliteInfo.java b/telephony/java/android/telephony/satellite/SatelliteInfo.java new file mode 100644 index 000000000000..bca907e49993 --- /dev/null +++ b/telephony/java/android/telephony/satellite/SatelliteInfo.java @@ -0,0 +1,169 @@ +/* + * 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.telephony.satellite; + +import android.annotation.FlaggedApi; +import android.os.Parcel; +import android.os.ParcelUuid; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import com.android.internal.telephony.flags.Flags; + +import java.util.List; +import java.util.UUID; + +/** + * SatelliteInfo stores a satellite's identification, position, and frequency information + * facilitating efficient satellite communications. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) +public class SatelliteInfo implements Parcelable { + /** + * Unique identification number for the satellite. + * This ID is used to distinguish between different satellites in the network. + */ + @NonNull + private UUID mId; + + /** + * Position information of a satellite. + * This includes the longitude and altitude of the satellite. + */ + private SatellitePosition mPosition; + + /** + * The frequency bands to scan. Bands and earfcns won't overlap. + * Bands will be filled only if the whole band is needed. + * Maximum length of the vector is 8. + */ + private int[] mBands; + + /** + * EARFCN (E-UTRA Absolute Radio Frequency Channel Number) Ranges + * The supported frequency range list. + * Maximum length of the vector is 8. + */ + private final List<EarfcnRange> mEarfcnRangeList; + + protected SatelliteInfo(Parcel in) { + ParcelUuid parcelUuid = in.readParcelable( + ParcelUuid.class.getClassLoader(), ParcelUuid.class); + if (parcelUuid != null) { + mId = parcelUuid.getUuid(); + } + mPosition = in.readParcelable(SatellitePosition.class.getClassLoader(), + SatellitePosition.class); + int numBands = in.readInt(); + mBands = new int[numBands]; + if (numBands > 0) { + for (int i = 0; i < numBands; i++) { + mBands[i] = in.readInt(); + } + } + mEarfcnRangeList = in.createTypedArrayList(EarfcnRange.CREATOR); + } + + /** + * Constructor for {@link SatelliteInfo}. + * + * @param satelliteId The ID of the satellite. + * @param satellitePosition The {@link SatellitePosition} of the satellite. + * @param bands The list of frequency bands supported by the satellite. + * @param earfcnRanges The list of {@link EarfcnRange} objects representing the EARFCN + * ranges supported by the satellite. + */ + public SatelliteInfo(@NonNull UUID satelliteId, @NonNull SatellitePosition satellitePosition, + @NonNull int[] bands, @NonNull List<EarfcnRange> earfcnRanges) { + mId = satelliteId; + mPosition = satellitePosition; + mBands = bands; + mEarfcnRangeList = earfcnRanges; + } + + public static final Creator<SatelliteInfo> CREATOR = new Creator<SatelliteInfo>() { + @Override + public SatelliteInfo createFromParcel(Parcel in) { + return new SatelliteInfo(in); + } + + @Override + public SatelliteInfo[] newArray(int size) { + return new SatelliteInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(new ParcelUuid(mId), flags); + dest.writeParcelable(mPosition, flags); + if (mBands != null && mBands.length > 0) { + dest.writeInt(mBands.length); + dest.writeIntArray(mBands); + } else { + dest.writeInt(0); + } + dest.writeTypedList(mEarfcnRangeList); + } + + /** + * Returns the ID of the satellite. + * + * @return The satellite ID. + */ + @NonNull + public UUID getSatelliteId() { + return mId; + } + + /** + * Returns the position of the satellite. + * + * @return The {@link SatellitePosition} of the satellite. + */ + public SatellitePosition getSatellitePosition() { + return mPosition; + } + + /** + * Returns the list of frequency bands supported by the satellite. + * + * @return The list of frequency bands. + */ + @NonNull + public int[] getBands() { + return mBands; + } + + /** + * Returns the list of EARFCN ranges supported by the satellite. + * + * @return The list of {@link EarfcnRange} objects. + */ + @NonNull + public List<EarfcnRange> getEarfcnRanges() { + return mEarfcnRangeList; + } +} diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 88dddcfc9180..7e3d99a5c4ac 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -36,6 +36,7 @@ import android.os.ICancellationSignal; import android.os.OutcomeReceiver; import android.os.RemoteException; import android.os.ResultReceiver; +import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionManager; import android.telephony.TelephonyCallback; import android.telephony.TelephonyFrameworkInitializer; @@ -272,6 +273,14 @@ public final class SatelliteManager { public static final String KEY_DEPROVISION_SATELLITE_TOKENS = "deprovision_satellite"; /** + * Bundle key to get the response from + * {@link #requestSatelliteAccessConfigurationForCurrentLocation(Executor, OutcomeReceiver)}. + * @hide + */ + public static final String KEY_SATELLITE_ACCESS_CONFIGURATION = + "satellite_access_configuration"; + + /** * The request was successfully processed. * @hide */ @@ -2332,6 +2341,68 @@ public final class SatelliteManager { } /** + * Request to get satellite access configuration for the current location. + * + * @param executor The executor on which the callback will be called. + * @param callback The callback object to which the result will be delivered. + * If the request is successful, {@link OutcomeReceiver#onResult(Object)} + * will return a {@code SatelliteAccessConfiguration} with value the regional + * satellite access configuration at the current location. + * If the request is not successful, {@link OutcomeReceiver#onError(Throwable)} + * will return a {@link SatelliteException} with the {@link SatelliteResult}. + * + * @throws SecurityException if the caller doesn't have required permission. + * + * @hide + */ + @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) + @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) + public void requestSatelliteAccessConfigurationForCurrentLocation( + @NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver<SatelliteAccessConfiguration, SatelliteException> callback) { + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + + try { + ITelephony telephony = getITelephony(); + if (telephony != null) { + ResultReceiver receiver = new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (resultCode == SATELLITE_RESULT_SUCCESS) { + if (resultData.containsKey(KEY_SATELLITE_ACCESS_CONFIGURATION)) { + SatelliteAccessConfiguration satelliteAccessConfiguration = + resultData.getParcelable(KEY_SATELLITE_ACCESS_CONFIGURATION, + SatelliteAccessConfiguration.class); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> + callback.onResult(satelliteAccessConfiguration))); + } else { + loge("KEY_SATELLITE_ACCESS_CONFIGURATION does not exist."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> + callback.onError(new SatelliteException( + SATELLITE_RESULT_REQUEST_FAILED)))); + } + } else { + executor.execute(() -> Binder.withCleanCallingIdentity(() -> + callback.onError(new SatelliteException(resultCode)))); + } + } + }; + telephony.requestSatelliteAccessConfigurationForCurrentLocation(receiver); + } else { + loge("requestSatelliteAccessConfigurationForCurrentLocation() invalid telephony"); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); + } + } catch (RemoteException ex) { + loge("requestSatelliteAccessConfigurationForCurrentLocation() RemoteException: " + + ex); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); + } + } + + /** * Request to get the duration in seconds after which the satellite will be visible. * This will be {@link Duration#ZERO} if the satellite is currently visible. * @@ -2436,7 +2507,7 @@ public final class SatelliteManager { * <li>There is no satellite communication restriction, which is added by * {@link #addAttachRestrictionForCarrier(int, int, Executor, Consumer)}</li> * <li>The carrier config {@link - * android.telephony.CarrierConfigManager#KEY_SATELLITE_ATTACH_SUPPORTED_BOOL} is set to + * CarrierConfigManager#KEY_SATELLITE_ATTACH_SUPPORTED_BOOL} is set to * {@code true}.</li> * </ul> * @@ -2759,7 +2830,7 @@ public final class SatelliteManager { * <p> * Note: This API is specifically designed for OEM enabled satellite connectivity only. * For satellite connectivity enabled using carrier roaming, please refer to - * {@link android.telephony.TelephonyCallback.SignalStrengthsListener}, and + * {@link TelephonyCallback.SignalStrengthsListener}, and * {@link TelephonyManager#registerTelephonyCallback(Executor, TelephonyCallback)}. * </p> * @@ -2830,7 +2901,7 @@ public final class SatelliteManager { * <p> * Note: This API is specifically designed for OEM enabled satellite connectivity only. * For satellite connectivity enabled using carrier roaming, please refer to - * {@link android.telephony.TelephonyCallback.SignalStrengthsListener}, and + * {@link TelephonyCallback.SignalStrengthsListener}, and * {@link TelephonyManager#registerTelephonyCallback(Executor, TelephonyCallback)}. * </p> * @@ -3147,6 +3218,15 @@ public final class SatelliteManager { () -> callback.onSatelliteCommunicationAllowedStateChanged( isAllowed))); } + + @Override + public void onSatelliteAccessConfigurationChanged( + @Nullable SatelliteAccessConfiguration + satelliteAccessConfiguration) { + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> callback.onSatelliteAccessConfigurationChanged( + satelliteAccessConfiguration))); + } }; sSatelliteCommunicationAllowedStateCallbackMap.put(callback, internalCallback); return telephony.registerForCommunicationAllowedStateChanged( diff --git a/telephony/java/android/telephony/satellite/SatellitePosition.aidl b/telephony/java/android/telephony/satellite/SatellitePosition.aidl new file mode 100644 index 000000000000..a8028eb48ee7 --- /dev/null +++ b/telephony/java/android/telephony/satellite/SatellitePosition.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package android.telephony.satellite; + + parcelable SatellitePosition;
\ No newline at end of file diff --git a/telephony/java/android/telephony/satellite/SatellitePosition.java b/telephony/java/android/telephony/satellite/SatellitePosition.java new file mode 100644 index 000000000000..1e8c0180f456 --- /dev/null +++ b/telephony/java/android/telephony/satellite/SatellitePosition.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.telephony.satellite; + +import android.annotation.FlaggedApi; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import com.android.internal.telephony.flags.Flags; + +/** + * The position of a satellite in Earth orbit. + * + * Longitude is the angular distance, measured in degrees, east or west of the prime longitude line + * ranging from -180 to 180 degrees + * Altitude is the distance from the center of the Earth to the satellite, measured in kilometers + * + * @hide + */ +@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) +public class SatellitePosition implements Parcelable { + + /** + * The longitude of the satellite in degrees, ranging from -180 to 180 degrees + */ + private double mLongitudeDegree; + + /** + * The distance from the center of the earth to the satellite, measured in kilometers + */ + private double mAltitudeKm; + + /** + * Constructor for {@link SatellitePosition} used to create an instance from a {@link Parcel}. + * + * @param in The {@link Parcel} to read the satellite position data from. + */ + public SatellitePosition(Parcel in) { + mLongitudeDegree = in.readDouble(); + mAltitudeKm = in.readDouble(); + } + + /** + * Constructor for {@link SatellitePosition}. + * + * @param longitudeDegree The longitude of the satellite in degrees. + * @param altitudeKm The altitude of the satellite in kilometers. + */ + public SatellitePosition(double longitudeDegree, double altitudeKm) { + mLongitudeDegree = longitudeDegree; + mAltitudeKm = altitudeKm; + } + + public static final Creator<SatellitePosition> CREATOR = new Creator<SatellitePosition>() { + @Override + public SatellitePosition createFromParcel(Parcel in) { + return new SatellitePosition(in); + } + + @Override + public SatellitePosition[] newArray(int size) { + return new SatellitePosition[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + /** + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. + * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}. + */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeDouble(mLongitudeDegree); + dest.writeDouble(mAltitudeKm); + } + + /** + * Returns the longitude of the satellite in degrees, ranging from -180 to 180 degrees. + * + * @return The longitude of the satellite. + */ + public double getLongitudeDegrees() { + return mLongitudeDegree; + } + + /** + * Returns the altitude of the satellite in kilometers + * + * @return The altitude of the satellite. + */ + public double getAltitudeKm() { + return mAltitudeKm; + } +} diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 544bfabcf20b..210200be4cf3 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -2999,6 +2999,16 @@ interface ITelephony { void requestIsCommunicationAllowedForCurrentLocation(int subId, in ResultReceiver receiver); /** + * Request to get satellite access configuration for the current location. + * + * @param receiver Result receiver to get the error code of the request + * and satellite access configuration for the current location. + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + + "android.Manifest.permission.SATELLITE_COMMUNICATION)") + void requestSatelliteAccessConfigurationForCurrentLocation(in ResultReceiver receiver); + + /** * Request to get the time after which the satellite will be visible. * * @param receiver Result receiver to get the error code of the request and the requested diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt index 9a9a331a3753..ea61ad9d4481 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt @@ -28,6 +28,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import android.tools.traces.wm.WindowingMode import android.view.WindowInsets import android.view.WindowManager +import android.window.DesktopModeFlags import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.uiautomator.By import androidx.test.uiautomator.BySelector @@ -35,7 +36,6 @@ import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.Until import com.android.server.wm.flicker.helpers.MotionEventHelper.InputMethod.TOUCH -import com.android.window.flags.Flags import java.time.Duration /** @@ -107,7 +107,7 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : // drag the window to move to desktop if (motionEventHelper.inputMethod == TOUCH - && Flags.enableHoldToDragAppHandle()) { + && DesktopModeFlags.ENABLE_HOLD_TO_DRAG_APP_HANDLE.isTrue) { // Touch requires hold-to-drag. motionEventHelper.holdToDrag(startX, startY, startX, endY, steps = 100) } else { diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 61400edba165..09a686ca2c3f 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -782,6 +782,30 @@ class KeyGestureControllerTests { KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), + TestData( + "META + ALT + M -> Toggle Magnification", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_M + ), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION, + intArrayOf(KeyEvent.KEYCODE_M), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + ALT + S -> Activate Select to Speak", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_S + ), + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK, + intArrayOf(KeyEvent.KEYCODE_S), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), ) } diff --git a/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java b/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java index e2099e652c49..635e5de935c7 100644 --- a/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java +++ b/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java @@ -18,19 +18,27 @@ package com.android.server.usblib; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.hardware.usb.UsbAccessory; import android.hardware.usb.UsbManager; +import android.hardware.usb.flags.Flags; import android.os.Binder; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Log; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.concurrent.atomic.AtomicInteger; /** @@ -43,13 +51,36 @@ public class UsbManagerTestLib { private UsbManager mUsbManagerSys; private UsbManager mUsbManagerMock; - @Mock private android.hardware.usb.IUsbManager mMockUsbService; + @Mock + private android.hardware.usb.IUsbManager mMockUsbService; + private TestParcelFileDescriptor mTestParcelFileDescriptor = new TestParcelFileDescriptor( + new ParcelFileDescriptor(new FileDescriptor())); + @Mock + private UsbAccessory mMockUsbAccessory; /** * Counter for tracking UsbOperation operations. */ private static final AtomicInteger sUsbOperationCount = new AtomicInteger(); + private class TestParcelFileDescriptor extends ParcelFileDescriptor { + + private final AtomicInteger mCloseCount = new AtomicInteger(); + + TestParcelFileDescriptor(ParcelFileDescriptor wrapped) { + super(wrapped); + } + + @Override + public void close() { + int unused = mCloseCount.incrementAndGet(); + } + + public void clearCloseCount() { + mCloseCount.set(0); + } + } + public UsbManagerTestLib(Context context) { MockitoAnnotations.initMocks(this); mContext = context; @@ -74,6 +105,34 @@ public class UsbManagerTestLib { mUsbManagerSys.setCurrentFunctions(functions); } + private InputStream openAccessoryInputStream(UsbAccessory accessory) { + try { + when(mMockUsbService.openAccessory(accessory)).thenReturn(mTestParcelFileDescriptor); + } catch (RemoteException remEx) { + Log.w(TAG, "RemoteException"); + } + + if (Flags.enableAccessoryStreamApi()) { + return mUsbManagerMock.openAccessoryInputStream(accessory); + } + + throw new UnsupportedOperationException("Stream APIs not available"); + } + + private OutputStream openAccessoryOutputStream(UsbAccessory accessory) { + try { + when(mMockUsbService.openAccessory(accessory)).thenReturn(mTestParcelFileDescriptor); + } catch (RemoteException remEx) { + Log.w(TAG, "RemoteException"); + } + + if (Flags.enableAccessoryStreamApi()) { + return mUsbManagerMock.openAccessoryOutputStream(accessory); + } + + throw new UnsupportedOperationException("Stream APIs not available"); + } + private void testSetGetCurrentFunctions_Matched(long functions) { setCurrentFunctions(functions); assertEquals("CurrentFunctions mismatched: ", functions, getCurrentFunctions()); @@ -94,7 +153,7 @@ public class UsbManagerTestLib { try { setCurrentFunctions(functions); - verify(mMockUsbService).setCurrentFunctions(eq(functions), operationId); + verify(mMockUsbService).setCurrentFunctions(eq(functions), eq(operationId)); } catch (RemoteException remEx) { Log.w(TAG, "RemoteException"); } @@ -118,7 +177,7 @@ public class UsbManagerTestLib { int operationId = sUsbOperationCount.incrementAndGet() + Binder.getCallingUid(); setCurrentFunctions(functions); - verify(mMockUsbService).setCurrentFunctions(eq(functions), operationId); + verify(mMockUsbService).setCurrentFunctions(eq(functions), eq(operationId)); } public void testGetCurrentFunctions_shouldMatched() { @@ -138,4 +197,47 @@ public class UsbManagerTestLib { testSetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_RNDIS); testSetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_NCM); } + + public void testParcelFileDescriptorClosedWhenAllOpenStreamsAreClosed() { + mTestParcelFileDescriptor.clearCloseCount(); + try { + try (InputStream ignored = openAccessoryInputStream(mMockUsbAccessory)) { + //noinspection EmptyTryBlock + try (OutputStream ignored2 = openAccessoryOutputStream(mMockUsbAccessory)) { + // do nothing + } + } + + // ParcelFileDescriptor is closed only once. + assertEquals(mTestParcelFileDescriptor.mCloseCount.get(), 1); + mTestParcelFileDescriptor.clearCloseCount(); + } catch (IOException e) { + // do nothing + } + } + + public void testOnlyOneOpenInputStreamAllowed() { + try { + //noinspection EmptyTryBlock + try (InputStream ignored = openAccessoryInputStream(mMockUsbAccessory)) { + assertThrows(IllegalStateException.class, + () -> openAccessoryInputStream(mMockUsbAccessory)); + } + } catch (IOException e) { + // do nothing + } + } + + public void testOnlyOneOpenOutputStreamAllowed() { + try { + //noinspection EmptyTryBlock + try (OutputStream ignored = openAccessoryOutputStream(mMockUsbAccessory)) { + assertThrows(IllegalStateException.class, + () -> openAccessoryOutputStream(mMockUsbAccessory)); + } + } catch (IOException e) { + // do nothing + } + } + } diff --git a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java index 8b21763b4a24..40fd0b431451 100644 --- a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java +++ b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java @@ -18,17 +18,21 @@ package com.android.server.usbtest; import android.content.Context; import android.hardware.usb.UsbManager; +import android.hardware.usb.flags.Flags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import org.junit.Ignore; +import com.android.server.usblib.UsbManagerTestLib; + +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import com.android.server.usblib.UsbManagerTestLib; - /** * Unit tests for {@link android.hardware.usb.UsbManager}. * Note: MUST claimed MANAGE_USB permission in Manifest @@ -41,6 +45,9 @@ public class UsbManagerApiTest { private final UsbManagerTestLib mUsbManagerTestLib = new UsbManagerTestLib(mContext = InstrumentationRegistry.getContext()); + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); /** * Verify NO SecurityException * Go through System Server @@ -92,4 +99,23 @@ public class UsbManagerApiTest { public void testUsbApi_SetCurrentFunctions_shouldMatched() { mUsbManagerTestLib.testSetCurrentFunctions_shouldMatched(); } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API) + public void testUsbApi_closesParcelFileDescriptorAfterAllStreamsClosed() { + mUsbManagerTestLib.testParcelFileDescriptorClosedWhenAllOpenStreamsAreClosed(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API) + public void testUsbApi_callingOpenAccessoryInputStreamTwiceThrowsException() { + mUsbManagerTestLib.testOnlyOneOpenInputStreamAllowed(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API) + public void testUsbApi_callingOpenAccessoryOutputStreamTwiceThrowsException() { + mUsbManagerTestLib.testOnlyOneOpenOutputStreamAllowed(); + } + } diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index ac96ef28f501..be5c84c0353c 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -53,7 +53,6 @@ public class TestableLooper { private static final Field MESSAGE_QUEUE_MESSAGES_FIELD; private static final Field MESSAGE_NEXT_FIELD; private static final Field MESSAGE_WHEN_FIELD; - private static Field MESSAGE_QUEUE_USE_CONCURRENT_FIELD = null; private Looper mLooper; private MessageQueue mQueue; @@ -64,14 +63,6 @@ public class TestableLooper { static { try { - MESSAGE_QUEUE_USE_CONCURRENT_FIELD = - MessageQueue.class.getDeclaredField("mUseConcurrent"); - MESSAGE_QUEUE_USE_CONCURRENT_FIELD.setAccessible(true); - } catch (NoSuchFieldException ignored) { - // Ignore - maybe this is not CombinedMessageQueue? - } - - try { MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); @@ -155,15 +146,6 @@ public class TestableLooper { mLooper = l; mQueue = mLooper.getQueue(); mHandler = new Handler(mLooper); - - // If we are using CombinedMessageQueue, we need to disable concurrent mode for testing. - if (MESSAGE_QUEUE_USE_CONCURRENT_FIELD != null) { - try { - MESSAGE_QUEUE_USE_CONCURRENT_FIELD.set(mQueue, false); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } } /** diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java index 1bcfaf60857d..56b0a25ed2dd 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooper.java +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -100,18 +100,6 @@ public class TestLooper { throw new RuntimeException("Reflection error constructing or accessing looper", e); } - // If we are using CombinedMessageQueue, we need to disable concurrent mode for testing. - try { - Field messageQueueUseConcurrentField = - MessageQueue.class.getDeclaredField("mUseConcurrent"); - messageQueueUseConcurrentField.setAccessible(true); - messageQueueUseConcurrentField.set(mLooper.getQueue(), false); - } catch (NoSuchFieldException e) { - // Ignore - maybe this is not CombinedMessageQueue? - } catch (IllegalAccessException e) { - throw new RuntimeException("Reflection error constructing or accessing looper", e); - } - mClock = clock; } |