diff options
126 files changed, 3975 insertions, 930 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 40dcc427732b..865afc060736 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -16704,7 +16704,6 @@ package android.graphics { } public final class RecordingCanvas extends android.graphics.Canvas { - method public final void drawMesh(@NonNull android.graphics.Mesh, android.graphics.BlendMode, @NonNull android.graphics.Paint); } public final class Rect implements android.os.Parcelable { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 2c01e3f42631..debf1bfdfc8c 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -56,6 +56,7 @@ package android { field public static final String BIND_CONTENT_SUGGESTIONS_SERVICE = "android.permission.BIND_CONTENT_SUGGESTIONS_SERVICE"; field public static final String BIND_DIRECTORY_SEARCH = "android.permission.BIND_DIRECTORY_SEARCH"; field public static final String BIND_DISPLAY_HASHING_SERVICE = "android.permission.BIND_DISPLAY_HASHING_SERVICE"; + field @FlaggedApi("com.android.internal.telephony.flags.ap_domain_selection_enabled") public static final String BIND_DOMAIN_SELECTION_SERVICE = "android.permission.BIND_DOMAIN_SELECTION_SERVICE"; field public static final String BIND_DOMAIN_VERIFICATION_AGENT = "android.permission.BIND_DOMAIN_VERIFICATION_AGENT"; field public static final String BIND_EUICC_SERVICE = "android.permission.BIND_EUICC_SERVICE"; field public static final String BIND_EXTERNAL_STORAGE_SERVICE = "android.permission.BIND_EXTERNAL_STORAGE_SERVICE"; @@ -392,6 +393,7 @@ package android { field @Deprecated public static final String UPDATE_TIME_ZONE_RULES = "android.permission.UPDATE_TIME_ZONE_RULES"; field public static final String UPGRADE_RUNTIME_PERMISSIONS = "android.permission.UPGRADE_RUNTIME_PERMISSIONS"; field public static final String USER_ACTIVITY = "android.permission.USER_ACTIVITY"; + field @FlaggedApi("android.hardware.biometrics.face_background_authentication") public static final String USE_BACKGROUND_FACE_AUTHENTICATION = "android.permission.USE_BACKGROUND_FACE_AUTHENTICATION"; field public static final String USE_COLORIZED_NOTIFICATIONS = "android.permission.USE_COLORIZED_NOTIFICATIONS"; field public static final String USE_RESERVED_DISK = "android.permission.USE_RESERVED_DISK"; field public static final String UWB_PRIVILEGED = "android.permission.UWB_PRIVILEGED"; @@ -3201,6 +3203,7 @@ package android.companion.virtual { public final class VirtualDeviceManager { method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.VirtualDeviceManager.VirtualDevice createVirtualDevice(int, @NonNull android.companion.virtual.VirtualDeviceParams); + method @FlaggedApi("android.companion.virtual.flags.persistent_device_id_api") @Nullable public CharSequence getDisplayNameForPersistentDeviceId(@NonNull String); field public static final int LAUNCH_FAILURE_NO_ACTIVITY = 2; // 0x2 field public static final int LAUNCH_FAILURE_PENDING_INTENT_CANCELED = 1; // 0x1 field public static final int LAUNCH_SUCCESS = 0; // 0x0 @@ -3551,6 +3554,7 @@ package android.content { field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String ECM_ENHANCED_CONFIRMATION_SERVICE = "ecm_enhanced_confirmation"; field public static final String ETHERNET_SERVICE = "ethernet"; field public static final String EUICC_CARD_SERVICE = "euicc_card"; + field @FlaggedApi("android.hardware.biometrics.face_background_authentication") public static final String FACE_SERVICE = "face"; field public static final String FONT_SERVICE = "font"; field public static final String HDMI_CONTROL_SERVICE = "hdmi_control"; field public static final String MEDIA_TRANSCODING_SERVICE = "media_transcoding"; @@ -4731,6 +4735,15 @@ package android.hardware.display { } +package android.hardware.face { + + @FlaggedApi("android.hardware.biometrics.face_background_authentication") public class FaceManager { + method @FlaggedApi("android.hardware.biometrics.face_background_authentication") @RequiresPermission(android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION) public void authenticateInBackground(@Nullable java.util.concurrent.Executor, @Nullable android.hardware.biometrics.BiometricPrompt.CryptoObject, @Nullable android.os.CancellationSignal, @NonNull android.hardware.biometrics.BiometricPrompt.AuthenticationCallback); + method @FlaggedApi("android.hardware.biometrics.face_background_authentication") @RequiresPermission(anyOf={"android.permission.USE_BIOMETRIC_INTERNAL", android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION}) public boolean hasEnrolledTemplates(); + } + +} + package android.hardware.hdmi { public abstract class HdmiClient { diff --git a/core/api/test-current.txt b/core/api/test-current.txt index a840fc2b394f..a866a34166f7 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -186,6 +186,7 @@ package android.app { method public void setEligibleForLegacyPermissionPrompt(boolean); method public static void setExitTransitionTimeout(long); method public void setLaunchActivityType(int); + method public void setLaunchCookie(@NonNull android.app.ActivityOptions.LaunchCookie); method public void setLaunchTaskDisplayAreaFeatureId(int); method public void setLaunchWindowingMode(int); method public void setLaunchedFromBubble(boolean); @@ -193,6 +194,10 @@ package android.app { method public void setTaskOverlay(boolean, boolean); } + public static final class ActivityOptions.LaunchCookie { + ctor public ActivityOptions.LaunchCookie(); + } + public static interface ActivityOptions.OnAnimationFinishedListener { method public void onAnimationFinished(long); } diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 5318bb722b5f..b2c64756e4bf 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -4413,6 +4413,10 @@ public class ActivityManager { * <p>The caller must hold the {@link android.Manifest.permission#PACKAGE_USAGE_STATS} * permission to use this feature.</p> * + * <p>Calling this API with the same instance of {@code listener} without + * unregistering with {@link #removeOnUidImportanceListener} before it will result in + * an {@link IllegalArgumentException}.</p> + * * @throws IllegalArgumentException If the listener is already registered. * @throws SecurityException If the caller does not hold * {@link android.Manifest.permission#PACKAGE_USAGE_STATS}. @@ -4438,6 +4442,10 @@ public class ActivityManager { * all UIDs will be monitored by this listener, this will be equivalent to the * {@link #addOnUidImportanceListener(OnUidImportanceListener, int)} in this case. * + * <p>Calling this API with the same instance of {@code listener} without + * unregistering with {@link #removeOnUidImportanceListener} before it will result in + * an {@link IllegalArgumentException}.</p> + * * @throws IllegalArgumentException If the listener is already registered. * @hide */ diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 8af7ed1bc8d3..57fca74e7e59 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -29,6 +29,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; import android.app.ExitTransitionCoordinator.ActivityExitTransitionCallbacks; @@ -41,6 +42,7 @@ import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Rect; import android.hardware.HardwareBuffer; +import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -1921,6 +1923,38 @@ public class ActivityOptions extends ComponentOptions { } /** + * An opaque token to use with {@link #setLaunchCookie(LaunchCookie)}. + * + * @hide + */ + @SuppressLint("UnflaggedApi") + @TestApi + public static final class LaunchCookie { + /** @hide */ + public final IBinder binder = new Binder(); + + /** @hide */ + @SuppressLint("UnflaggedApi") + @TestApi + public LaunchCookie() {} + } + + /** + * Sets a launch cookie that can be used to track the {@link Activity} and task that are + * launched as a result of this option. If the launched activity is a trampoline that starts + * another activity immediately, the cookie will be transferred to the next activity. + * + * @param launchCookie a developer specified identifier for a specific task. + * + * @hide + */ + @SuppressLint("UnflaggedApi") + @TestApi + public void setLaunchCookie(@NonNull LaunchCookie launchCookie) { + setLaunchCookie(launchCookie.binder); + } + + /** * Sets a launch cookie that can be used to track the activity and task that are launch as a * result of this option. If the launched activity is a trampoline that starts another activity * immediately, the cookie will be transferred to the next activity. diff --git a/core/java/android/app/GrammaticalInflectionManager.java b/core/java/android/app/GrammaticalInflectionManager.java index a55121aaa12c..483a6e11a42a 100644 --- a/core/java/android/app/GrammaticalInflectionManager.java +++ b/core/java/android/app/GrammaticalInflectionManager.java @@ -114,7 +114,7 @@ public class GrammaticalInflectionManager { } try { - mService.setSystemWideGrammaticalGender(mContext.getUserId(), grammaticalGender); + mService.setSystemWideGrammaticalGender(grammaticalGender, mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -131,7 +131,8 @@ public class GrammaticalInflectionManager { @Configuration.GrammaticalGender public int getSystemGrammaticalGender() { try { - return mService.getSystemGrammaticalGender(mContext.getUserId()); + return mService.getSystemGrammaticalGender(mContext.getAttributionSource(), + mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/app/IGrammaticalInflectionManager.aidl b/core/java/android/app/IGrammaticalInflectionManager.aidl index 48a48416d592..86f2e9110889 100644 --- a/core/java/android/app/IGrammaticalInflectionManager.aidl +++ b/core/java/android/app/IGrammaticalInflectionManager.aidl @@ -1,5 +1,22 @@ +/** + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package android.app; +import android.content.AttributionSource; /** * Internal interface used to control app-specific gender. @@ -20,10 +37,10 @@ package android.app; /** * Sets the grammatical gender to system. */ - void setSystemWideGrammaticalGender(int userId, int gender); + void setSystemWideGrammaticalGender(int gender, int userId); /** * Gets the grammatical gender from system. */ - int getSystemGrammaticalGender(int userId); + int getSystemGrammaticalGender(in AttributionSource attributionSource, int userId); } diff --git a/core/java/android/app/IWallpaperManager.aidl b/core/java/android/app/IWallpaperManager.aidl index d7d654672abc..5acb9b5cf9ae 100644 --- a/core/java/android/app/IWallpaperManager.aidl +++ b/core/java/android/app/IWallpaperManager.aidl @@ -16,6 +16,7 @@ package android.app; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.os.Bundle; @@ -26,6 +27,8 @@ import android.app.WallpaperInfo; import android.content.ComponentName; import android.app.WallpaperColors; +import java.util.List; + /** @hide */ interface IWallpaperManager { @@ -39,15 +42,21 @@ interface IWallpaperManager { * FLAG_SET_SYSTEM * FLAG_SET_LOCK * - * A 'null' cropHint rectangle is explicitly permitted as a sentinel for "whatever - * the source image's bounding rect is." + * 'screenOrientations' and 'crops' define how the wallpaper will be positioned for + * different screen orientations. If some screen orientations are missing, crops for these + * orientations will be added by the system. + * + * If 'screenOrientations' is null, 'crops' can be null or a singleton list. The system will + * fit the provided crop (or the whole image, if 'crops' is 'null') for the current device + * orientation, and add crops for the missing orientations. * * The completion callback's "onWallpaperChanged()" method is invoked when the * new wallpaper content is ready to display. */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.SET_WALLPAPER)") ParcelFileDescriptor setWallpaper(String name, in String callingPackage, - in Rect cropHint, boolean allowBackup, out Bundle extras, int which, - IWallpaperManagerCallback completion, int userId); + in int[] screenOrientations, in List<Rect> crops, boolean allowBackup, + out Bundle extras, int which, IWallpaperManagerCallback completion, int userId); /** * Set the live wallpaper. @@ -78,6 +87,30 @@ interface IWallpaperManager { boolean getCropped); /** + * For a given user and a list of display sizes, get a list of Rect representing the + * area of the current wallpaper that is displayed for each display size. + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL)") + @SuppressWarnings(value={"untyped-collection"}) + List getBitmapCrops(in List<Point> displaySizes, int which, boolean originalBitmap, 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 + */ + @SuppressWarnings(value={"untyped-collection"}) + List getFutureBitmapCrops(in Point bitmapSize, in List<Point> displaySizes, + in int[] screenOrientations, in List<Rect> crops); + + /** + * Return how a bitmap of a given size would be cropped when set with the given suggested crops. + * @hide + */ + @SuppressWarnings(value={"untyped-collection"}) + Rect getBitmapCrop(in Point bitmapSize, in int[] screenOrientations, in List<Rect> crops); + + /** * Retrieve the given user's current wallpaper ID of the given kind. */ int getWallpaperIdForUser(int which, int userId); @@ -245,11 +278,4 @@ interface IWallpaperManager { * @hide */ boolean isStaticWallpaper(int which); - - /** - * Temporary method for project b/270726737. - * Return true if the wallpaper supports different crops for different display dimensions. - * @hide - */ - boolean isMultiCropEnabled(); } diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 62db90f79091..63f37f150d33 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -18,11 +18,14 @@ 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.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; +import static com.android.window.flags.Flags.FLAG_MULTI_CROP; import static com.android.window.flags.Flags.multiCrop; +import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.NonNull; @@ -58,6 +61,7 @@ import android.graphics.ImageDecoder; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; +import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; @@ -84,6 +88,7 @@ import android.util.ArraySet; import android.util.Log; import android.util.MathUtils; import android.util.Pair; +import android.util.SparseArray; import android.view.Display; import android.view.WindowManagerGlobal; @@ -104,6 +109,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -289,6 +295,79 @@ public class WallpaperManager { public static final String EXTRA_FROM_FOREGROUND_APP = "android.service.wallpaper.extra.FROM_FOREGROUND_APP"; + /** + * The different screen orientations. {@link #getOrientation} provides their exact definition. + * This is only used internally by the framework and the WallpaperBackupAgent. + * @hide + */ + @IntDef(value = { + ORIENTATION_UNKNOWN, + PORTRAIT, + LANDSCAPE, + SQUARE_PORTRAIT, + SQUARE_LANDSCAPE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ScreenOrientation {} + + /** + * @hide + */ + public static final int ORIENTATION_UNKNOWN = -1; + + /** + * Portrait orientation of most screens + * @hide + */ + public static final int PORTRAIT = 0; + + /** + * Landscape orientation of most screens + * @hide + */ + public static final int LANDSCAPE = 1; + + /** + * Portrait orientation with similar width and height (e.g. the inner screen of a foldable) + * @hide + */ + public static final int SQUARE_PORTRAIT = 2; + + /** + * Landscape orientation with similar width and height (e.g. the inner screen of a foldable) + * @hide + */ + public static final int SQUARE_LANDSCAPE = 3; + + /** + * Converts a (width, height) screen size to a {@link ScreenOrientation}. + * @param screenSize the dimensions of a screen + * @return the corresponding {@link ScreenOrientation}. + * @hide + */ + public static @ScreenOrientation int getOrientation(Point screenSize) { + float ratio = ((float) screenSize.x) / screenSize.y; + // ratios between 3/4 and 4/3 are considered square + return ratio >= 4 / 3f ? LANDSCAPE + : ratio > 1f ? SQUARE_LANDSCAPE + : ratio > 3 / 4f ? SQUARE_PORTRAIT + : PORTRAIT; + } + + /** + * Get the 90° rotation of a given orientation + * @hide + */ + public static @ScreenOrientation int getRotatedOrientation(@ScreenOrientation int orientation) { + switch (orientation) { + case PORTRAIT: return LANDSCAPE; + case LANDSCAPE: return PORTRAIT; + case SQUARE_PORTRAIT: return SQUARE_LANDSCAPE; + case SQUARE_LANDSCAPE: return SQUARE_PORTRAIT; + default: return ORIENTATION_UNKNOWN; + } + } + // flags for which kind of wallpaper to act on /** @hide */ @@ -867,15 +946,8 @@ public class WallpaperManager { * @hide */ public static boolean isMultiCropEnabled() { - if (sGlobals == null) { - sIsMultiCropEnabled = multiCrop(); - } if (sIsMultiCropEnabled == null) { - try { - sIsMultiCropEnabled = sGlobals.mService.isMultiCropEnabled(); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } + sIsMultiCropEnabled = multiCrop(); } return sIsMultiCropEnabled; } @@ -1502,6 +1574,99 @@ public class WallpaperManager { } /** + * For the current user, given a list of display sizes, return a list of rectangles representing + * the area of the current wallpaper that would be shown for each of these sizes. + * + * @param displaySizes the display sizes. + * @param which wallpaper type. Must be either {@link #FLAG_SYSTEM} or {@link #FLAG_LOCK}. + * @param originalBitmap If true, return areas relative to the original bitmap. + * 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 + * shared home + lock wallpaper. + * @hide + */ + @FlaggedApi(FLAG_MULTI_CROP) + @RequiresPermission(READ_WALLPAPER_INTERNAL) + @Nullable + public List<Rect> getBitmapCrops(@NonNull List<Point> displaySizes, + @SetWallpaperFlags int which, boolean originalBitmap) { + checkExactlyOneWallpaperFlagSet(which); + try { + return sGlobals.mService.getBitmapCrops(displaySizes, which, originalBitmap, + mContext.getUserId()); + } 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 + * {@link #setStreamWithCrops(InputStream, Map, boolean, int)}. + * + * @return A List of Rect where the Rect is within the bitmap, and corresponds to what is + * displayed for each display size. The Rect may have a larger width/height ratio than + * the display due to parallax. + * @hide + */ + @FlaggedApi(FLAG_MULTI_CROP) + @Nullable + public List<Rect> getBitmapCrops(@NonNull Point bitmapSize, @NonNull List<Point> displaySizes, + @Nullable Map<Point, Rect> cropHints) { + try { + if (cropHints == null) cropHints = Map.of(); + Set<Map.Entry<Point, Rect>> entries = cropHints.entrySet(); + int[] screenOrientations = entries.stream().mapToInt(entry -> + getOrientation(entry.getKey())).toArray(); + List<Rect> crops = entries.stream().map(Map.Entry::getValue).toList(); + return sGlobals.mService.getFutureBitmapCrops(bitmapSize, displaySizes, + screenOrientations, crops); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * For preview purposes. + * Compute the wallpaper colors of the given bitmap, if it was set as wallpaper via + * {@link #setBitmapWithCrops(Bitmap, Map, boolean, int)} or + * {@link #setStreamWithCrops(InputStream, Map, boolean, int)}. + * Return {@code null} if an error occurred and the colors could not be computed. + * + * @hide + */ + @FlaggedApi(FLAG_MULTI_CROP) + @RequiresPermission(SET_WALLPAPER_DIM_AMOUNT) + @Nullable + public WallpaperColors getWallpaperColors(@NonNull Bitmap bitmap, + @Nullable Map<Point, Rect> cropHints) { + if (sGlobals.mService == null) { + Log.w(TAG, "WallpaperService not running"); + throw new RuntimeException(new DeadSystemException()); + } + try { + if (cropHints == null) cropHints = Map.of(); + Set<Map.Entry<Point, Rect>> entries = cropHints.entrySet(); + int[] screenOrientations = entries.stream().mapToInt(entry -> + getOrientation(entry.getKey())).toArray(); + List<Rect> crops = entries.stream().map(Map.Entry::getValue).toList(); + Point bitmapSize = new Point(bitmap.getWidth(), bitmap.getHeight()); + Rect crop = sGlobals.mService.getBitmapCrop(bitmapSize, screenOrientations, crops); + float dimAmount = getWallpaperDimAmount(); + Bitmap croppedBitmap = Bitmap.createBitmap( + bitmap, crop.left, crop.top, crop.width(), crop.height()); + WallpaperColors result = WallpaperColors.fromBitmap(croppedBitmap, dimAmount); + return result; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * <strong> Important note: </strong> * <ul> * <li>Up to version S, this method requires the @@ -1971,7 +2136,7 @@ public class WallpaperManager { /* Set the wallpaper to the default values */ ParcelFileDescriptor fd = sGlobals.mService.setWallpaper( "res:" + resources.getResourceName(resid), - mContext.getOpPackageName(), null, false, result, which, completion, + mContext.getOpPackageName(), null, null, false, result, which, completion, mContext.getUserId()); if (fd != null) { FileOutputStream fos = null; @@ -2089,6 +2254,11 @@ public class WallpaperManager { public int setBitmap(Bitmap fullImage, Rect visibleCropHint, boolean allowBackup, @SetWallpaperFlags int which, int userId) throws IOException { + if (multiCrop()) { + SparseArray<Rect> cropMap = new SparseArray<>(); + if (visibleCropHint != null) cropMap.put(ORIENTATION_UNKNOWN, visibleCropHint); + return setBitmapWithCrops(fullImage, cropMap, allowBackup, which, userId); + } validateRect(visibleCropHint); if (sGlobals.mService == null) { Log.w(TAG, "WallpaperService not running"); @@ -2096,9 +2266,69 @@ public class WallpaperManager { } final Bundle result = new Bundle(); final WallpaperSetCompletion completion = new WallpaperSetCompletion(); + final List<Rect> crops = visibleCropHint == null ? null : List.of(visibleCropHint); try { ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null, - mContext.getOpPackageName(), visibleCropHint, allowBackup, + mContext.getOpPackageName(), null, crops, allowBackup, result, which, + completion, userId); + if (fd != null) { + FileOutputStream fos = null; + try { + fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd); + fullImage.compress(Bitmap.CompressFormat.PNG, 90, fos); + fos.close(); + completion.waitForCompletion(); + } finally { + IoUtils.closeQuietly(fos); + } + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + return result.getInt(EXTRA_NEW_WALLPAPER_ID, 0); + } + + /** + * 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 + * map is empty or some entries are missing, the system will apply a default + * strategy to position the wallpaper for any unspecified screen dimensions. + * @hide + */ + @FlaggedApi(FLAG_MULTI_CROP) + @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) + public int setBitmapWithCrops(@Nullable Bitmap fullImage, @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 setBitmapWithCrops(fullImage, crops, allowBackup, which, mContext.getUserId()); + } + + @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) + private int setBitmapWithCrops(@Nullable Bitmap fullImage, @NonNull SparseArray<Rect> cropHints, + boolean allowBackup, @SetWallpaperFlags int which, int userId) throws IOException { + if (sGlobals.mService == null) { + Log.w(TAG, "WallpaperService not running"); + throw new RuntimeException(new DeadSystemException()); + } + int size = cropHints.size(); + int[] screenOrientations = new int[size]; + List<Rect> crops = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + screenOrientations[i] = cropHints.keyAt(i); + Rect cropHint = cropHints.valueAt(i); + validateRect(cropHint); + crops.add(cropHint); + } + final Bundle result = new Bundle(); + final WallpaperSetCompletion completion = new WallpaperSetCompletion(); + try { + ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null, + mContext.getOpPackageName(), screenOrientations, crops, allowBackup, result, which, completion, userId); if (fd != null) { FileOutputStream fos = null; @@ -2214,6 +2444,11 @@ public class WallpaperManager { public int setStream(InputStream bitmapData, Rect visibleCropHint, boolean allowBackup, @SetWallpaperFlags int which) throws IOException { + if (multiCrop()) { + SparseArray<Rect> cropMap = new SparseArray<>(); + if (visibleCropHint != null) cropMap.put(ORIENTATION_UNKNOWN, visibleCropHint); + return setStreamWithCrops(bitmapData, cropMap, allowBackup, which); + } validateRect(visibleCropHint); if (sGlobals.mService == null) { Log.w(TAG, "WallpaperService not running"); @@ -2221,10 +2456,11 @@ public class WallpaperManager { } final Bundle result = new Bundle(); final WallpaperSetCompletion completion = new WallpaperSetCompletion(); + final List<Rect> crops = visibleCropHint == null ? null : List.of(visibleCropHint); try { ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null, - mContext.getOpPackageName(), visibleCropHint, allowBackup, - result, which, completion, mContext.getUserId()); + mContext.getOpPackageName(), null, crops, allowBackup, result, which, + completion, mContext.getUserId()); if (fd != null) { FileOutputStream fos = null; try { @@ -2244,6 +2480,75 @@ 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 + * map is empty or some entries are missing, the system will apply a default + * strategy to position the wallpaper for any unspecified screen dimensions. + * @hide + */ + @FlaggedApi(FLAG_MULTI_CROP) + @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) + public int setStreamWithCrops(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); + } + + /** + * 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 cropHints map from {@link ScreenOrientation} to a sub-region of the image to display + * for that screen orientation. + * @hide + */ + @FlaggedApi(FLAG_MULTI_CROP) + @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) + public int setStreamWithCrops(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()); + } + int size = cropHints.size(); + int[] screenOrientations = new int[size]; + List<Rect> crops = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + screenOrientations[i] = cropHints.keyAt(i); + Rect cropHint = cropHints.valueAt(i); + validateRect(cropHint); + crops.add(cropHint); + } + final Bundle result = new Bundle(); + final WallpaperSetCompletion completion = new WallpaperSetCompletion(); + try { + ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null, + mContext.getOpPackageName(), screenOrientations, crops, allowBackup, + result, which, completion, mContext.getUserId()); + if (fd != null) { + FileOutputStream fos = null; + try { + fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd); + copyStreamToWallpaperFile(bitmapData, fos); + fos.close(); + completion.waitForCompletion(); + } finally { + IoUtils.closeQuietly(fos); + } + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + return result.getInt(EXTRA_NEW_WALLPAPER_ID, 0); + } + + /** * Return whether any users are currently set to use the wallpaper * with the given resource ID. That is, their wallpaper has been * set through {@link #setResource(int)} with the same resource id. @@ -2499,7 +2804,7 @@ public class WallpaperManager { * @hide */ @SystemApi - @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT) + @RequiresPermission(SET_WALLPAPER_DIM_AMOUNT) public void setWallpaperDimAmount(@FloatRange (from = 0f, to = 1f) float dimAmount) { if (sGlobals.mService == null) { Log.w(TAG, "WallpaperService not running"); @@ -2519,7 +2824,7 @@ public class WallpaperManager { * @hide */ @SystemApi - @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT) + @RequiresPermission(SET_WALLPAPER_DIM_AMOUNT) public @FloatRange (from = 0f, to = 1f) float getWallpaperDimAmount() { if (sGlobals.mService == null) { Log.w(TAG, "WallpaperService not running"); diff --git a/core/java/android/app/grammatical_inflection_manager.aconfig b/core/java/android/app/grammatical_inflection_manager.aconfig index 989ce61337a3..68d12ba75560 100644 --- a/core/java/android/app/grammatical_inflection_manager.aconfig +++ b/core/java/android/app/grammatical_inflection_manager.aconfig @@ -2,7 +2,7 @@ package: "android.app" flag { name: "system_terms_of_address_enabled" - namespace: "grammatical_gender" + namespace: "globalintl" description: "Feature flag for System Terms of Address" bug: "297798866" } diff --git a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl index 04933126f294..325aa28fde08 100644 --- a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl +++ b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl @@ -78,6 +78,11 @@ interface IVirtualDeviceManager { int getDeviceIdForDisplayId(int displayId); /** + * Returns the display name corresponding to the given persistent device ID, if any. + */ + CharSequence getDisplayNameForPersistentDeviceId(in String persistentDeviceId); + + /** * Checks whether the passed {@code deviceId} is a valid virtual device ID or not. * {@link VirtualDeviceManager#DEVICE_ID_DEFAULT} is not valid as it is the ID of the default * device which is not a virtual device. {@code deviceId} must correspond to a virtual device diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index a4cada28999e..90d251b04f67 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -361,6 +361,34 @@ public final class VirtualDeviceManager { } /** + * Get the display name for a given persistent device ID. + * + * <p>This will work even if currently there is no valid virtual device with the given + * persistent ID, as long as such a device has been created or can be created.</p> + * + * @return the display name associated with the given persistent device ID, or {@code null} if + * the persistent ID is invalid or does not correspond to a virtual device. + * + * @hide + */ + // TODO(b/315481938): Link @see VirtualDevice#getPersistentDeviceId() + @FlaggedApi(Flags.FLAG_PERSISTENT_DEVICE_ID_API) + @SystemApi + @Nullable + public CharSequence getDisplayNameForPersistentDeviceId(@NonNull String persistentDeviceId) { + if (mService == null) { + Log.w(TAG, "Failed to retrieve virtual devices; no virtual device manager service."); + return null; + } + try { + return mService.getDisplayNameForPersistentDeviceId( + Objects.requireNonNull(persistentDeviceId)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Checks whether the passed {@code deviceId} is a valid virtual device ID or not. * {@link Context#DEVICE_ID_DEFAULT} is not valid as it is the ID of the default * device which is not a virtual device. {@code deviceId} must correspond to a virtual device diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 249c0e434e78..67a3627a399f 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -5079,6 +5079,8 @@ public abstract class Context { * @see #getSystemService * @see android.hardware.face.FaceManager */ + @FlaggedApi(android.hardware.biometrics.Flags.FLAG_FACE_BACKGROUND_AUTHENTICATION) + @SystemApi public static final String FACE_SERVICE = "face"; /** diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig index 3ba8be4cc2ab..8165d44b251a 100644 --- a/core/java/android/hardware/biometrics/flags.aconfig +++ b/core/java/android/hardware/biometrics/flags.aconfig @@ -28,3 +28,10 @@ flag { bug: "302735104" } +flag { + name: "face_background_authentication" + namespace: "biometrics_framework" + description: "Feature flag for allowing face background authentication with USE_BACKGROUND_FACE_AUTHENTICATION." + bug: "318584190" +} + diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java index 02304b5ba4f3..bae5e7f83569 100644 --- a/core/java/android/hardware/face/FaceManager.java +++ b/core/java/android/hardware/face/FaceManager.java @@ -18,18 +18,23 @@ package android.hardware.face; import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.Manifest.permission.MANAGE_BIOMETRIC; +import static android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_NONE; +import static android.hardware.biometrics.Flags.FLAG_FACE_BACKGROUND_AUTHENTICATION; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SystemApi; import android.annotation.SystemService; import android.content.Context; import android.content.pm.PackageManager; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricFaceConstants; +import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.BiometricStateListener; import android.hardware.biometrics.CryptoObject; import android.hardware.biometrics.IBiometricServiceLockoutResetCallback; @@ -37,9 +42,9 @@ import android.os.Binder; import android.os.CancellationSignal; import android.os.CancellationSignal.OnCancelListener; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IBinder; import android.os.IRemoteCallback; -import android.os.Looper; import android.os.PowerManager; import android.os.RemoteException; import android.os.Trace; @@ -49,15 +54,21 @@ import android.util.Slog; import android.view.Surface; import com.android.internal.R; -import com.android.internal.os.SomeArgs; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executor; /** * A class that coordinates access to the face authentication hardware. + * + * <p>Please use {@link BiometricPrompt} for face authentication unless the experience must be + * customized for unique system-level utilities, like the lock screen or ambient background usage. + * * @hide */ +@FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION) +@SystemApi @SystemService(Context.FACE_SERVICE) public class FaceManager implements BiometricAuthenticator, BiometricFaceConstants { @@ -88,81 +99,76 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan @Nullable private GenerateChallengeCallback mGenerateChallengeCallback; private CryptoObject mCryptoObject; private Face mRemovalFace; - private Handler mHandler; + private Executor mExecutor; private List<FaceSensorPropertiesInternal> mProps = new ArrayList<>(); private final IFaceServiceReceiver mServiceReceiver = new IFaceServiceReceiver.Stub() { @Override // binder call public void onEnrollResult(Face face, int remaining) { - mHandler.obtainMessage(MSG_ENROLL_RESULT, remaining, 0, face).sendToTarget(); + mExecutor.execute(() -> sendEnrollResult(face, remaining)); } @Override // binder call public void onAcquired(int acquireInfo, int vendorCode) { - mHandler.obtainMessage(MSG_ACQUIRED, acquireInfo, vendorCode).sendToTarget(); + mExecutor.execute(() -> sendAcquiredResult(acquireInfo, vendorCode)); } @Override // binder call public void onAuthenticationSucceeded(Face face, int userId, boolean isStrongBiometric) { - mHandler.obtainMessage(MSG_AUTHENTICATION_SUCCEEDED, userId, - isStrongBiometric ? 1 : 0, face).sendToTarget(); + mExecutor.execute(() -> sendAuthenticatedSucceeded(face, userId, isStrongBiometric)); } @Override // binder call public void onFaceDetected(int sensorId, int userId, boolean isStrongBiometric) { - mHandler.obtainMessage(MSG_FACE_DETECTED, sensorId, userId, isStrongBiometric) - .sendToTarget(); + mExecutor.execute(() -> sendFaceDetected(sensorId, userId, isStrongBiometric)); } @Override // binder call public void onAuthenticationFailed() { - mHandler.obtainMessage(MSG_AUTHENTICATION_FAILED).sendToTarget(); + mExecutor.execute(() -> sendAuthenticatedFailed()); } @Override // binder call public void onError(int error, int vendorCode) { - mHandler.obtainMessage(MSG_ERROR, error, vendorCode).sendToTarget(); + mExecutor.execute(() -> sendErrorResult(error, vendorCode)); } @Override // binder call public void onRemoved(Face face, int remaining) { - mHandler.obtainMessage(MSG_REMOVED, remaining, 0, face).sendToTarget(); - if (remaining == 0) { - Settings.Secure.putIntForUser(mContext.getContentResolver(), - Settings.Secure.FACE_UNLOCK_RE_ENROLL, 0, - UserHandle.USER_CURRENT); - } + mExecutor.execute(() -> { + sendRemovedResult(face, remaining); + if (remaining == 0) { + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.FACE_UNLOCK_RE_ENROLL, 0, + UserHandle.USER_CURRENT); + } + }); } @Override public void onFeatureSet(boolean success, int feature) { - mHandler.obtainMessage(MSG_SET_FEATURE_COMPLETED, feature, 0, success).sendToTarget(); + mExecutor.execute(() -> sendSetFeatureCompleted(success, feature)); } @Override public void onFeatureGet(boolean success, int[] features, boolean[] featureState) { - SomeArgs args = SomeArgs.obtain(); - args.arg1 = success; - args.arg2 = features; - args.arg3 = featureState; - mHandler.obtainMessage(MSG_GET_FEATURE_COMPLETED, args).sendToTarget(); + mExecutor.execute(() -> sendGetFeatureCompleted(success, features, featureState)); } @Override public void onChallengeGenerated(int sensorId, int userId, long challenge) { - mHandler.obtainMessage(MSG_CHALLENGE_GENERATED, sensorId, userId, challenge) - .sendToTarget(); + mExecutor.execute(() -> sendChallengeGenerated(sensorId, userId, challenge)); } @Override public void onAuthenticationFrame(FaceAuthenticationFrame frame) { - mHandler.obtainMessage(MSG_AUTHENTICATION_FRAME, frame).sendToTarget(); + mExecutor.execute(() -> sendAuthenticationFrame(frame)); } @Override public void onEnrollmentFrame(FaceEnrollFrame frame) { - mHandler.obtainMessage(MSG_ENROLLMENT_FRAME, frame).sendToTarget(); + mExecutor.execute(() -> sendEnrollmentFrame(frame)); } }; @@ -175,7 +181,7 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan if (mService == null) { Slog.v(TAG, "FaceAuthenticationManagerService was null"); } - mHandler = new MyHandler(context); + mExecutor = context.getMainExecutor(); if (context.checkCallingOrSelfPermission(USE_BIOMETRIC_INTERNAL) == PackageManager.PERMISSION_GRANTED) { addAuthenticatorsRegisteredCallback(new IFaceAuthenticatorsRegisteredCallback.Stub() { @@ -189,18 +195,16 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan } /** - * Use the provided handler thread for events. + * Returns an {@link Executor} for the given {@link Handler} or the main {@link Executor} if + * {@code handler} is {@code null}. */ - private void useHandler(Handler handler) { - if (handler != null) { - mHandler = new MyHandler(handler.getLooper()); - } else if (mHandler.getLooper() != mContext.getMainLooper()) { - mHandler = new MyHandler(mContext.getMainLooper()); - } + private @NonNull Executor createExecutorForHandlerIfNeeded(@Nullable Handler handler) { + return handler != null ? new HandlerExecutor(handler) : mContext.getMainExecutor(); } /** * @deprecated use {@link #authenticate(CryptoObject, CancellationSignal, AuthenticationCallback, Handler, FaceAuthenticateOptions)}. + * @hide */ @Deprecated @RequiresPermission(USE_BIOMETRIC_INTERNAL) @@ -212,17 +216,22 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan } /** - * Request authentication. This call operates the face recognition hardware and starts capturing images. + * Request authentication. + * + * <p>This call operates the face recognition hardware and starts capturing images. * It terminates when * {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or * {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called, at * which point the object is no longer valid. The operation can be canceled by using the - * provided cancel object. + * provided {@code cancel} object. * - * @param crypto object associated with the call or null if none required - * @param cancel an object that can be used to cancel authentication + * @param crypto the cryptographic operations to use for authentication or {@code null} if + * none required + * @param cancel an object that can be used to cancel authentication or {@code null} if not + * needed * @param callback an object to receive authentication events - * @param handler an optional handler to handle callback events + * @param handler an optional handler to handle callback events or {@code null} to obtain main + * {@link Executor} from {@link Context} * @param options additional options to customize this request * @throws IllegalArgumentException if the crypto operation is not supported or is not backed * by @@ -235,6 +244,14 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan public void authenticate(@Nullable CryptoObject crypto, @Nullable CancellationSignal cancel, @NonNull AuthenticationCallback callback, @Nullable Handler handler, @NonNull FaceAuthenticateOptions options) { + authenticate(crypto, cancel, callback, createExecutorForHandlerIfNeeded(handler), + options, false /* allowBackgroundAuthentication */); + } + + @RequiresPermission(anyOf = {USE_BIOMETRIC_INTERNAL, USE_BACKGROUND_FACE_AUTHENTICATION}) + private void authenticate(@Nullable CryptoObject crypto, @Nullable CancellationSignal cancel, + @NonNull AuthenticationCallback callback, @NonNull Executor executor, + @NonNull FaceAuthenticateOptions options, boolean allowBackgroundAuthentication) { if (callback == null) { throw new IllegalArgumentException("Must supply an authentication callback"); } @@ -249,13 +266,15 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan if (mService != null) { try { - useHandler(handler); + mExecutor = executor; mAuthenticationCallback = callback; mCryptoObject = crypto; final long operationId = crypto != null ? crypto.getOpId() : 0; Trace.beginSection("FaceManager#authenticate"); - final long authId = mService.authenticate( - mToken, operationId, mServiceReceiver, options); + final long authId = allowBackgroundAuthentication + ? mService.authenticateInBackground( + mToken, operationId, mServiceReceiver, options) + : mService.authenticate(mToken, operationId, mServiceReceiver, options); if (cancel != null) { cancel.setOnCancelListener(new OnAuthenticationCancelListener(authId)); } @@ -273,6 +292,67 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan } /** + * Request background face authentication. + * + * <p>This call operates the face recognition hardware and starts capturing images. + * It terminates when + * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationError(int, CharSequence)} or + * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationSucceeded( + * BiometricPrompt.AuthenticationResult)} is called, at which point the object is no longer + * valid. The operation can be canceled by using the provided cancel object. + * + * <p>See {@link BiometricPrompt#authenticate} for more details. Please use + * {@link BiometricPrompt} for face authentication unless the experience must be customized for + * unique system-level utilities, like the lock screen or ambient background usage. + * + * @param executor the specified {@link Executor} to handle callback events; if {@code null}, + * the callback will be executed on the main {@link Executor}. + * @param crypto the cryptographic operations to use for authentication or {@code null} if + * none required. + * @param cancel an object that can be used to cancel authentication or {@code null} if not + * needed. + * @param callback an object to receive authentication events. + * @throws IllegalArgumentException if the crypto operation is not supported or is not backed + * by + * <a href="{@docRoot}training/articles/keystore.html">Android + * Keystore facility</a>. + * @hide + */ + @RequiresPermission(USE_BACKGROUND_FACE_AUTHENTICATION) + @FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION) + @SystemApi + public void authenticateInBackground(@Nullable Executor executor, + @Nullable BiometricPrompt.CryptoObject crypto, @Nullable CancellationSignal cancel, + @NonNull BiometricPrompt.AuthenticationCallback callback) { + authenticate(crypto, cancel, new AuthenticationCallback() { + @Override + public void onAuthenticationError(int errorCode, CharSequence errString) { + callback.onAuthenticationError(errorCode, errString); + } + + @Override + public void onAuthenticationHelp(int helpCode, CharSequence helpString) { + callback.onAuthenticationHelp(helpCode, helpString); + } + + @Override + public void onAuthenticationSucceeded(AuthenticationResult result) { + callback.onAuthenticationSucceeded( + new BiometricPrompt.AuthenticationResult( + crypto, + BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC)); + } + + @Override + public void onAuthenticationFailed() { + callback.onAuthenticationFailed(); + } + }, executor == null ? mContext.getMainExecutor() : executor, + new FaceAuthenticateOptions.Builder().build(), + true /* allowBackgroundAuthentication */); + } + + /** * Uses the face hardware to detect for the presence of a face, without giving details about * accept/reject/lockout. * @hide @@ -628,12 +708,14 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan } /** - * Determine if there is a face enrolled. + * Determine if there are enrolled {@link Face} templates. * - * @return true if a face is enrolled, false otherwise + * @return {@code true} if there are enrolled {@link Face} templates, {@code false} otherwise * @hide */ - @RequiresPermission(USE_BIOMETRIC_INTERNAL) + @RequiresPermission(anyOf = {USE_BIOMETRIC_INTERNAL, USE_BACKGROUND_FACE_AUTHENTICATION}) + @FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION) + @SystemApi public boolean hasEnrolledTemplates() { return hasEnrolledTemplates(UserHandle.myUserId()); } @@ -798,7 +880,7 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan PowerManager.PARTIAL_WAKE_LOCK, "faceLockoutResetCallback"); wakeLock.acquire(); - mHandler.post(() -> { + mExecutor.execute(() -> { try { callback.onLockoutReset(sensorId); } finally { @@ -1268,70 +1350,6 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan } } - private class MyHandler extends Handler { - private MyHandler(Context context) { - super(context.getMainLooper()); - } - - private MyHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(android.os.Message msg) { - Trace.beginSection("FaceManager#handleMessage: " + Integer.toString(msg.what)); - switch (msg.what) { - case MSG_ENROLL_RESULT: - sendEnrollResult((Face) msg.obj, msg.arg1 /* remaining */); - break; - case MSG_ACQUIRED: - sendAcquiredResult(msg.arg1 /* acquire info */, msg.arg2 /* vendorCode */); - break; - case MSG_AUTHENTICATION_SUCCEEDED: - sendAuthenticatedSucceeded((Face) msg.obj, msg.arg1 /* userId */, - msg.arg2 == 1 /* isStrongBiometric */); - break; - case MSG_AUTHENTICATION_FAILED: - sendAuthenticatedFailed(); - break; - case MSG_ERROR: - sendErrorResult(msg.arg1 /* errMsgId */, msg.arg2 /* vendorCode */); - break; - case MSG_REMOVED: - sendRemovedResult((Face) msg.obj, msg.arg1 /* remaining */); - break; - case MSG_SET_FEATURE_COMPLETED: - sendSetFeatureCompleted((boolean) msg.obj /* success */, - msg.arg1 /* feature */); - break; - case MSG_GET_FEATURE_COMPLETED: - SomeArgs args = (SomeArgs) msg.obj; - sendGetFeatureCompleted((boolean) args.arg1 /* success */, - (int[]) args.arg2 /* features */, - (boolean[]) args.arg3 /* featureState */); - args.recycle(); - break; - case MSG_CHALLENGE_GENERATED: - sendChallengeGenerated(msg.arg1 /* sensorId */, msg.arg2 /* userId */, - (long) msg.obj /* challenge */); - break; - case MSG_FACE_DETECTED: - sendFaceDetected(msg.arg1 /* sensorId */, msg.arg2 /* userId */, - (boolean) msg.obj /* isStrongBiometric */); - break; - case MSG_AUTHENTICATION_FRAME: - sendAuthenticationFrame((FaceAuthenticationFrame) msg.obj /* frame */); - break; - case MSG_ENROLLMENT_FRAME: - sendEnrollmentFrame((FaceEnrollFrame) msg.obj /* frame */); - break; - default: - Slog.w(TAG, "Unknown message: " + msg.what); - } - Trace.endSection(); - } - } - private void sendSetFeatureCompleted(boolean success, int feature) { if (mSetFeatureCallback == null) { return; diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl index 0096877f548a..e267e6b22f9d 100644 --- a/core/java/android/hardware/face/IFaceService.aidl +++ b/core/java/android/hardware/face/IFaceService.aidl @@ -45,7 +45,7 @@ interface IFaceService { byte[] dumpSensorServiceStateProto(int sensorId, boolean clearSchedulerBuffer); // Retrieve static sensor properties for all face sensors - @EnforcePermission("USE_BIOMETRIC_INTERNAL") + @EnforcePermission(anyOf = {"USE_BIOMETRIC_INTERNAL", "USE_BACKGROUND_FACE_AUTHENTICATION"}) List<FaceSensorPropertiesInternal> getSensorPropertiesInternal(String opPackageName); // Retrieve static sensor properties for the specified sensor @@ -57,6 +57,11 @@ interface IFaceService { long authenticate(IBinder token, long operationId, IFaceServiceReceiver receiver, in FaceAuthenticateOptions options); + // Authenticate with a face. A requestId is returned that can be used to cancel this operation. + @EnforcePermission("USE_BACKGROUND_FACE_AUTHENTICATION") + long authenticateInBackground(IBinder token, long operationId, IFaceServiceReceiver receiver, + in FaceAuthenticateOptions options); + // Uses the face hardware to detect for the presence of a face, without giving details // about accept/reject/lockout. A requestId is returned that can be used to cancel this // operation. @@ -131,7 +136,7 @@ interface IFaceService { void revokeChallenge(IBinder token, int sensorId, int userId, String opPackageName, long challenge); // Determine if a user has at least one enrolled face - @EnforcePermission("USE_BIOMETRIC_INTERNAL") + @EnforcePermission(anyOf = {"USE_BIOMETRIC_INTERNAL", "USE_BACKGROUND_FACE_AUTHENTICATION"}) boolean hasEnrolledFaces(int sensorId, int userId, String opPackageName); // Return the LockoutTracker status for the specified user diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 58159c2b5693..ab98c94cba73 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -10162,7 +10162,9 @@ public final class Settings { public static final int HUB_MODE_TUTORIAL_STARTED = 1; /** - * Indicates that the user has completed the hub mode tutorial. + * Any value greater than or equal to this value is considered that the user has + * completed the hub mode tutorial. + * * One of the possible states for {@link #HUB_MODE_TUTORIAL_STATE}. * * @hide @@ -10181,8 +10183,11 @@ public final class Settings { /** * Defines the user's current state of navigating through the hub mode tutorial. - * The possible states are defined in {@link HubModeTutorialState}. + * Some possible states are defined in {@link HubModeTutorialState}. * + * Any value greater than or equal to {@link HUB_MODE_TUTORIAL_COMPLETED} indicates that + * the user has completed that version of the hub mode tutorial. And tutorial may be + * shown again when a new version becomes available. * @hide */ public static final String HUB_MODE_TUTORIAL_STATE = "hub_mode_tutorial_state"; diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index bf8e6135fd01..757978b71a01 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -1384,8 +1384,8 @@ public class LockPatternUtils { } public boolean isUserInLockdown(int userId) { - return getStrongAuthForUser(userId) - == StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; + return (getStrongAuthForUser(userId) + & StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN) != 0; } private static class WrappedCallback extends ICheckCredentialProgressCallback.Stub { diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index adea8000cebc..a3d5cf6e3ab5 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2956,6 +2956,16 @@ <permission android:name="android.permission.MANAGE_SENSORS" android:protectionLevel="signature" /> + <!-- Must be required by a DomainSelectionService to ensure that only the + system can bind to it. + <p>Protection level: signature + @SystemApi + @hide + @FlaggedApi("com.android.internal.telephony.flags.ap_domain_selection_enabled") + --> + <permission android:name="android.permission.BIND_DOMAIN_SELECTION_SERVICE" + android:protectionLevel="signature" /> + <!-- Must be required by an ImsService to ensure that only the system can bind to it. <p>Protection level: signature|privileged|vendorPrivileged @@ -6612,6 +6622,13 @@ <permission android:name="android.permission.USE_BIOMETRIC_INTERNAL" android:protectionLevel="signature" /> + <!-- Allows privileged apps to access the background face authentication. + @SystemApi + @FlaggedApi("android.hardware.biometrics.face_background_authentication") + @hide --> + <permission android:name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" + android:protectionLevel="signature|privileged" /> + <!-- Allows the system to control the BiometricDialog (SystemUI). Reserved for the system. @hide --> <permission android:name="android.permission.MANAGE_BIOMETRIC_DIALOG" android:protectionLevel="signature" /> diff --git a/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java b/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java index b843ad75ac0f..d816d0853ab6 100644 --- a/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java +++ b/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java @@ -18,6 +18,7 @@ package android.hardware.face; import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_HW_UNAVAILABLE; import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_UNABLE_TO_PROCESS; +import static android.hardware.biometrics.Flags.FLAG_FACE_BACKGROUND_AUTHENTICATION; import static com.google.common.truth.Truth.assertThat; @@ -35,12 +36,15 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.res.Resources; +import android.hardware.biometrics.BiometricPrompt; import android.os.CancellationSignal; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IBinder; import android.os.RemoteException; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; import com.android.internal.R; @@ -58,6 +62,7 @@ import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.Executor; @Presubmit @RunWith(MockitoJUnitRunner.class) @@ -78,6 +83,8 @@ public class FaceManagerTest { @Mock private FaceManager.AuthenticationCallback mAuthCallback; @Mock + private BiometricPrompt.AuthenticationCallback mBioAuthCallback; + @Mock private FaceManager.EnrollmentCallback mEnrollmentCallback; @Mock private FaceManager.FaceDetectionCallback mFaceDetectionCallback; @@ -91,13 +98,16 @@ public class FaceManagerTest { private TestLooper mLooper; private Handler mHandler; private FaceManager mFaceManager; + private Executor mExecutor; @Before public void setUp() throws Exception { mLooper = new TestLooper(); mHandler = new Handler(mLooper.getLooper()); + mExecutor = new HandlerExecutor(mHandler); when(mContext.getMainLooper()).thenReturn(mLooper.getLooper()); + when(mContext.getMainExecutor()).thenReturn(mExecutor); when(mContext.getOpPackageName()).thenReturn(PACKAGE_NAME); when(mContext.getAttributionTag()).thenReturn(ATTRIBUTION_TAG); when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo()); @@ -159,6 +169,19 @@ public class FaceManagerTest { } @Test + @RequiresFlagsEnabled(FLAG_FACE_BACKGROUND_AUTHENTICATION) + public void authenticateInBackground_errorWhenUnavailable() throws Exception { + when(mService.authenticateInBackground(any(), anyLong(), any(), any())) + .thenThrow(new RemoteException()); + + mFaceManager.authenticateInBackground(mExecutor, null, new CancellationSignal(), + mBioAuthCallback); + mLooper.dispatchAll(); + + verify(mBioAuthCallback).onAuthenticationError(eq(FACE_ERROR_HW_UNAVAILABLE), any()); + } + + @Test public void enrollment_errorWhenFaceEnrollmentExists() throws RemoteException { when(mResources.getInteger(R.integer.config_faceMaxTemplatesPerUser)).thenReturn(1); when(mService.getEnrolledFaces(anyInt(), anyInt(), anyString())) diff --git a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java index 0df5b0a4093e..dcaf67660ffa 100644 --- a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java +++ b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java @@ -19,8 +19,10 @@ package com.android.internal.util; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_MANAGED; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; -import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED; import static com.google.common.truth.Truth.assertThat; @@ -76,12 +78,15 @@ public class LockPatternUtilsTest { @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule(); + private ILockSettings mLockSettings; + private static final int USER_ID = 1; private static final int DEMO_USER_ID = 5; private LockPatternUtils mLockPatternUtils; private void configureTest(boolean isSecure, boolean isDemoUser, int deviceDemoMode) throws Exception { + mLockSettings = Mockito.mock(ILockSettings.class); final Context context = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); final MockContentResolver cr = new MockContentResolver(context); @@ -89,15 +94,14 @@ public class LockPatternUtilsTest { when(context.getContentResolver()).thenReturn(cr); Settings.Global.putInt(cr, Settings.Global.DEVICE_DEMO_MODE, deviceDemoMode); - final ILockSettings ils = Mockito.mock(ILockSettings.class); - when(ils.getCredentialType(DEMO_USER_ID)).thenReturn( + when(mLockSettings.getCredentialType(DEMO_USER_ID)).thenReturn( isSecure ? LockPatternUtils.CREDENTIAL_TYPE_PASSWORD : LockPatternUtils.CREDENTIAL_TYPE_NONE); - when(ils.getLong("lockscreen.password_type", PASSWORD_QUALITY_UNSPECIFIED, DEMO_USER_ID)) - .thenReturn((long) PASSWORD_QUALITY_MANAGED); + when(mLockSettings.getLong("lockscreen.password_type", PASSWORD_QUALITY_UNSPECIFIED, + DEMO_USER_ID)).thenReturn((long) PASSWORD_QUALITY_MANAGED); // TODO(b/63758238): stop spying the class under test mLockPatternUtils = spy(new LockPatternUtils(context)); - when(mLockPatternUtils.getLockSettings()).thenReturn(ils); + when(mLockPatternUtils.getLockSettings()).thenReturn(mLockSettings); doReturn(true).when(mLockPatternUtils).hasSecureLockScreen(); final UserInfo userInfo = Mockito.mock(UserInfo.class); @@ -108,6 +112,31 @@ public class LockPatternUtilsTest { } @Test + public void isUserInLockDown() throws Exception { + configureTest(true, false, 2); + + // GIVEN strong auth not required + when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn(STRONG_AUTH_NOT_REQUIRED); + + // THEN user isn't in lockdown + assertFalse(mLockPatternUtils.isUserInLockdown(USER_ID)); + + // GIVEN lockdown + when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn( + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); + + // THEN user is in lockdown + assertTrue(mLockPatternUtils.isUserInLockdown(USER_ID)); + + // GIVEN lockdown and lockout + when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn( + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN | STRONG_AUTH_REQUIRED_AFTER_LOCKOUT); + + // THEN user is in lockdown + assertTrue(mLockPatternUtils.isUserInLockdown(USER_ID)); + } + + @Test public void isLockScreenDisabled_isDemoUser_true() throws Exception { configureTest(false, true, 2); assertTrue(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID)); diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index f7c278c76838..607b4bdf71ca 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -121,6 +121,7 @@ applications that come with the platform <permission name="android.permission.BIND_CARRIER_MESSAGING_SERVICE"/> <permission name="android.permission.BIND_CARRIER_SERVICES"/> <permission name="android.permission.BIND_CELL_BROADCAST_SERVICE"/> + <permission name="android.permission.BIND_DOMAIN_SELECTION_SERVICE"/> <permission name="android.permission.BIND_IMS_SERVICE"/> <permission name="android.permission.BIND_SATELLITE_GATEWAY_SERVICE"/> <permission name="android.permission.BIND_SATELLITE_SERVICE"/> @@ -427,6 +428,7 @@ applications that come with the platform <permission name="android.permission.USE_BIOMETRIC" /> <permission name="android.permission.TEST_BIOMETRIC" /> <permission name="android.permission.MANAGE_BIOMETRIC_DIALOG" /> + <permission name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" /> <!-- Permissions required for CTS test - CtsContactsProviderTestCases --> <permission name="android.contacts.permission.MANAGE_SIM_ACCOUNTS" /> <!-- Permissions required for CTS test - CtsHdmiCecHostTestCases --> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 3a778c314606..c77004d4eb17 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -4333,6 +4333,12 @@ "group": "WM_DEBUG_ANIM", "at": "com\/android\/server\/wm\/WindowStateAnimator.java" }, + "1810872941": { + "message": "setWallpaperCropHints: non-existent wallpaper token: %s", + "level": "WARN", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "1820873642": { "message": "SyncGroup %d: Unfinished dependencies: %s", "level": "VERBOSE", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index ca283128891c..c25d41275f2b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -300,6 +300,9 @@ public class BubbleStackView extends FrameLayout */ private int mPointerIndexDown = -1; + /** Indicates whether bubbles should be reordered at the end of a gesture. */ + private boolean mShouldReorderBubblesAfterGestureCompletes = false; + @Nullable private BubblesNavBarGestureTracker mBubblesNavBarGestureTracker; @@ -708,6 +711,11 @@ public class BubbleStackView extends FrameLayout // Hide the stack after a delay, if needed. updateTemporarilyInvisibleAnimation(false /* hideImmediately */); + + if (mShouldReorderBubblesAfterGestureCompletes) { + mShouldReorderBubblesAfterGestureCompletes = false; + updateBubbleOrderInternal(mBubbleData.getBubbles(), true); + } } }; @@ -1928,7 +1936,18 @@ public class BubbleStackView extends FrameLayout /** * Update bubble order and pointer position. */ - public void updateBubbleOrder(List<Bubble> bubbles, boolean updatePointerPositoion) { + public void updateBubbleOrder(List<Bubble> bubbles, boolean updatePointerPosition) { + // Don't reorder bubbles in the middle of a gesture because that would remove bubbles from + // view hierarchy and will cancel all touch events. Instead wait until the gesture is + // finished and then reorder. + if (mIsGestureInProgress) { + mShouldReorderBubblesAfterGestureCompletes = true; + return; + } + updateBubbleOrderInternal(bubbles, updatePointerPosition); + } + + private void updateBubbleOrderInternal(List<Bubble> bubbles, boolean updatePointerPosition) { final Runnable reorder = () -> { for (int i = 0; i < bubbles.size(); i++) { Bubble bubble = bubbles.get(i); @@ -1939,13 +1958,13 @@ public class BubbleStackView extends FrameLayout reorder.run(); updateBadges(false /* setBadgeForCollapsedStack */); updateZOrder(); - } else if (!isExpansionAnimating()) { + } else { List<View> bubbleViews = bubbles.stream() .map(b -> b.getIconView()).collect(Collectors.toList()); mStackAnimationController.animateReorder(bubbleViews, reorder); } - if (updatePointerPositoion) { + if (updatePointerPosition) { updatePointerPosition(false /* forIme */); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java index 23a4e3956289..4ddc539eb220 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java @@ -127,6 +127,7 @@ public class CompatUILayoutTest extends ShellTestCase { @Test public void testOnClickForSizeCompatHint() { mWindowManager.mHasSizeCompat = true; + doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo); mWindowManager.createLayout(/* canShow= */ true); final LinearLayout sizeCompatHint = mLayout.findViewById(R.id.size_compat_hint); sizeCompatHint.performClick(); diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 62412df97043..17f25255fd4b 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -3084,11 +3084,7 @@ public final class MediaRouter2 { public void registerRouteCallback() { synchronized (mLock) { try { - if (mStub == null) { - MediaRouter2Stub stub = new MediaRouter2Stub(); - mMediaRouterService.registerRouter2(stub, mPackageName); - mStub = stub; - } + registerRouterStubIfNeededLocked(); if (updateDiscoveryPreferenceIfNeededLocked()) { mMediaRouterService.setDiscoveryRequestWithRouter2( @@ -3114,8 +3110,7 @@ public final class MediaRouter2 { } if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty()) { - mMediaRouterService.unregisterRouter2(mStub); - mStub = null; + unregisterRouterStubLocked(); } } catch (RemoteException ex) { Log.e(TAG, "unregisterRouteCallback: Unable to set discovery request.", ex); @@ -3132,11 +3127,7 @@ public final class MediaRouter2 { } mRouteListingPreference = preference; try { - if (mStub == null) { - MediaRouter2Stub stub = new MediaRouter2Stub(); - mMediaRouterService.registerRouter2(stub, mImpl.getPackageName()); - mStub = stub; - } + registerRouterStubIfNeededLocked(); mMediaRouterService.setRouteListingPreference(mStub, mRouteListingPreference); } catch (RemoteException ex) { ex.rethrowFromSystemServer(); @@ -3328,18 +3319,31 @@ public final class MediaRouter2 { obtainMessage(MediaRouter2::notifyStop, MediaRouter2.this, controller)); } - if (mRouteCallbackRecords.isEmpty() - && mNonSystemRoutingControllers.isEmpty() - && mStub != null) { + if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty()) { try { - mMediaRouterService.unregisterRouter2(mStub); + unregisterRouterStubLocked(); } catch (RemoteException ex) { ex.rethrowFromSystemServer(); } - mStub = null; } } } + @GuardedBy("mLock") + private void registerRouterStubIfNeededLocked() throws RemoteException { + if (mStub == null) { + MediaRouter2Stub stub = new MediaRouter2Stub(); + mMediaRouterService.registerRouter2(stub, mPackageName); + mStub = stub; + } + } + + @GuardedBy("mLock") + private void unregisterRouterStubLocked() throws RemoteException { + if (mStub != null) { + mMediaRouterService.unregisterRouter2(mStub); + mStub = null; + } + } } } diff --git a/packages/SettingsLib/Spa/build.gradle.kts b/packages/SettingsLib/Spa/build.gradle.kts index 8b136da04405..6f9556f91bd6 100644 --- a/packages/SettingsLib/Spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/build.gradle.kts @@ -29,7 +29,7 @@ val androidTop: String = File(rootDir, "../../../../..").canonicalPath allprojects { extra["androidTop"] = androidTop - extra["jetpackComposeVersion"] = "1.6.0-beta02" + extra["jetpackComposeVersion"] = "1.6.0-rc01" } subprojects { diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml index 9703c347859c..1f78a9c3ac07 100644 --- a/packages/SettingsLib/Spa/gradle/libs.versions.toml +++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml @@ -15,7 +15,7 @@ # [versions] -agp = "8.2.0" +agp = "8.2.1" compose-compiler = "1.5.1" dexmaker-mockito = "2.28.3" jvm = "17" diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts index 7eccfe5ed508..618dc37037aa 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/spa/build.gradle.kts @@ -57,13 +57,13 @@ dependencies { api("androidx.slice:slice-builders:1.1.0-alpha02") api("androidx.slice:slice-core:1.1.0-alpha02") api("androidx.slice:slice-view:1.1.0-alpha02") - api("androidx.compose.material3:material3:1.2.0-alpha12") + api("androidx.compose.material3:material3:1.2.0-beta02") api("androidx.compose.material:material-icons-extended:$jetpackComposeVersion") api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion") api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion") api("androidx.lifecycle:lifecycle-livedata-ktx") api("androidx.lifecycle:lifecycle-runtime-compose") - api("androidx.navigation:navigation-compose:2.7.4") + api("androidx.navigation:navigation-compose:2.7.6") api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha") api("com.google.android.material:material:1.7.0-alpha03") debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion") diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt index 5605485c4b84..da1ee77bcbfb 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt @@ -22,12 +22,14 @@ import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier import androidx.core.view.WindowCompat import androidx.navigation.NavBackStackEntry import androidx.navigation.NavGraph.Companion.findStartDestination @@ -133,6 +135,7 @@ private fun NavControllerWrapperImpl.NavContent( NavHost( navController = navController, startDestination = NullPageProvider.name, + modifier = Modifier.fillMaxSize(), ) { composable(NullPageProvider.name) {} for (spp in allProvider) { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt index 81bee5ec0b94..0281ab817340 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt @@ -86,7 +86,7 @@ fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): Settings ) } -object NullPageProvider : SettingsPageProvider { +internal object NullPageProvider : SettingsPageProvider { override val name = NULL_PAGE_NAME } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.kt index 192b12500978..93ad644bf5de 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.kt @@ -30,6 +30,7 @@ import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDeepLink import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import com.android.settingslib.spa.framework.common.NullPageProvider /** * Add the [Composable] to the [NavGraphBuilder] with animation @@ -49,11 +50,13 @@ internal fun NavGraphBuilder.animatedComposable( arguments = arguments, deepLinks = deepLinks, enterTransition = { - slideIntoContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Start, - animationSpec = slideInEffect, - initialOffset = offsetFunc, - ) + fadeIn(animationSpec = fadeInEffect) + if (initialState.destination.route != NullPageProvider.name) { + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = slideInEffect, + initialOffset = offsetFunc, + ) + fadeIn(animationSpec = fadeInEffect) + } else null }, exitTransition = { slideOutOfContainer( diff --git a/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/LiveDataTestUtil.kt b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/LiveDataTestUtil.kt deleted file mode 100644 index dddda5511c7b..000000000000 --- a/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/LiveDataTestUtil.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.testutils - -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException - -fun <T> LiveData<T>.getOrAwaitValue( - timeout: Long = 1, - timeUnit: TimeUnit = TimeUnit.SECONDS, - afterObserve: () -> Unit = {}, -): T? { - var data: T? = null - val latch = CountDownLatch(1) - val observer = Observer<T> { newData -> - data = newData - latch.countDown() - } - this.observeForever(observer) - - afterObserve() - - try { - // Don't wait indefinitely if the LiveData is not set. - if (!latch.await(timeout, timeUnit)) { - throw TimeoutException("LiveData value was never set.") - } - } finally { - this.removeObserver(observer) - } - - return data -} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlow.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlow.kt new file mode 100644 index 000000000000..367244aa2cfe --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlow.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spaprivileged.model.app + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import com.android.settingslib.spaprivileged.framework.common.asUser +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn + +/** + * Creates an instance of a cold Flow for permissions changed callback of given [app]. + * + * An initial element will be always sent. + */ +fun Context.permissionsChangedFlow(app: ApplicationInfo) = callbackFlow { + val userPackageManager = asUser(app.userHandle).packageManager + + val onPermissionsChangedListener = PackageManager.OnPermissionsChangedListener { uid -> + if (uid == app.uid) trySend(Unit) + } + userPackageManager.addOnPermissionsChangeListener(onPermissionsChangedListener) + trySend(Unit) + + awaitClose { + userPackageManager.removeOnPermissionsChangeListener(onPermissionsChangedListener) + } +}.conflate().flowOn(Dispatchers.Default) diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlowTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlowTest.kt new file mode 100644 index 000000000000..31522c1209f7 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlowTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spaprivileged.model.app + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull +import com.android.settingslib.spa.testutils.toListWithTimeout +import com.android.settingslib.spaprivileged.framework.common.asUser +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy + +@RunWith(AndroidJUnit4::class) +class PermissionsChangedFlowTest { + + private var onPermissionsChangedListener: PackageManager.OnPermissionsChangedListener? = null + + private val mockPackageManager = mock<PackageManager> { + on { addOnPermissionsChangeListener(any()) } doAnswer { + onPermissionsChangedListener = + it.arguments[0] as PackageManager.OnPermissionsChangedListener + } + } + + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { + on { asUser(APP.userHandle) } doReturn mock + on { packageManager } doReturn mockPackageManager + } + + @Test + fun permissionsChangedFlow_sendInitialValueTrue() = runBlocking { + val flow = context.permissionsChangedFlow(APP) + + assertThat(flow.firstWithTimeoutOrNull()).isNotNull() + } + + @Test + fun permissionsChangedFlow_collectChanged_getTwo() = runBlocking { + val listDeferred = async { + context.permissionsChangedFlow(APP).toListWithTimeout() + } + delay(100) + + onPermissionsChangedListener?.onPermissionsChanged(APP.uid) + + assertThat(listDeferred.await()).hasSize(2) + } + + private companion object { + val APP = ApplicationInfo().apply { + packageName = "package.name" + uid = 10000 + } + } +} diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index db1ca95fd83e..cc63996494a0 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -562,6 +562,9 @@ <!-- Permission required for CTS test - android.server.biometrics --> <uses-permission android:name="android.permission.MANAGE_BIOMETRIC_DIALOG" /> + <!-- Permission required for CTS test - android.server.biometrics --> + <uses-permission android:name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" /> + <!-- Permissions required for CTS test - NotificationManagerTest --> <uses-permission android:name="android.permission.MANAGE_NOTIFICATION_LISTENERS" /> diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt index f4ffb3c66219..2052e2c01410 100644 --- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.LifecycleOwner import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel +import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene @@ -52,6 +53,7 @@ object ComposeFacade : BaseComposeFacade { override fun setCommunalEditWidgetActivityContent( activity: ComponentActivity, viewModel: BaseCommunalViewModel, + widgetConfigurator: WidgetConfigurator, onOpenWidgetPicker: () -> Unit, onEditDone: () -> Unit, ) { diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt index 43745f9bf027..b607d596390d 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -37,6 +37,7 @@ import com.android.systemui.common.ui.compose.windowinsets.DisplayCutoutProvider import com.android.systemui.communal.ui.compose.CommunalContainer import com.android.systemui.communal.ui.compose.CommunalHub import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel +import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.people.ui.compose.PeopleScreen import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.compose.FooterActions @@ -69,6 +70,7 @@ object ComposeFacade : BaseComposeFacade { override fun setCommunalEditWidgetActivityContent( activity: ComponentActivity, viewModel: BaseCommunalViewModel, + widgetConfigurator: WidgetConfigurator, onOpenWidgetPicker: () -> Unit, onEditDone: () -> Unit, ) { @@ -77,6 +79,7 @@ object ComposeFacade : BaseComposeFacade { CommunalHub( viewModel = viewModel, onOpenWidgetPicker = onOpenWidgetPicker, + widgetConfigurator = widgetConfigurator, onEditDone = onEditDone, ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index a390305b144e..556a315894b8 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -20,7 +20,10 @@ import android.appwidget.AppWidgetHostView import android.os.Bundle import android.util.SizeF import android.widget.FrameLayout +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -47,6 +50,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.TouchApp import androidx.compose.material.icons.outlined.Widgets import androidx.compose.material3.Button @@ -54,8 +58,10 @@ import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonColors import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text @@ -66,6 +72,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -99,12 +106,15 @@ import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset import com.android.systemui.communal.ui.compose.extensions.observeTapsWithoutConsuming import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel +import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.res.R +import kotlinx.coroutines.launch @Composable fun CommunalHub( modifier: Modifier = Modifier, viewModel: BaseCommunalViewModel, + widgetConfigurator: WidgetConfigurator? = null, onOpenWidgetPicker: (() -> Unit)? = null, onEditDone: (() -> Unit)? = null, ) { @@ -116,7 +126,7 @@ fun CommunalHub( var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) } var isDraggingToRemove by remember { mutableStateOf(false) } val gridState = rememberLazyGridState() - val contentListState = rememberContentListState(communalContent, viewModel) + val contentListState = rememberContentListState(widgetConfigurator, communalContent, viewModel) val reorderingWidgets by viewModel.reorderingWidgets.collectAsState() val selectedIndex = viewModel.selectedIndex.collectAsState() val removeButtonEnabled by remember { @@ -167,7 +177,8 @@ fun CommunalHub( onOpenWidgetPicker = onOpenWidgetPicker, gridState = gridState, contentListState = contentListState, - selectedIndex = selectedIndex + selectedIndex = selectedIndex, + widgetConfigurator = widgetConfigurator, ) if (viewModel.isEditMode && onOpenWidgetPicker != null && onEditDone != null) { @@ -221,6 +232,7 @@ private fun BoxScope.CommunalHubLazyGrid( setGridCoordinates: (coordinates: LayoutCoordinates) -> Unit, updateDragPositionForRemove: (offset: Offset) -> Boolean, onOpenWidgetPicker: (() -> Unit)? = null, + widgetConfigurator: WidgetConfigurator?, ) { var gridModifier = Modifier.align(Alignment.CenterStart) var list = communalContent @@ -283,21 +295,24 @@ private fun BoxScope.CommunalHubLazyGrid( enabled = list[index] is CommunalContentModel.Widget, index = index, size = size - ) { _ -> + ) { isDragging -> CommunalContent( modifier = cardModifier, model = list[index], viewModel = viewModel, size = size, onOpenWidgetPicker = onOpenWidgetPicker, + selected = selected && !isDragging, + widgetConfigurator = widgetConfigurator, ) } } else { CommunalContent( - modifier = cardModifier, model = list[index], viewModel = viewModel, size = size, + selected = false, + modifier = cardModifier, ) } } @@ -453,11 +468,14 @@ private fun CommunalContent( model: CommunalContentModel, viewModel: BaseCommunalViewModel, size: SizeF, + selected: Boolean, modifier: Modifier = Modifier, onOpenWidgetPicker: (() -> Unit)? = null, + widgetConfigurator: WidgetConfigurator? = null, ) { when (model) { - is CommunalContentModel.Widget -> WidgetContent(viewModel, model, size, modifier) + is CommunalContentModel.Widget -> + WidgetContent(viewModel, model, size, selected, widgetConfigurator, modifier) is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(size) is CommunalContentModel.CtaTileInViewMode -> CtaTileInViewModeContent(viewModel, size, modifier) @@ -594,15 +612,17 @@ private fun WidgetContent( viewModel: BaseCommunalViewModel, model: CommunalContentModel.Widget, size: SizeF, + selected: Boolean, + widgetConfigurator: WidgetConfigurator?, modifier: Modifier = Modifier, ) { Box( modifier = modifier.height(size.height.dp), - contentAlignment = Alignment.Center, ) { val paddingInPx = with(LocalDensity.current) { CardOutlineWidth.toPx().toInt() } AndroidView( - modifier = modifier.allowGestures(allowed = !viewModel.isEditMode), + modifier = + modifier.align(Alignment.Center).allowGestures(allowed = !viewModel.isEditMode), factory = { context -> // The AppWidgetHostView will inherit the interaction handler from the // AppWidgetHost. So set the interaction handler here before creating the view, and @@ -624,6 +644,55 @@ private fun WidgetContent( // For reusing composition in lazy lists. onReset = {}, ) + if ( + viewModel is CommunalEditModeViewModel && + model.reconfigurable && + widgetConfigurator != null + ) { + WidgetConfigureButton( + visible = selected, + model = model, + widgetConfigurator = widgetConfigurator, + modifier = Modifier.align(Alignment.BottomEnd) + ) + } + } +} + +@Composable +fun WidgetConfigureButton( + visible: Boolean, + model: CommunalContentModel.Widget, + modifier: Modifier = Modifier, + widgetConfigurator: WidgetConfigurator, +) { + val colors = LocalAndroidColorScheme.current + val scope = rememberCoroutineScope() + + AnimatedVisibility( + visible = visible, + enter = fadeIn(), + exit = fadeOut(), + modifier = modifier.padding(16.dp), + ) { + FilledIconButton( + shape = RoundedCornerShape(16.dp), + modifier = Modifier.size(48.dp), + colors = + IconButtonColors( + containerColor = colors.primary, + contentColor = colors.onPrimary, + disabledContainerColor = Color.Transparent, + disabledContentColor = Color.Transparent + ), + onClick = { scope.launch { widgetConfigurator.configureWidget(model.appWidgetId) } }, + ) { + Icon( + imageVector = Icons.Outlined.Edit, + contentDescription = stringResource(id = R.string.edit_widget), + modifier = Modifier.padding(12.dp) + ) + } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt index 45f98b879dd7..67b79a06b4a0 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt @@ -22,16 +22,24 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.toMutableStateList import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel +import com.android.systemui.communal.widgets.WidgetConfigurator @Composable fun rememberContentListState( + widgetConfigurator: WidgetConfigurator?, communalContent: List<CommunalContentModel>, viewModel: BaseCommunalViewModel, ): ContentListState { return remember(communalContent) { ContentListState( communalContent, - viewModel::onAddWidget, + { componentName, priority -> + viewModel.onAddWidget( + componentName, + priority, + widgetConfigurator, + ) + }, viewModel::onDeleteWidget, viewModel::onReorderWidgets, ) diff --git a/packages/SystemUI/docs/imgs/ribbon.png b/packages/SystemUI/docs/imgs/ribbon.png Binary files differindex 9f5765232aed..3379d3d95025 100644 --- a/packages/SystemUI/docs/imgs/ribbon.png +++ b/packages/SystemUI/docs/imgs/ribbon.png diff --git a/packages/SystemUI/docs/scene.md b/packages/SystemUI/docs/scene.md index 3e4a1b4a05c7..105e4385bb25 100644 --- a/packages/SystemUI/docs/scene.md +++ b/packages/SystemUI/docs/scene.md @@ -20,14 +20,16 @@ over several dimensions: from one scene to another) are also pulled out and separated from the content of the UI. -In addition to the above, some of the **secondary goals** are: 4. Make -**customization easier**: by separating scenes to standalone pieces, it becomes -possible for variant owners and OEMs to exclude or replace certain scenes or to -add brand-new scenes. 5. **Enable modularization**: by separating scenes to -standalone pieces, it becomes possible to break down System UI into smaller -codebases, each one of which could be built on its own. Note: this isn't part of -the scene framework itself but is something that can be done more easily once -the scene framework is in place. +In addition to the above, some of the **secondary goals** are: + +4. Make **customization easier**: by separating scenes to standalone pieces, it +becomes possible for variant owners and OEMs to exclude or replace certain scenes +or to add brand-new scenes. +5. **Enable modularization**: by separating scenes to standalone pieces, it +becomes possible to break down System UI into smaller codebases, each one of +which could be built on its own. Note: this isn't part of the scene framework +itself but is something that can be done more easily once the scene framework +is in place. ## Terminology @@ -70,15 +72,17 @@ file evalutes to `true`. running: `console $ adb shell statusbar cmd migrate_keyguard_status_bar_view true` 3. Set a collection of **aconfig flags** to `true` by running the following - commands: `console $ adb shell device_config put systemui - com.android.systemui.scene_container true $ adb shell device_config put - systemui com.android.systemui.keyguard_bottom_area_refactor true $ adb shell - device_config put systemui - com.android.systemui.keyguard_shade_migration_nssl true $ adb shell - device_config put systemui com.android.systemui.media_in_scene_container - true` -4. **Restart** System UI by issuing the following command: `console $ adb shell - am crash com.android.systemui` + commands: + ```console + $ adb shell device_config put systemui com.android.systemui.scene_container true + $ adb shell device_config put systemui com.android.systemui.keyguard_bottom_area_refactor true + $ adb shell device_config put systemui com.android.systemui.keyguard_shade_migration_nssl true + $ adb shell device_config put systemui com.android.systemui.media_in_scene_container true + ``` +4. **Restart** System UI by issuing the following command: + ```console + $ adb shell am crash com.android.systemui + ``` 5. **Verify** that the scene framework was turned on. There are two ways to do this: @@ -94,15 +98,29 @@ file evalutes to `true`. $ adb shell cmd statusbar echo -b SceneFramework:verbose -# Look for the log statements from the framework: +### Checking if the framework is enabled + +Look for the log statements from the framework: -$ adb logcat -v time SceneFramework:* *:S ``` +```console +$ adb logcat -v time SceneFramework:* *:S +``` + +### Disabling the framework -To **disable** the framework, simply turn off the main aconfig flag: `console $ -adb shell device_config put systemui com.android.systemui.scene_container false` +To **disable** the framework, simply turn off the main aconfig flag: + +```console +$ adb shell device_config put systemui com.android.systemui.scene_container false +``` ## Defining a scene +By default, the framework ships with fully functional scenes as enumarated +[here](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneKey.kt). +Should a variant owner or OEM want to replace or add a new scene, they could +do so by defining their own scene. This section describes how to do that. + Each scene is defined as an implementation of the [`ComposableScene`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposableScene.kt) interface, which has three parts: 1. The `key` property returns the @@ -118,28 +136,28 @@ between any two scenes. The Scene Framework has other ways to define how the content of your UI changes with and throughout a transition to learn more please see the [Scene transition animations](#Scene-transition-animations) section -For example: ```kotlin @SysUISingleton class YourScene @Inject constructor( // -your dependencies here ) : ComposableScene { override val key = -SceneKey.YourScene +For example: -``` -override val destinationScenes: StateFlow<Map<UserAction, SceneModel>> = - MutableStateFlow<Map<UserAction, SceneModel>>( - mapOf( - // This is where scene navigation is defined, more on that below. - ) - ).asStateFlow() - -@Composable -override fun SceneScope.Content( - modifier: Modifier, -) { - // This is where the UI is defined using Jetpack Compose. +```kotlin +@SysUISingleton class YourScene @Inject constructor( /* your dependencies here */ ) : ComposableScene { + override val key = SceneKey.YourScene + + override val destinationScenes: StateFlow<Map<UserAction, SceneModel>> = + MutableStateFlow<Map<UserAction, SceneModel>>( + mapOf( + // This is where scene navigation is defined, more on that below. + ) + ).asStateFlow() + + @Composable + override fun SceneScope.Content( + modifier: Modifier, + ) { + // This is where the UI is defined using Jetpack Compose. + } } ``` -} ``` - ### Injecting scenes Scenes are injected into the Dagger dependency graph from the @@ -200,20 +218,21 @@ fun TransitionBuilder.lockscreenToShadeTransition() { } ``` -Going through the example code: * The `spec` is the animation that should be -invoked, in the example above, we use a `tween` animation with a duration of 500 -milliseconds * Then there's a series of function calls: `punchHole` applies a -clip mask to the `Scrim` element in the destination scene (in this case it's the -`Shade` scene) which has the position and size determined by the `bounds` -parameter and the shape passed into the `shape` parameter. This lets the -`Lockscreen` scene render "through" the `Shade` scene * The `translate` call -shifts the `Scrim` element to/from the `Top` edge of the scene container * The -first `fractionRange` wrapper tells the system to apply its contained functions +Going through the example code: + +* The `spec` is the animation that should be invoked, in the example above, we use a `tween` +animation with a duration of 500 milliseconds +* Then there's a series of function calls: `punchHole` applies a clip mask to the `Scrim` +element in the destination scene (in this case it's the `Shade` scene) which has the +position and size determined by the `bounds` parameter and the shape passed into the `shape` +parameter. This lets the `Lockscreen` scene render "through" the `Shade` scene +* The `translate` call shifts the `Scrim` element to/from the `Top` edge of the scene container +* The first `fractionRange` wrapper tells the system to apply its contained functions only during the first half of the transition. Inside of it, we see a `fade` of the `ScrimBackground` element and a `translate` o the `CollpasedGrid` element -to/from the `Top` edge * The second `fractionRange` only starts at the second -half of the transition (e.g. when the previous one ends) and applies a `fade` on -the `Notifications` element +to/from the `Top` edge +* The second `fractionRange` only starts at the second half of the transition (e.g. when +the previous one ends) and applies a `fade` on the `Notifications` element You can find the actual documentation for this API [here](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt). @@ -295,3 +314,52 @@ top-level Dagger module at this puts together the scenes from `SceneModule`, the configuration from `SceneContainerConfigModule`, and the startable from `SceneContainerStartableModule`. + +## Integration Notes + +### Relationship to Jetpack Compose + +The scene framework depends on Jetpack Compose; therefore, compiling System UI with +Jetpack Compose is required. However, because Jetpack Compose and Android Views +[interoperate](https://developer.android.com/jetpack/compose/migrate/interoperability-apis/views-in-compose), +the UI in each scene doesn't necessarily need to be a pure hierarchy of `@Composable` +functions; instead, it's acceptable to use an `AndroidView` somewhere in the +hierarchy of composable functions to include a `View` or `ViewGroup` subtree. + +#### Interoperability with Views +The scene framework comes with built-in functionality to animate the entire scene and/or +elements within the scene in-tandem with the actual scene transition progress. + +For example, as the user drags their finger down rom the top of the lockscreen, +the shade scene becomes visible and gradually expands, the amount of expansion tracks +the movement of the finger. + +That feature of the framework uses a custom `element(ElementKey)` Jetpack Compose +`Modifier` to refer to elements within a scene. +The transition builders then use the same `ElementKey` objects to refer to those elements +and describe how they animate in-tandem with scene transitions. Because this is a +Jetpack Compose `Modifier`, it means that, in order for an element in a scene to be +animated automatically by the framework, that element must be nested within a pure +`@Composable` hierarchy. The element itself is allowed to be a classic Android `View` +(nested within a Jetpack Compose `AndroidView`) but all ancestors must be `@Composable` +functions. + +### Notifications + +As of January 2024, the integration of notifications and heads-up notifications (HUNs) +into the scene framework follows an unusual pattern. We chose this pattern due to migration +risk and performance concerns but will eventually replace it with the more common element +placement pattern that all other elements are following. + +The special pattern for notifications is that, instead of the notification list +(`NotificationStackScrollLayout` or "NSSL", which also displays HUNs) being placed in the element +hierarchy within the scenes that display notifications, the NSSL (which continues to be an Android View) +"floats" above the scene container, rendering on top of everything. This is very similar to +how NSSL is integrated with the legacy shade, prior to the scene framework. + +In order to render the NSSL as if it's part of the organic hierarchy of elements within its +scenes, we control the NSSL's self-imposed effective bounds (e.g. position offsets, clip path, +size) from `@Composable` elements within the normal scene hierarchy. These special +"placeholder" elements can be found +[here](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt). + diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index 2a793ea70292..030d41ddd8fb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -45,6 +45,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants import com.android.systemui.classifier.FalsingA11yDelegate import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor @@ -819,6 +820,8 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { // While listening, going from the bouncer scene to the gone scene, does dismiss the // keyguard. + kosmos.fakeDeviceEntryRepository.setUnlocked(true) + runCurrent() sceneInteractor.changeScene(SceneModel(SceneKey.Gone, null), "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Transition( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt index fdb17c298e17..bb3429e72b35 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt @@ -18,6 +18,8 @@ package com.android.systemui.communal.data.repository import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProviderInfo +import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL +import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE import android.content.ComponentName import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -28,6 +30,9 @@ import com.android.systemui.communal.data.db.CommunalWidgetItem import com.android.systemui.communal.shared.CommunalWidgetHost import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.widgets.CommunalAppWidgetHost +import com.android.systemui.communal.widgets.WidgetConfigurator +import com.android.systemui.communal.widgets.widgetConfiguratorFail +import com.android.systemui.communal.widgets.widgetConfiguratorSuccess import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope @@ -71,12 +76,12 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var communalWidgetDao: CommunalWidgetDao - private val kosmos = testKosmos() + private lateinit var logBuffer: LogBuffer + private val kosmos = testKosmos() + private val testDispatcher = kosmos.testDispatcher private val testScope = kosmos.testScope - private lateinit var logBuffer: LogBuffer - private val fakeAllowlist = listOf( "com.android.fake/WidgetProviderA", @@ -157,10 +162,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 - whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true) + whenever(communalWidgetHost.getAppWidgetInfo(id)) + .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION) whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>())) .thenReturn(id) - underTest.addWidget(provider, priority) { true } + underTest.addWidget(provider, priority, kosmos.widgetConfiguratorSuccess) runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider) @@ -175,9 +181,10 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 - whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true) + whenever(communalWidgetHost.getAppWidgetInfo(id)) + .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION) whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id) - underTest.addWidget(provider, priority) { false } + underTest.addWidget(provider, priority, kosmos.widgetConfiguratorFail) runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider) @@ -193,9 +200,18 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 - whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true) + whenever(communalWidgetHost.getAppWidgetInfo(id)) + .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION) whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id) - underTest.addWidget(provider, priority) { throw IllegalStateException("some error") } + underTest.addWidget( + provider, + priority, + object : WidgetConfigurator { + override suspend fun configureWidget(appWidgetId: Int): Boolean { + throw IllegalStateException("some error") + } + } + ) runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider) @@ -211,19 +227,15 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 - whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(false) + whenever(communalWidgetHost.getAppWidgetInfo(id)) + .thenReturn(PROVIDER_INFO_CONFIGURATION_OPTIONAL) whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>())) .thenReturn(id) - var configured = false - underTest.addWidget(provider, priority) { - configured = true - true - } + underTest.addWidget(provider, priority, kosmos.widgetConfiguratorFail) runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider) verify(communalWidgetDao).addWidget(id, provider, priority) - assertThat(configured).isFalse() } @Test @@ -280,4 +292,15 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { private fun setAppWidgetIds(ids: List<Int>) { whenever(appWidgetHost.appWidgetIds).thenReturn(ids.toIntArray()) } + + private companion object { + val PROVIDER_INFO_REQUIRES_CONFIGURATION = + AppWidgetProviderInfo().apply { configure = ComponentName("test.pkg", "test.cmp") } + val PROVIDER_INFO_CONFIGURATION_OPTIONAL = + AppWidgetProviderInfo().apply { + configure = ComponentName("test.pkg", "test.cmp") + widgetFeatures = + WIDGET_FEATURE_CONFIGURATION_OPTIONAL or WIDGET_FEATURE_RECONFIGURABLE + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 09243e5282da..125ede413784 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -16,10 +16,7 @@ package com.android.systemui.communal.view.viewmodel -import android.app.Activity.RESULT_CANCELED -import android.app.Activity.RESULT_OK import android.app.smartspace.SmartspaceTarget -import android.content.ComponentName import android.provider.Settings import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -35,7 +32,6 @@ import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel -import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.kosmos.testScope @@ -46,7 +42,6 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -61,7 +56,6 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class CommunalEditModeViewModelTest : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost - @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost @Mock private lateinit var uiEventLogger: UiEventLogger private val kosmos = testKosmos() @@ -91,7 +85,6 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { underTest = CommunalEditModeViewModel( withDeps.communalInteractor, - appWidgetHost, mediaHost, uiEventLogger, ) @@ -154,55 +147,6 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { } @Test - fun addingWidgetTriggersConfiguration() = - testScope.runTest { - val provider = ComponentName("pkg.test", "testWidget") - val widgetToConfigure by collectLastValue(underTest.widgetsToConfigure) - assertThat(widgetToConfigure).isNull() - underTest.onAddWidget(componentName = provider, priority = 0) - assertThat(widgetToConfigure).isEqualTo(1) - } - - @Test - fun settingResultOkAddsWidget() = - testScope.runTest { - val provider = ComponentName("pkg.test", "testWidget") - val widgetAdded by collectLastValue(widgetRepository.widgetAdded) - assertThat(widgetAdded).isNull() - underTest.onAddWidget(componentName = provider, priority = 0) - assertThat(widgetAdded).isNull() - underTest.setConfigurationResult(RESULT_OK) - assertThat(widgetAdded).isEqualTo(1) - } - - @Test - fun settingResultCancelledDoesNotAddWidget() = - testScope.runTest { - val provider = ComponentName("pkg.test", "testWidget") - val widgetAdded by collectLastValue(widgetRepository.widgetAdded) - assertThat(widgetAdded).isNull() - underTest.onAddWidget(componentName = provider, priority = 0) - assertThat(widgetAdded).isNull() - underTest.setConfigurationResult(RESULT_CANCELED) - assertThat(widgetAdded).isNull() - } - - @Test(expected = IllegalStateException::class) - fun settingResultBeforeWidgetAddedThrowsException() { - underTest.setConfigurationResult(RESULT_OK) - } - - @Test(expected = IllegalStateException::class) - fun addingWidgetWhileConfigurationActiveFails() = - testScope.runTest { - val providerOne = ComponentName("pkg.test", "testWidget") - underTest.onAddWidget(componentName = providerOne, priority = 0) - runCurrent() - val providerTwo = ComponentName("pkg.test", "testWidget2") - underTest.onAddWidget(componentName = providerTwo, priority = 0) - } - - @Test fun reorderWidget_uiEventLogging_start() { underTest.onReorderWidgetStart() verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetConfigurationControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetConfigurationControllerTest.kt new file mode 100644 index 000000000000..55fafdff795f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetConfigurationControllerTest.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.widgets + +import android.app.Activity +import android.content.ActivityNotFoundException +import androidx.activity.ComponentActivity +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class WidgetConfigurationControllerTest : SysuiTestCase() { + @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost + @Mock private lateinit var ownerActivity: ComponentActivity + + private val kosmos = testKosmos() + + private lateinit var underTest: WidgetConfigurationController + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + underTest = + WidgetConfigurationController(ownerActivity, appWidgetHost, kosmos.testDispatcher) + } + + @Test + fun configurationFailsWhenActivityNotFound() = + with(kosmos) { + testScope.runTest { + whenever( + appWidgetHost.startAppWidgetConfigureActivityForResult( + eq(ownerActivity), + eq(123), + anyInt(), + eq(WidgetConfigurationController.REQUEST_CODE), + any() + ) + ) + .thenThrow(ActivityNotFoundException()) + + assertThat(underTest.configureWidget(123)).isFalse() + } + } + + @Test + fun configurationFails() = + with(kosmos) { + testScope.runTest { + val result = async { underTest.configureWidget(123) } + runCurrent() + assertThat(result.isCompleted).isFalse() + + underTest.setConfigurationResult(Activity.RESULT_CANCELED) + runCurrent() + + assertThat(result.await()).isFalse() + result.cancel() + } + } + + @Test + fun configurationSuccessful() = + with(kosmos) { + testScope.runTest { + val result = async { underTest.configureWidget(123) } + runCurrent() + assertThat(result.isCompleted).isFalse() + + underTest.setConfigurationResult(Activity.RESULT_OK) + runCurrent() + + assertThat(result.await()).isTrue() + result.cancel() + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt index 62d23152b77a..52305b1f5212 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt @@ -336,6 +336,7 @@ class DeviceEntryInteractorTest : SysuiTestCase() { kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.None ) + runCurrent() underTest.attemptDeviceEntry() @@ -353,6 +354,7 @@ class DeviceEntryInteractorTest : SysuiTestCase() { kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.None ) + runCurrent() underTest.attemptDeviceEntry() 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 new file mode 100644 index 000000000000..32943a19be28 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt @@ -0,0 +1,115 @@ +/* + * 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.deviceentry.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DeviceUnlockedInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val authenticationRepository = kosmos.fakeAuthenticationRepository + private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository + + val underTest = + DeviceUnlockedInteractor( + applicationScope = testScope.backgroundScope, + authenticationInteractor = kosmos.authenticationInteractor, + deviceEntryRepository = deviceEntryRepository, + ) + + @Test + fun isDeviceUnlocked_whenUnlockedAndAuthMethodIsNone_isTrue() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isDeviceUnlocked) + + deviceEntryRepository.setUnlocked(true) + authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + + assertThat(isUnlocked).isTrue() + } + + @Test + fun isDeviceUnlocked_whenUnlockedAndAuthMethodIsPin_isTrue() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isDeviceUnlocked) + + deviceEntryRepository.setUnlocked(true) + authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + + assertThat(isUnlocked).isTrue() + } + + @Test + fun isDeviceUnlocked_whenUnlockedAndAuthMethodIsSim_isFalse() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isDeviceUnlocked) + + deviceEntryRepository.setUnlocked(true) + authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Sim) + + assertThat(isUnlocked).isFalse() + } + + @Test + fun isDeviceUnlocked_whenLockedAndAuthMethodIsNone_isTrue() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isDeviceUnlocked) + + deviceEntryRepository.setUnlocked(false) + authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + + assertThat(isUnlocked).isTrue() + } + + @Test + fun isDeviceUnlocked_whenLockedAndAuthMethodIsPin_isFalse() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isDeviceUnlocked) + + deviceEntryRepository.setUnlocked(false) + authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + + assertThat(isUnlocked).isFalse() + } + + @Test + fun isDeviceUnlocked_whenLockedAndAuthMethodIsSim_isFalse() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isDeviceUnlocked) + + deviceEntryRepository.setUnlocked(false) + authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Sim) + + assertThat(isUnlocked).isFalse() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index d159986015a0..bf99a8687aa4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.kosmos.testScope import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.scene.data.repository.sceneContainerRepository @@ -33,6 +34,7 @@ import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -69,6 +71,27 @@ class SceneInteractorTest : SysuiTestCase() { } @Test + fun changeScene_toGoneWhenUnl_doesNotThrow() = + testScope.runTest { + val desiredScene by collectLastValue(underTest.desiredScene) + assertThat(desiredScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + + kosmos.fakeDeviceEntryRepository.setUnlocked(true) + runCurrent() + + underTest.changeScene(SceneModel(SceneKey.Gone), "reason") + assertThat(desiredScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test(expected = IllegalStateException::class) + fun changeScene_toGoneWhenStillLocked_throws() = + testScope.runTest { + kosmos.fakeDeviceEntryRepository.setUnlocked(false) + + underTest.changeScene(SceneModel(SceneKey.Gone), "reason") + } + + @Test fun onSceneChanged() = testScope.runTest { val desiredScene by collectLastValue(underTest.desiredScene) 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 4afa5f2a44b9..fc0df1288553 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 @@ -311,6 +311,10 @@ class SceneContainerStartableTest : SysuiTestCase() { SceneKey.QuickSettings, ) .forEachIndexed { index, sceneKey -> + if (sceneKey == SceneKey.Gone) { + kosmos.fakeDeviceEntryRepository.setUnlocked(true) + runCurrent() + } sceneInteractor.changeScene(SceneModel(sceneKey), "reason") runCurrent() verify(sysUiState, times(index)).commitUpdate(Display.DEFAULT_DISPLAY) @@ -420,6 +424,8 @@ class SceneContainerStartableTest : SysuiTestCase() { } // Changing to the Gone scene should report a successful unlock. + kosmos.fakeDeviceEntryRepository.setUnlocked(true) + runCurrent() sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason") runCurrent() verify(falsingCollector).onSuccessfulUnlock() @@ -613,6 +619,8 @@ class SceneContainerStartableTest : SysuiTestCase() { runCurrent() verify(falsingCollector).onBouncerShown() + kosmos.fakeDeviceEntryRepository.setUnlocked(true) + runCurrent() sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason") runCurrent() verify(falsingCollector, times(2)).onBouncerHidden() @@ -741,9 +749,15 @@ class SceneContainerStartableTest : SysuiTestCase() { "Lockscreen cannot be disabled while having a secure authentication method" } } + + check(initialSceneKey != SceneKey.Gone || isDeviceUnlocked) { + "Cannot start on the Gone scene and have the device be locked at the same time." + } + sceneContainerFlags.enabled = true kosmos.fakeDeviceEntryRepository.setUnlocked(isDeviceUnlocked) kosmos.fakeDeviceEntryRepository.setBypassEnabled(isBypassEnabled) + runCurrent() val transitionStateFlow = MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(SceneKey.Lockscreen) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index e9801652f060..251daffe2d91 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -157,9 +157,11 @@ class ShadeSceneViewModelTest : SysuiTestCase() { testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.None ) + runCurrent() sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason") sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone), "reason") diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 19895897ef31..7fa35dbb0575 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1089,6 +1089,8 @@ <string name="cta_label_to_open_widget_picker">Add more widgets</string> <!-- Text for the popup to be displayed after dismissing the CTA tile. [CHAR LIMIT=50] --> <string name="popup_on_dismiss_cta_tile_text">Long press to customize widgets</string> + <!-- Label for the button which configures widgets [CHAR LIMIT=NONE] --> + <string name="edit_widget">Edit widget</string> <!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] --> <string name="button_to_remove_widget">Remove</string> <!-- Text for the button that launches the hub mode widget picker. [CHAR LIMIT=50] --> diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDisplayListener.kt b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDisplayListener.kt index e9b637230ede..ca479f5e6b5f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDisplayListener.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDisplayListener.kt @@ -21,6 +21,7 @@ import android.hardware.display.DisplayManager import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.os.Handler import android.view.DisplayInfo +import com.android.app.tracing.traceSection import com.android.systemui.biometrics.BiometricDisplayListener.SensorType.Generic /** @@ -42,13 +43,15 @@ class BiometricDisplayListener( override fun onDisplayAdded(displayId: Int) {} override fun onDisplayRemoved(displayId: Int) {} override fun onDisplayChanged(displayId: Int) { - val rotationChanged = didRotationChange() + traceSection({ "BiometricDisplayListener($sensorType)#onDisplayChanged" }) { + val rotationChanged = didRotationChange() - when (sensorType) { - is SensorType.SideFingerprint -> onChanged() - else -> { - if (rotationChanged) { - onChanged() + when (sensorType) { + is SensorType.SideFingerprint -> onChanged() + else -> { + if (rotationChanged) { + onChanged() + } } } } @@ -82,8 +85,8 @@ class BiometricDisplayListener( * biometric prompt (and this object will likely change as multi-mode auth is added). */ sealed class SensorType { - object Generic : SensorType() - object UnderDisplayFingerprint : SensorType() + data object Generic : SensorType() + data object UnderDisplayFingerprint : SensorType() data class SideFingerprint( val properties: FingerprintSensorPropertiesInternal ) : SensorType() diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt index aaccbc1d2f9e..792a7efeb109 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt @@ -24,6 +24,7 @@ import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED import android.os.Handler import android.util.Size import android.view.DisplayInfo +import com.android.app.tracing.traceSection import com.android.internal.util.ArrayUtils import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.toDisplayRotation @@ -130,12 +131,17 @@ constructor( override fun onDisplayAdded(displayId: Int) {} override fun onDisplayChanged(displayId: Int) { - val displayInfo = getDisplayInfo() - trySendWithFailureLogging( - displayInfo, - TAG, - "Error sending displayInfo to $displayInfo" - ) + traceSection( + "DisplayStateRepository" + + ".currentRotationDisplayListener#onDisplayChanged" + ) { + val displayInfo = getDisplayInfo() + trySendWithFailureLogging( + displayInfo, + TAG, + "Error sending displayInfo to $displayInfo" + ) + } } } displayManager.registerDisplayListener( diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index 1c362e993509..3287ed4d4991 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -25,6 +25,7 @@ import com.android.systemui.communal.data.db.CommunalWidgetItem import com.android.systemui.communal.shared.CommunalWidgetHost import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.widgets.CommunalAppWidgetHost +import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -54,7 +55,7 @@ interface CommunalWidgetRepository { fun addWidget( provider: ComponentName, priority: Int, - configureWidget: suspend (id: Int) -> Boolean + configurator: WidgetConfigurator? = null ) {} /** Delete a widget by id from app widget service and the database. */ @@ -121,41 +122,48 @@ constructor( override fun addWidget( provider: ComponentName, priority: Int, - configureWidget: suspend (id: Int) -> Boolean + configurator: WidgetConfigurator? ) { applicationScope.launch(bgDispatcher) { val id = communalWidgetHost.allocateIdAndBindWidget(provider) - if (id != null) { - val configured = - if (communalWidgetHost.requiresConfiguration(id)) { - logger.i("Widget ${provider.flattenToString()} requires configuration.") - try { - configureWidget.invoke(id) - } catch (ex: Exception) { - // Cleanup the app widget id if an error happens during configuration. - logger.e("Error during widget configuration, cleaning up id $id", ex) - if (ex is CancellationException) { - appWidgetHost.deleteAppWidgetId(id) - // Re-throw cancellation to ensure the parent coroutine also gets - // cancelled. - throw ex - } else { - false - } + if (id == null) { + logger.e("Failed to allocate widget id to ${provider.flattenToString()}") + return@launch + } + val info = communalWidgetHost.getAppWidgetInfo(id) + val configured = + if ( + configurator != null && + info != null && + CommunalWidgetHost.requiresConfiguration(info) + ) { + logger.i("Widget ${provider.flattenToString()} requires configuration.") + try { + configurator.configureWidget(id) + } catch (ex: Exception) { + // Cleanup the app widget id if an error happens during configuration. + logger.e("Error during widget configuration, cleaning up id $id", ex) + if (ex is CancellationException) { + appWidgetHost.deleteAppWidgetId(id) + // Re-throw cancellation to ensure the parent coroutine also gets + // cancelled. + throw ex + } else { + false } - } else { - logger.i("Skipping configuration for ${provider.flattenToString()}") - true } - if (configured) { - communalWidgetDao.addWidget( - widgetId = id, - provider = provider, - priority = priority, - ) } else { - appWidgetHost.deleteAppWidgetId(id) + logger.i("Skipping configuration for ${provider.flattenToString()}") + true } + if (configured) { + communalWidgetDao.addWidget( + widgetId = id, + provider = provider, + priority = priority, + ) + } else { + appWidgetHost.deleteAppWidgetId(id) } logger.i("Added widget ${provider.flattenToString()} at position $priority.") } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 71523b9e750f..b6180cb03416 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -31,6 +31,7 @@ import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.EditWidgetsActivityStarter +import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -148,17 +149,12 @@ constructor( /** Dismiss the CTA tile from the hub in view mode. */ suspend fun dismissCtaTile() = communalPrefsRepository.setCtaDismissedForCurrentUser() - /** - * Add a widget at the specified position. - * - * @param configureWidget The callback to trigger if widget configuration is needed. Should - * return whether configuration was successful. - */ + /** Add a widget at the specified position. */ fun addWidget( componentName: ComponentName, priority: Int, - configureWidget: suspend (id: Int) -> Boolean - ) = widgetRepository.addWidget(componentName, priority, configureWidget) + configurator: WidgetConfigurator?, + ) = widgetRepository.addWidget(componentName, priority, configurator) /** Delete a widget by id. */ fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id) diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt index 0d52afd4fff5..acd6cb09e241 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.communal.domain.model import android.appwidget.AppWidgetProviderInfo +import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE import android.widget.RemoteViews import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.widgets.CommunalAppWidgetHost @@ -41,7 +42,7 @@ sealed interface CommunalContentModel { val createdTimestampMillis: Long } - class Widget( + data class Widget( val appWidgetId: Int, val providerInfo: AppWidgetProviderInfo, val appWidgetHost: CommunalAppWidgetHost, @@ -49,6 +50,12 @@ sealed interface CommunalContentModel { override val key = KEY.widget(appWidgetId) // Widget size is always half. override val size = CommunalContentSize.HALF + + /** Whether this widget can be reconfigured after it has already been added. */ + val reconfigurable: Boolean + get() = + (providerInfo.widgetFeatures and WIDGET_FEATURE_RECONFIGURABLE != 0) && + providerInfo.configure != null } /** A placeholder item representing a new widget being added */ diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt index 7fe37ccf0dc8..965c1e873279 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt @@ -17,6 +17,7 @@ package com.android.systemui.communal.shared import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE import android.content.ComponentName @@ -24,6 +25,7 @@ import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog +import com.android.systemui.util.kotlin.getOrNull import java.util.Optional import javax.inject.Inject @@ -40,6 +42,17 @@ constructor( ) { companion object { private const val TAG = "CommunalWidgetHost" + + /** Returns whether a particular widget requires configuration when it is first added. */ + fun requiresConfiguration(widgetInfo: AppWidgetProviderInfo): Boolean { + val featureFlags: Int = widgetInfo.widgetFeatures + // A widget's configuration is optional only if it's configuration is marked as optional + // AND it can be reconfigured later. + val configurationOptional = + (featureFlags and WIDGET_FEATURE_CONFIGURATION_OPTIONAL != 0 && + featureFlags and WIDGET_FEATURE_RECONFIGURABLE != 0) + return widgetInfo.configure != null && !configurationOptional + } } private val logger = Logger(logBuffer, TAG) @@ -66,22 +79,7 @@ constructor( return false } - /** - * Returns whether a particular widget requires configuration when it is first added. - * - * Must be called after the widget id has been bound. - */ - fun requiresConfiguration(widgetId: Int): Boolean { - if (appWidgetManager.isPresent) { - val widgetInfo = appWidgetManager.get().getAppWidgetInfo(widgetId) - val featureFlags: Int = widgetInfo.widgetFeatures - // A widget's configuration is optional only if it's configuration is marked as optional - // AND it can be reconfigured later. - val configurationOptional = - (featureFlags and WIDGET_FEATURE_CONFIGURATION_OPTIONAL != 0 && - featureFlags and WIDGET_FEATURE_RECONFIGURABLE != 0) - return widgetInfo.configure != null && !configurationOptional - } - return false + fun getAppWidgetInfo(widgetId: Int): AppWidgetProviderInfo? { + return appWidgetManager.getOrNull()?.getAppWidgetInfo(widgetId) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt index 75f9d809cc69..c5dac775c8a8 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt @@ -39,7 +39,7 @@ import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Named -/** Controller for managing the smartspace view on the dream */ +/** Controller for managing the smartspace view on the glanceable hub */ @SysUISingleton class CommunalSmartspaceController @Inject @@ -125,7 +125,7 @@ constructor( smartspaceManager.createSmartspaceSession( SmartspaceConfig.Builder(context, UI_SURFACE_GLANCEABLE_HUB).build() ) - Log.d(TAG, "Starting smartspace session for dream") + Log.d(TAG, "Starting smartspace session for communal") newSession.addOnTargetsAvailableListener(uiExecutor, sessionListener) this.session = newSession @@ -153,7 +153,7 @@ constructor( plugin?.registerSmartspaceEventNotifier(null) plugin?.onTargetsAvailable(emptyList()) - Log.d(TAG, "Ending smartspace session for dream") + Log.d(TAG, "Ending smartspace session for communal") } fun addListener(listener: SmartspaceTargetListener) { diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index 73bb0b0ec8a3..f1b16c555a6b 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -22,6 +22,7 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState +import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.media.controls.ui.MediaHost import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -64,16 +65,12 @@ abstract class BaseCommunalViewModel( /** * Called when a widget is added via drag and drop from the widget picker into the communal hub. */ - open fun onAddWidget(componentName: ComponentName, priority: Int) { - communalInteractor.addWidget(componentName, priority, ::configureWidget) - } - - /** - * Called when a widget needs to be configured, with the id of the widget. The return value - * should represent whether configuring the widget was successful. - */ - protected open suspend fun configureWidget(widgetId: Int): Boolean { - return true + open fun onAddWidget( + componentName: ComponentName, + priority: Int, + configurator: WidgetConfigurator? = null + ) { + communalInteractor.addWidget(componentName, priority, configurator) } /** A list of all the communal content to be displayed in the communal hub. */ diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index 317dd4040e7c..c69fa6f74c94 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -16,27 +16,17 @@ package com.android.systemui.communal.ui.viewmodel -import android.app.Activity -import android.app.Activity.RESULT_CANCELED -import android.app.Activity.RESULT_OK -import android.app.ActivityOptions -import android.content.ActivityNotFoundException -import android.content.ComponentName import android.widget.RemoteViews import com.android.internal.logging.UiEventLogger import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalUiEvent -import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.media.dagger.MediaModule -import com.android.systemui.util.nullableAtomicReference import javax.inject.Inject import javax.inject.Named -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map @@ -48,26 +38,9 @@ class CommunalEditModeViewModel @Inject constructor( private val communalInteractor: CommunalInteractor, - private val appWidgetHost: CommunalAppWidgetHost, @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, private val uiEventLogger: UiEventLogger, ) : BaseCommunalViewModel(communalInteractor, mediaHost) { - - private companion object { - private const val KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle" - private const val SPLASH_SCREEN_STYLE_EMPTY = 0 - } - - private val _widgetsToConfigure = MutableSharedFlow<Int>() - - /** - * Flow emitting ids of widgets which need to be configured. The consumer of this flow should - * trigger [startConfigurationActivity] to initiate configuration. - */ - val widgetsToConfigure: Flow<Int> = _widgetsToConfigure - - private var pendingConfiguration: CompletableDeferred<Int>? by nullableAtomicReference() - override val isEditMode = true // Only widgets are editable. The CTA tile comes last in the list and remains visible. @@ -92,57 +65,6 @@ constructor( return RemoteViews.InteractionHandler { _, _, _ -> false } } - override fun onAddWidget(componentName: ComponentName, priority: Int) { - if (pendingConfiguration != null) { - throw IllegalStateException( - "Cannot add $componentName widget while widget configuration is pending" - ) - } - super.onAddWidget(componentName, priority) - } - - fun startConfigurationActivity(activity: Activity, widgetId: Int, requestCode: Int) { - val options = - ActivityOptions.makeBasic().apply { - setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED - ) - } - val bundle = options.toBundle() - bundle.putInt(KEY_SPLASH_SCREEN_STYLE, SPLASH_SCREEN_STYLE_EMPTY) - try { - appWidgetHost.startAppWidgetConfigureActivityForResult( - activity, - widgetId, - 0, - // Use the widget id as the request code. - requestCode, - bundle - ) - } catch (e: ActivityNotFoundException) { - setConfigurationResult(RESULT_CANCELED) - } - } - - override suspend fun configureWidget(widgetId: Int): Boolean { - if (pendingConfiguration != null) { - throw IllegalStateException( - "Attempting to configure $widgetId while another configuration is already active" - ) - } - pendingConfiguration = CompletableDeferred() - _widgetsToConfigure.emit(widgetId) - val resultCode = pendingConfiguration?.await() ?: RESULT_CANCELED - pendingConfiguration = null - return resultCode == RESULT_OK - } - - /** Sets the result of widget configuration. */ - fun setConfigurationResult(resultCode: Int) { - pendingConfiguration?.complete(resultCode) - ?: throw IllegalStateException("No widget pending configuration") - } - override fun onReorderWidgetStart() { // Clear selection status setSelectedIndex(null) diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index 380ed61a556d..c7a14f9eefe1 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -27,15 +27,11 @@ import android.view.WindowInsets import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import com.android.internal.logging.UiEventLogger import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.compose.ComposeFacade.setCommunalEditWidgetActivityContent import javax.inject.Inject -import kotlinx.coroutines.launch /** An Activity for editing the widgets that appear in hub mode. */ class EditWidgetsActivity @@ -44,15 +40,17 @@ constructor( private val communalViewModel: CommunalEditModeViewModel, private var windowManagerService: IWindowManager? = null, private val uiEventLogger: UiEventLogger, + private val widgetConfiguratorFactory: WidgetConfigurationController.Factory ) : ComponentActivity() { companion object { private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag" private const val EXTRA_FILTER_STRATEGY = "filter_strategy" private const val FILTER_STRATEGY_GLANCEABLE_HUB = 1 - private const val REQUEST_CODE_CONFIGURE_WIDGET = 1 private const val TAG = "EditWidgetsActivity" } + private val widgetConfigurator by lazy { widgetConfiguratorFactory.create(this) } + private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(StartActivityForResult()) { result -> when (result.resultCode) { @@ -71,7 +69,7 @@ constructor( Intent.EXTRA_COMPONENT_NAME, ComponentName::class.java ) - ?.let { communalViewModel.onAddWidget(it, 0) } + ?.let { communalViewModel.onAddWidget(it, 0, widgetConfigurator) } ?: run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") } } } @@ -92,22 +90,10 @@ constructor( windowInsetsController?.hide(WindowInsets.Type.systemBars()) window.setDecorFitsSystemWindows(false) - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - // Start the configuration activity when new widgets are added. - communalViewModel.widgetsToConfigure.collect { widgetId -> - communalViewModel.startConfigurationActivity( - activity = this@EditWidgetsActivity, - widgetId = widgetId, - requestCode = REQUEST_CODE_CONFIGURE_WIDGET - ) - } - } - } - setCommunalEditWidgetActivityContent( activity = this, viewModel = communalViewModel, + widgetConfigurator = widgetConfigurator, onOpenWidgetPicker = { val intent = Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) } @@ -145,8 +131,8 @@ constructor( override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - if (requestCode == REQUEST_CODE_CONFIGURE_WIDGET) { - communalViewModel.setConfigurationResult(resultCode) + if (requestCode == WidgetConfigurationController.REQUEST_CODE) { + widgetConfigurator.setConfigurationResult(resultCode) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurationController.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurationController.kt new file mode 100644 index 000000000000..3e68479e717a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurationController.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.widgets + +import android.app.Activity +import android.app.ActivityOptions +import android.content.ActivityNotFoundException +import android.window.SplashScreen +import androidx.activity.ComponentActivity +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.util.nullableAtomicReference +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +/** + * Handles starting widget configuration activities and receiving the response to determine if + * configuration was successful. + */ +class WidgetConfigurationController +@AssistedInject +constructor( + @Assisted private val activity: ComponentActivity, + private val appWidgetHost: CommunalAppWidgetHost, + @Background private val bgDispatcher: CoroutineDispatcher +) : WidgetConfigurator { + @AssistedFactory + fun interface Factory { + fun create(activity: ComponentActivity): WidgetConfigurationController + } + + private var result: CompletableDeferred<Boolean>? by nullableAtomicReference() + + override suspend fun configureWidget(appWidgetId: Int): Boolean = + withContext(bgDispatcher) { + if (result != null) { + throw IllegalStateException("There is already a pending configuration") + } + result = CompletableDeferred() + val options = + ActivityOptions.makeBasic().apply { + pendingIntentBackgroundActivityStartMode = + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + splashScreenStyle = SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR + } + + try { + appWidgetHost.startAppWidgetConfigureActivityForResult( + activity, + appWidgetId, + 0, + REQUEST_CODE, + options.toBundle() + ) + } catch (e: ActivityNotFoundException) { + setConfigurationResult(Activity.RESULT_CANCELED) + } + val value = result?.await() ?: false + result = null + return@withContext value + } + + fun setConfigurationResult(resultCode: Int) { + result?.complete(resultCode == Activity.RESULT_OK) + } + + companion object { + const val REQUEST_CODE = 100 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurator.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurator.kt new file mode 100644 index 000000000000..916faa1b88ea --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurator.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.widgets + +/** Configurator which can be used to request a certain widget be reconfigured. */ +fun interface WidgetConfigurator { + /** Launch configuration for a widget, and return the result */ + suspend fun configureWidget(appWidgetId: Int): Boolean +} diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt index acbdecc5d514..641064becf24 100644 --- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt +++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.LifecycleOwner import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel +import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene @@ -64,6 +65,7 @@ interface BaseComposeFacade { fun setCommunalEditWidgetActivityContent( activity: ComponentActivity, viewModel: BaseCommunalViewModel, + widgetConfigurator: WidgetConfigurator, onOpenWidgetPicker: () -> Unit, onEditDone: () -> Unit, ) diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepository.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepository.kt index 5bb6eece9098..074b9ff3ca1f 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepository.kt @@ -17,23 +17,36 @@ package com.android.systemui.controls.panels import android.content.ComponentName +import android.os.UserHandle import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.flags.Flags +import kotlinx.coroutines.flow.Flow /** Stores user-selected preferred component. */ interface SelectedComponentRepository { + /** Returns current set preferred component for the specified user. */ + fun selectedComponentFlow(userHandle: UserHandle): Flow<SelectedComponent?> + /** - * Returns currently set preferred component, or null when nothing is set. Consider using - * [ControlsUiController.getPreferredSelectedItem] to get domain specific data + * Returns the current set preferred component for the specified user, or null when nothing is + * set. If no user is specified, the current user's preference is used. This method by default + * operates in the context of the current user unless another user is explicitly specified. + * Consider using [ControlsUiController.getPreferredSelectedItem] to get domain specific data. */ - fun getSelectedComponent(): SelectedComponent? + fun getSelectedComponent(userHandle: UserHandle = UserHandle.CURRENT): SelectedComponent? - /** Sets preferred component. Use [getSelectedComponent] to get current one */ + /** + * Sets the preferred component for the current user. Use [getSelectedComponent] to retrieve the + * currently set preferred component. This method applies to the current user's settings. + */ fun setSelectedComponent(selectedComponent: SelectedComponent) - /** Clears current preferred component. [getSelectedComponent] will return null afterwards */ + /** + * Clears the current user's preferred component. After this operation, [getSelectedComponent] + * will return null for the current user. + */ fun removeSelectedComponent() /** diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt index c9edd4ac7426..0baa81a12e4f 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt @@ -19,13 +19,24 @@ package com.android.systemui.controls.panels import android.content.ComponentName import android.content.Context import android.content.SharedPreferences +import android.os.UserHandle +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.flags.FeatureFlags import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) @SysUISingleton class SelectedComponentRepositoryImpl @Inject @@ -33,6 +44,8 @@ constructor( private val userFileManager: UserFileManager, private val userTracker: UserTracker, private val featureFlags: FeatureFlags, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application private val applicationScope: CoroutineScope ) : SelectedComponentRepository { private companion object { @@ -42,16 +55,42 @@ constructor( const val SHOULD_ADD_DEFAULT_PANEL = "should_add_default_panel" } - private val sharedPreferences: SharedPreferences - get() = - userFileManager.getSharedPreferences( - fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, - mode = Context.MODE_PRIVATE, - userId = userTracker.userId - ) + private fun getSharedPreferencesForUser(userId: Int): SharedPreferences { + return userFileManager.getSharedPreferences( + fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, + mode = Context.MODE_PRIVATE, + userId = userId + ) + } + + override fun selectedComponentFlow( + userHandle: UserHandle + ): Flow<SelectedComponentRepository.SelectedComponent?> { + return conflatedCallbackFlow { + val sharedPreferencesByUserId = getSharedPreferencesForUser(userHandle.identifier) + val listener = + SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + applicationScope.launch(bgDispatcher) { + if (key == PREF_COMPONENT) { + trySend(getSelectedComponent(userHandle)) + } + } + } + sharedPreferencesByUserId.registerOnSharedPreferenceChangeListener(listener) + send(getSelectedComponent(userHandle)) + awaitClose { + sharedPreferencesByUserId.unregisterOnSharedPreferenceChangeListener(listener) + } + } + .flowOn(bgDispatcher) + } - override fun getSelectedComponent(): SelectedComponentRepository.SelectedComponent? { - with(sharedPreferences) { + override fun getSelectedComponent( + userHandle: UserHandle + ): SelectedComponentRepository.SelectedComponent? { + val userId = + if (userHandle == UserHandle.CURRENT) userTracker.userId else userHandle.identifier + with(getSharedPreferencesForUser(userId)) { val componentString = getString(PREF_COMPONENT, null) ?: return null return SelectedComponentRepository.SelectedComponent( name = getString(PREF_STRUCTURE_OR_APP_NAME, "")!!, @@ -64,7 +103,7 @@ constructor( override fun setSelectedComponent( selectedComponent: SelectedComponentRepository.SelectedComponent ) { - sharedPreferences + getSharedPreferencesForUser(userTracker.userId) .edit() .putString(PREF_COMPONENT, selectedComponent.componentName?.flattenToString()) .putString(PREF_STRUCTURE_OR_APP_NAME, selectedComponent.name) @@ -73,7 +112,7 @@ constructor( } override fun removeSelectedComponent() { - sharedPreferences + getSharedPreferencesForUser(userTracker.userId) .edit() .remove(PREF_COMPONENT) .remove(PREF_STRUCTURE_OR_APP_NAME) @@ -82,9 +121,12 @@ constructor( } override fun shouldAddDefaultComponent(): Boolean = - sharedPreferences.getBoolean(SHOULD_ADD_DEFAULT_PANEL, true) + getSharedPreferencesForUser(userTracker.userId).getBoolean(SHOULD_ADD_DEFAULT_PANEL, true) override fun setShouldAddDefaultComponent(shouldAdd: Boolean) { - sharedPreferences.edit().putBoolean(SHOULD_ADD_DEFAULT_PANEL, shouldAdd).apply() + getSharedPreferencesForUser(userTracker.userId) + .edit() + .putBoolean(SHOULD_ADD_DEFAULT_PANEL, shouldAdd) + .apply() } } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt index 2680328d5d7e..09853578d3f1 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt @@ -61,6 +61,7 @@ constructor( deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository, trustRepository: TrustRepository, flags: SceneContainerFlags, + deviceUnlockedInteractor: DeviceUnlockedInteractor, ) { val enteringDeviceFromBiometricUnlock: Flow<BiometricUnlockSource> = repository.enteringDeviceFromBiometricUnlock @@ -74,19 +75,7 @@ constructor( * of this flow will always be `true`, even if the lockscreen is showing and still needs to be * dismissed by the user to proceed. */ - val isUnlocked: StateFlow<Boolean> = - combine( - repository.isUnlocked, - authenticationInteractor.authenticationMethod, - ) { isUnlocked, authenticationMethod -> - (!authenticationMethod.isSecure || isUnlocked) && - authenticationMethod != AuthenticationMethodModel.Sim - } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = false, - ) + val isUnlocked: StateFlow<Boolean> = deviceUnlockedInteractor.isDeviceUnlocked /** * Whether the device has been entered (i.e. the lockscreen has been dismissed, by any method). 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 new file mode 100644 index 000000000000..b0495fb8e819 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt @@ -0,0 +1,62 @@ +/* + * 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.deviceentry.domain.interactor + +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn + +@SysUISingleton +class DeviceUnlockedInteractor +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + authenticationInteractor: AuthenticationInteractor, + deviceEntryRepository: DeviceEntryRepository, +) { + + /** + * Whether the device is unlocked. + * + * A device that is not yet unlocked requires unlocking by completing an authentication + * challenge according to the current authentication method, unless in cases when the current + * authentication method is not "secure" (for example, None and Swipe); in such cases, the value + * of this flow will always be `true`, even if the lockscreen is showing and still needs to be + * dismissed by the user to proceed. + */ + val isDeviceUnlocked: StateFlow<Boolean> = + combine( + deviceEntryRepository.isUnlocked, + authenticationInteractor.authenticationMethod, + ) { isUnlocked, authenticationMethod -> + (!authenticationMethod.isSecure || isUnlocked) && + authenticationMethod != AuthenticationMethodModel.Sim + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java index 92f17f9db0f4..e098929c1b1d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java @@ -119,6 +119,8 @@ class TileAdapterDelegate extends AccessibilityDelegateCompat { info.removeAction(listOfActions.get(i)); } } + // We really don't want it to be clickable in this case. + info.setClickable(false); return; } @@ -126,6 +128,7 @@ class TileAdapterDelegate extends AccessibilityDelegateCompat { new AccessibilityNodeInfoCompat.AccessibilityActionCompat( AccessibilityNodeInfo.ACTION_CLICK, clickActionString); info.addAction(action); + info.setClickable(true); } private void maybeAddActionMoveToPosition( diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 0abde4d5c3f4..b3d2e0918db6 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -18,6 +18,7 @@ package com.android.systemui.scene.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.data.repository.SceneContainerRepository import com.android.systemui.scene.shared.logger.SceneLogger @@ -50,6 +51,7 @@ constructor( private val repository: SceneContainerRepository, private val powerInteractor: PowerInteractor, private val logger: SceneLogger, + private val deviceUnlockedInteractor: DeviceUnlockedInteractor, ) { /** @@ -222,6 +224,11 @@ constructor( loggingReason: String, log: (from: SceneKey, to: SceneKey, loggingReason: String) -> Unit, ) { + check(scene.key != SceneKey.Gone || deviceUnlockedInteractor.isDeviceUnlocked.value) { + "Cannot change to the Gone scene while the device is locked. Logging reason for scene" + + " change was: $loggingReason" + } + val currentSceneKey = desiredScene.value.key if (currentSceneKey == scene.key) { return diff --git a/packages/SystemUI/src/com/android/systemui/settings/DisplayTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/DisplayTrackerImpl.kt index 68cc483fbe80..2ef27a8df117 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/DisplayTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/DisplayTrackerImpl.kt @@ -23,6 +23,7 @@ import android.view.Display import androidx.annotation.GuardedBy import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread +import com.android.app.tracing.traceSection import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.Assert import java.lang.ref.WeakReference @@ -46,18 +47,30 @@ internal constructor( val displayChangedListener: DisplayManager.DisplayListener = object : DisplayManager.DisplayListener { override fun onDisplayAdded(displayId: Int) { - val list = synchronized(displayCallbacks) { displayCallbacks.toList() } - onDisplayAdded(displayId, list) + traceSection( + "DisplayTrackerImpl.displayChangedDisplayListener#onDisplayAdded", + ) { + val list = synchronized(displayCallbacks) { displayCallbacks.toList() } + onDisplayAdded(displayId, list) + } } override fun onDisplayRemoved(displayId: Int) { - val list = synchronized(displayCallbacks) { displayCallbacks.toList() } - onDisplayRemoved(displayId, list) + traceSection( + "DisplayTrackerImpl.displayChangedDisplayListener#onDisplayRemoved", + ) { + val list = synchronized(displayCallbacks) { displayCallbacks.toList() } + onDisplayRemoved(displayId, list) + } } override fun onDisplayChanged(displayId: Int) { - val list = synchronized(displayCallbacks) { displayCallbacks.toList() } - onDisplayChanged(displayId, list) + traceSection( + "DisplayTrackerImpl.displayChangedDisplayListener#onDisplayChanged", + ) { + val list = synchronized(displayCallbacks) { displayCallbacks.toList() } + onDisplayChanged(displayId, list) + } } } @@ -69,8 +82,12 @@ internal constructor( override fun onDisplayRemoved(displayId: Int) {} override fun onDisplayChanged(displayId: Int) { - val list = synchronized(brightnessCallbacks) { brightnessCallbacks.toList() } - onDisplayChanged(displayId, list) + traceSection( + "DisplayTrackerImpl.displayBrightnessChangedDisplayListener#onDisplayChanged", + ) { + val list = synchronized(brightnessCallbacks) { brightnessCallbacks.toList() } + onDisplayChanged(displayId, list) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt index fa2748c1dc77..6e2beb45f3f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt @@ -61,7 +61,8 @@ internal constructor( controller.setNotifStats(notifStats) if (NotificationIconContainerRefactor.isEnabled || FooterViewRefactor.isEnabled) { renderListInteractor.setRenderedList(entries) - } else { + } + if (!NotificationIconContainerRefactor.isEnabled) { notificationIconAreaController.updateNotificationIcons(entries) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt index 695f21569f3c..ab54bdad66c1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.domain.interactor import android.graphics.drawable.Icon import android.util.ArrayMap +import com.android.app.tracing.traceSection import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -44,10 +45,12 @@ constructor( * Sets the current list of rendered notification entries as displayed in the notification list. */ fun setRenderedList(entries: List<ListEntry>) { - repository.activeNotifications.update { existingModels -> - buildActiveNotificationsStore(existingModels, sectionStyleProvider) { - entries.forEach(::addListEntry) - setRankingsMap(entries) + traceSection("RenderNotificationListInteractor.setRenderedList") { + repository.activeNotifications.update { existingModels -> + buildActiveNotificationsStore(existingModels, sectionStyleProvider) { + entries.forEach(::addListEntry) + setRankingsMap(entries) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java index e57c0e7f1044..9bfc4ce49cc7 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java @@ -476,10 +476,15 @@ public class ImageWallpaper extends WallpaperService { @Override public void onDisplayChanged(int displayId) { - // changes the display in the color extractor - // the new display dimensions will be used in the next color computation - if (displayId == getDisplayContext().getDisplayId()) { - getDisplaySizeAndUpdateColorExtractor(); + Trace.beginSection("ImageWallpaper.CanvasEngine#onDisplayChanged"); + try { + // changes the display in the color extractor + // the new display dimensions will be used in the next color computation + if (displayId == getDisplayContext().getDisplayId()) { + getDisplaySizeAndUpdateColorExtractor(); + } + } finally { + Trace.endSection(); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt index a7677cca9f29..002862e949ba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt @@ -16,27 +16,55 @@ package com.android.systemui.controls.panels -class FakeSelectedComponentRepository : SelectedComponentRepository { +import android.os.UserHandle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow - private var selectedComponent: SelectedComponentRepository.SelectedComponent? = null +class FakeSelectedComponentRepository : SelectedComponentRepository { private var shouldAddDefaultPanel: Boolean = true + private val _selectedComponentFlows = + mutableMapOf<UserHandle, MutableStateFlow<SelectedComponentRepository.SelectedComponent?>>() + private var currentUserHandle: UserHandle = UserHandle.of(0) - override fun getSelectedComponent(): SelectedComponentRepository.SelectedComponent? = - selectedComponent + override fun selectedComponentFlow( + userHandle: UserHandle + ): Flow<SelectedComponentRepository.SelectedComponent?> { + // Return an existing flow for the user or create a new one + return _selectedComponentFlows.getOrPut(getUserHandle(userHandle)) { + MutableStateFlow(null) + } + } + + override fun getSelectedComponent( + userHandle: UserHandle + ): SelectedComponentRepository.SelectedComponent? { + return _selectedComponentFlows[getUserHandle(userHandle)]?.value + } override fun setSelectedComponent( selectedComponent: SelectedComponentRepository.SelectedComponent ) { - this.selectedComponent = selectedComponent + val flow = _selectedComponentFlows.getOrPut(currentUserHandle) { MutableStateFlow(null) } + flow.value = selectedComponent } override fun removeSelectedComponent() { - selectedComponent = null + _selectedComponentFlows[currentUserHandle]?.value = null } - override fun shouldAddDefaultComponent(): Boolean = shouldAddDefaultPanel override fun setShouldAddDefaultComponent(shouldAdd: Boolean) { shouldAddDefaultPanel = shouldAdd } + + fun setCurrentUserHandle(userHandle: UserHandle) { + currentUserHandle = userHandle + } + private fun getUserHandle(userHandle: UserHandle): UserHandle { + return if (userHandle == UserHandle.CURRENT) { + currentUserHandle + } else { + userHandle + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt index 6230ea7ecd31..b463adf40a91 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt @@ -18,28 +18,43 @@ package com.android.systemui.controls.panels import android.content.ComponentName import android.content.SharedPreferences +import android.os.UserHandle import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl +import com.android.systemui.testKosmos import com.android.systemui.util.FakeSharedPreferences -import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import java.io.File +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations +@ExperimentalCoroutinesApi @RunWith(AndroidTestingRunner::class) @SmallTest class SelectedComponentRepositoryTest : SysuiTestCase() { private companion object { + const val PREF_COMPONENT = "controls_component" + const val PREF_STRUCTURE_OR_APP_NAME = "controls_structure" + const val PREF_IS_PANEL = "controls_is_panel" + val PRIMARY_USER: UserHandle = UserHandle.of(0) + val SECONDARY_USER: UserHandle = UserHandle.of(12) val COMPONENT_A = SelectedComponentRepository.SelectedComponent( name = "a", @@ -53,24 +68,40 @@ class SelectedComponentRepositoryTest : SysuiTestCase() { isPanel = false, ) } + private lateinit var primaryUserSharedPref: FakeSharedPreferences + private lateinit var secondaryUserSharedPref: FakeSharedPreferences @Mock private lateinit var userTracker: UserTracker - @Mock private lateinit var userFileManager: UserFileManager + private lateinit var userFileManager: UserFileManager private val featureFlags = FakeFeatureFlags() - private val sharedPreferences: SharedPreferences = FakeSharedPreferences() - // under test private lateinit var repository: SelectedComponentRepository - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(userFileManager.getSharedPreferences(any(), any(), any())) - .thenReturn(sharedPreferences) + private val kosmos = testKosmos() - repository = SelectedComponentRepositoryImpl(userFileManager, userTracker, featureFlags) - } + @Before + fun setUp() = + with(kosmos) { + primaryUserSharedPref = FakeSharedPreferences() + secondaryUserSharedPref = FakeSharedPreferences() + MockitoAnnotations.initMocks(this@SelectedComponentRepositoryTest) + userFileManager = + FakeUserFileManager( + mapOf( + PRIMARY_USER.identifier to primaryUserSharedPref, + SECONDARY_USER.identifier to secondaryUserSharedPref + ) + ) + repository = + SelectedComponentRepositoryImpl( + userFileManager, + userTracker, + featureFlags, + bgDispatcher = testDispatcher, + applicationScope = applicationCoroutineScope + ) + } @Test fun testUnsetIsNull() { @@ -115,18 +146,10 @@ class SelectedComponentRepositoryTest : SysuiTestCase() { @Test fun testGetPreferredStructure_differentUserId() { - sharedPreferences.savePanel(COMPONENT_A) - whenever( - userFileManager.getSharedPreferences( - DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, - 0, - 1, - ) - ) - .thenReturn(FakeSharedPreferences().also { it.savePanel(COMPONENT_B) }) - + primaryUserSharedPref.savePanel(COMPONENT_A) + secondaryUserSharedPref.savePanel(COMPONENT_B) val previousPreferredStructure = repository.getSelectedComponent() - whenever(userTracker.userId).thenReturn(1) + whenever(userTracker.userId).thenReturn(SECONDARY_USER.identifier) val currentPreferredStructure = repository.getSelectedComponent() assertThat(previousPreferredStructure).isEqualTo(COMPONENT_A) @@ -134,11 +157,90 @@ class SelectedComponentRepositoryTest : SysuiTestCase() { assertThat(currentPreferredStructure).isEqualTo(COMPONENT_B) } + @Test + fun testEmitValueFromGetSelectedComponent() = + with(kosmos) { + testScope.runTest { + primaryUserSharedPref.savePanel(COMPONENT_A) + val emittedValue by collectLastValue(repository.selectedComponentFlow(PRIMARY_USER)) + assertThat(emittedValue).isEqualTo(COMPONENT_A) + } + } + + @Test + fun testEmitNullWhenRemoveSelectedComponentIsCalled() = + with(kosmos) { + testScope.runTest { + primaryUserSharedPref.savePanel(COMPONENT_A) + primaryUserSharedPref.removePanel() + val emittedValue by collectLastValue(repository.selectedComponentFlow(PRIMARY_USER)) + assertThat(emittedValue).isEqualTo(null) + } + } + + @Test + fun testChangeEmitValueChangeWhenANewComponentIsSelected() = + with(kosmos) { + testScope.runTest { + primaryUserSharedPref.savePanel(COMPONENT_A) + val emittedValue by collectLastValue(repository.selectedComponentFlow(PRIMARY_USER)) + advanceUntilIdle() + assertThat(emittedValue).isEqualTo(COMPONENT_A) + primaryUserSharedPref.savePanel(COMPONENT_B) + advanceUntilIdle() + assertThat(emittedValue).isEqualTo(COMPONENT_B) + } + } + + @Test + fun testDifferentUsersWithDifferentComponentSelected() = + with(kosmos) { + testScope.runTest { + primaryUserSharedPref.savePanel(COMPONENT_A) + secondaryUserSharedPref.savePanel(COMPONENT_B) + val primaryUserValue by + collectLastValue(repository.selectedComponentFlow(PRIMARY_USER)) + val secondaryUserValue by + collectLastValue(repository.selectedComponentFlow(SECONDARY_USER)) + assertThat(primaryUserValue).isEqualTo(COMPONENT_A) + assertThat(secondaryUserValue).isEqualTo(COMPONENT_B) + } + } + private fun SharedPreferences.savePanel(panel: SelectedComponentRepository.SelectedComponent) { edit() - .putString("controls_component", panel.componentName?.flattenToString()) - .putString("controls_structure", panel.name) - .putBoolean("controls_is_panel", panel.isPanel) + .putString(PREF_COMPONENT, panel.componentName?.flattenToString()) + .putString(PREF_STRUCTURE_OR_APP_NAME, panel.name) + .putBoolean(PREF_IS_PANEL, panel.isPanel) + .commit() + } + + private fun SharedPreferences.removePanel() { + edit() + .remove(PREF_COMPONENT) + .remove(PREF_STRUCTURE_OR_APP_NAME) + .remove(PREF_IS_PANEL) .commit() } + + private class FakeUserFileManager(private val sharedPrefs: Map<Int, SharedPreferences>) : + UserFileManager { + override fun getFile(fileName: String, userId: Int): File { + throw UnsupportedOperationException() + } + + override fun getSharedPreferences( + fileName: String, + mode: Int, + userId: Int + ): SharedPreferences { + if (fileName != DeviceControlsControllerImpl.PREFS_CONTROLS_FILE) { + throw IllegalArgumentException( + "Preference files must be " + + "$DeviceControlsControllerImpl.PREFS_CONTROLS_FILE" + ) + } + return sharedPrefs.getValue(userId) + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java index 6cad985c7b57..6e2f5db2eeda 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java @@ -31,8 +31,8 @@ import android.view.accessibility.AccessibilityNodeInfo; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.test.filters.SmallTest; -import com.android.systemui.res.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.res.R; import org.junit.Before; import org.junit.Test; @@ -136,6 +136,7 @@ public class TileAdapterDelegateTest extends SysuiTestCase { AccessibilityNodeInfoCompat.AccessibilityActionCompat action = getActionForId(mInfo, AccessibilityNodeInfo.ACTION_CLICK); assertThat(action.getLabel().toString()).contains(expectedString); + assertThat(mInfo.isClickable()).isTrue(); } @Test @@ -152,10 +153,11 @@ public class TileAdapterDelegateTest extends SysuiTestCase { AccessibilityNodeInfoCompat.AccessibilityActionCompat action = getActionForId(mInfo, AccessibilityNodeInfo.ACTION_CLICK); assertThat(action.getLabel().toString()).contains(expectedString); + assertThat(mInfo.isClickable()).isTrue(); } @Test - public void testNoClickAction() { + public void testNoClickActionAndNotClickable() { mView.setTag(mHolder); when(mHolder.canTakeAccessibleAction()).thenReturn(true); when(mHolder.canAdd()).thenReturn(false); @@ -167,6 +169,7 @@ public class TileAdapterDelegateTest extends SysuiTestCase { AccessibilityNodeInfoCompat.AccessibilityActionCompat action = getActionForId(mInfo, AccessibilityNodeInfo.ACTION_CLICK); assertThat(action).isNull(); + assertThat(mInfo.isClickable()).isFalse(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index a894f877fe3c..11da2376328a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -188,7 +188,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mTestScope.getBackgroundScope(), mKosmos.getFakeSceneContainerConfig()), powerInteractor, - mock(SceneLogger.class)); + mock(SceneLogger.class), + mKosmos.getDeviceUnlockedInteractor()); FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository(); FakeSceneContainerFlags sceneContainerFlags = new FakeSceneContainerFlags(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java index cbd4d2bfe377..8f46a37bf540 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java @@ -220,7 +220,8 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { mTestScope.getBackgroundScope(), mKosmos.getFakeSceneContainerConfig()), powerInteractor, - mock(SceneLogger.class)); + mock(SceneLogger.class), + mKosmos.getDeviceUnlockedInteractor()); FakeSceneContainerFlags sceneContainerFlags = new FakeSceneContainerFlags(); KeyguardInteractor keyguardInteractor = new KeyguardInteractor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt index 9b4a100a1d64..a1daff14eea6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt @@ -81,7 +81,7 @@ class StackCoordinatorTest : SysuiTestCase() { } @Test - @DisableFlags(NotificationIconContainerRefactor.FLAG_NAME, FooterViewRefactor.FLAG_NAME) + @DisableFlags(NotificationIconContainerRefactor.FLAG_NAME) fun testUpdateNotificationIcons() { afterRenderListListener.onAfterRenderList(listOf(entry), stackController) verify(notificationIconAreaController).updateNotificationIcons(eq(listOf(entry))) @@ -89,7 +89,14 @@ class StackCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(NotificationIconContainerRefactor.FLAG_NAME) - fun testSetRenderedListOnInteractor() { + fun testSetRenderedListOnInteractor_iconContainerFlagOn() { + afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + verify(renderListInteractor).setRenderedList(eq(listOf(entry))) + } + + @Test + @EnableFlags(FooterViewRefactor.FLAG_NAME) + fun testSetRenderedListOnInteractor_footerFlagOn() { afterRenderListListener.onAfterRenderList(listOf(entry), stackController) verify(renderListInteractor).setRenderedList(eq(listOf(entry))) } 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 1ed045ff6546..589f7c23ea13 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -419,7 +419,8 @@ public class BubblesTest extends SysuiTestCase { mTestScope.getBackgroundScope(), mKosmos.getFakeSceneContainerConfig()), powerInteractor, - mock(SceneLogger.class)); + mock(SceneLogger.class), + mKosmos.getDeviceUnlockedInteractor()); FakeSceneContainerFlags sceneContainerFlags = new FakeSceneContainerFlags(); KeyguardInteractor keyguardInteractor = new KeyguardInteractor( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index d0c2d4b82fb3..e25e8c099c21 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -1,11 +1,11 @@ package com.android.systemui.communal.data.repository +import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName import com.android.systemui.communal.shared.model.CommunalWidgetContentModel +import com.android.systemui.communal.widgets.WidgetConfigurator import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -14,8 +14,6 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : CommunalWidgetRepository { private val _communalWidgets = MutableStateFlow<List<CommunalWidgetContentModel>>(emptyList()) override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = _communalWidgets - private val _widgetAdded = MutableSharedFlow<Int>() - val widgetAdded: Flow<Int> = _widgetAdded private var nextWidgetId = 1 @@ -26,16 +24,22 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : override fun addWidget( provider: ComponentName, priority: Int, - configureWidget: suspend (id: Int) -> Boolean + configurator: WidgetConfigurator? ) { coroutineScope.launch { val id = nextWidgetId++ - if (configureWidget.invoke(id)) { - _widgetAdded.emit(id) + val providerInfo = AppWidgetProviderInfo().apply { this.provider = provider } + val configured = configurator?.configureWidget(id) ?: true + if (configured) { + onConfigured(id, providerInfo, priority) } } } + private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) { + _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority)) + } + private var isHostActive = false override fun updateAppWidgetHostActive(active: Boolean) { isHostActive = active diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/FakeWidgetConfigurator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/FakeWidgetConfigurator.kt new file mode 100644 index 000000000000..662303e27ba9 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/FakeWidgetConfigurator.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.widgets + +import kotlinx.coroutines.CompletableDeferred + +class FakeWidgetConfigurator(private val immediateResult: Boolean? = null) : WidgetConfigurator { + private val result: CompletableDeferred<Boolean> = + immediateResult?.let { CompletableDeferred(it) } ?: CompletableDeferred() + + override suspend fun configureWidget(appWidgetId: Int): Boolean = result.await() + + fun setResult(success: Boolean) { + result.complete(success) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/WidgetConfiguratorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/WidgetConfiguratorKosmos.kt new file mode 100644 index 000000000000..7bb86afd1d08 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/WidgetConfiguratorKosmos.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.widgets + +import com.android.systemui.kosmos.Kosmos + +/** A fake configurator which always successfully configures */ +val Kosmos.widgetConfiguratorSuccess: WidgetConfigurator by + Kosmos.Fixture { FakeWidgetConfigurator(true) } + +/** A fake configurator which always fails to configures */ +val Kosmos.widgetConfiguratorFail: WidgetConfigurator by + Kosmos.Fixture { FakeWidgetConfigurator(false) } + +/** A fake configurator whose result can be set programmatically in a test */ +val Kosmos.fakeWidgetConfigurator by Kosmos.Fixture { FakeWidgetConfigurator() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt index b600b50b8d2d..8dcdd3a9425c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt @@ -38,5 +38,6 @@ val Kosmos.deviceEntryInteractor by deviceEntryFaceAuthRepository = deviceEntryFaceAuthRepository, trustRepository = trustRepository, flags = sceneContainerFlags, + deviceUnlockedInteractor = deviceUnlockedInteractor, ) } 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 new file mode 100644 index 000000000000..df1cdc2f72cb --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt @@ -0,0 +1,31 @@ +/* + * 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.deviceentry.domain.interactor + +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.deviceentry.data.repository.deviceEntryRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.applicationCoroutineScope + +val Kosmos.deviceUnlockedInteractor by Fixture { + DeviceUnlockedInteractor( + applicationScope = applicationCoroutineScope, + authenticationInteractor = authenticationInteractor, + deviceEntryRepository = deviceEntryRepository, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 24670b12193a..cc0449d7e7bb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.kosmos import android.content.applicationContext @@ -24,6 +26,8 @@ import com.android.systemui.classifier.falsingCollector import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.jank.interactionJankMonitor import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository @@ -36,6 +40,7 @@ import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.statusbar.phone.screenOffAnimationController import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository import com.android.systemui.util.time.systemClock +import kotlinx.coroutines.ExperimentalCoroutinesApi /** Helper for using [Kosmos] from Java. */ @Deprecated("Please convert your test to Kotlin and use [Kosmos] directly.") @@ -65,6 +70,8 @@ class KosmosJavaAdapter( val sceneInteractor by lazy { kosmos.sceneInteractor } val falsingCollector by lazy { kosmos.falsingCollector } val powerInteractor by lazy { kosmos.powerInteractor } + val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor } + val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor } init { kosmos.applicationContext = testCase.context diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt index 998987602234..fc023758fdf6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.scene.domain.interactor +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.power.domain.interactor.powerInteractor @@ -29,5 +30,6 @@ val Kosmos.sceneInteractor by repository = sceneContainerRepository, powerInteractor = powerInteractor, logger = sceneLogger, + deviceUnlockedInteractor = deviceUnlockedInteractor, ) } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt index 82ea362e8049..bb91f9b8cf0b 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt @@ -20,6 +20,7 @@ import android.content.Context import android.hardware.display.DisplayManager import android.os.Handler import android.os.RemoteException +import android.os.Trace import com.android.systemui.unfold.util.CallbackController import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -86,14 +87,19 @@ constructor( private inner class RotationDisplayListener : DisplayManager.DisplayListener { override fun onDisplayChanged(displayId: Int) { - val display = context.display ?: return - - if (displayId == display.displayId) { - val currentRotation = display.rotation - if (lastRotation == null || lastRotation != currentRotation) { - listeners.forEach { it.onRotationChanged(currentRotation) } - lastRotation = currentRotation + Trace.beginSection("RotationChangeProvider.RotationDisplayListener#onDisplayChanged") + try { + val display = context.display ?: return + + if (displayId == display.displayId) { + val currentRotation = display.rotation + if (lastRotation == null || lastRotation != currentRotation) { + listeners.forEach { it.onRotationChanged(currentRotation) } + lastRotation = currentRotation + } } + } finally { + Trace.endSection() } } diff --git a/packages/WallpaperBackup/AndroidManifest.xml b/packages/WallpaperBackup/AndroidManifest.xml index c548101080b4..3ce97cdb4c07 100644 --- a/packages/WallpaperBackup/AndroidManifest.xml +++ b/packages/WallpaperBackup/AndroidManifest.xml @@ -17,11 +17,19 @@ */ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.wallpaperbackup" - android:sharedUserId="android.uid.system" > + package="com.android.wallpaperbackup" > + + <uses-permission android:name="android.permission.READ_WALLPAPER_INTERNAL" /> + <uses-permission android:name="android.permission.SET_WALLPAPER_COMPONENT" /> + <uses-permission android:name="android.permission.SET_WALLPAPER" /> + + <queries> + <intent> + <action android:name="android.service.wallpaper.WallpaperService" /> + </intent> + </queries> <application android:allowClearUserData="false" - android:process="system" android:killAfterRestore="false" android:allowBackup="true" android:backupInForeground="true" diff --git a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java index 98421a9e1d3e..f31eb44f23f5 100644 --- a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java +++ b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java @@ -18,11 +18,13 @@ package com.android.wallpaperbackup; import static android.app.WallpaperManager.FLAG_LOCK; import static android.app.WallpaperManager.FLAG_SYSTEM; +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_INELIGIBLE; import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_NO_METADATA; import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_NO_WALLPAPER; import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_QUOTA_EXCEEDED; +import static com.android.window.flags.Flags.multiCrop; import android.app.AppGlobals; import android.app.WallpaperManager; @@ -43,7 +45,9 @@ import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; +import android.util.Pair; import android.util.Slog; +import android.util.SparseArray; import android.util.Xml; import com.android.internal.annotations.VisibleForTesting; @@ -55,6 +59,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.util.List; /** * Backs up and restores wallpaper and metadata related to it. @@ -432,6 +437,27 @@ public class WallpaperBackupAgent extends BackupAgent { private void restoreFromStage(File stage, File info, String hintTag, int which) throws IOException { if (stage.exists()) { + if (multiCrop()) { + SparseArray<Rect> cropHints = parseCropHints(info, hintTag); + if (cropHints != null) { + Slog.i(TAG, "Got restored wallpaper; applying which=" + which + + "; cropHints = " + cropHints); + try (FileInputStream in = new FileInputStream(stage)) { + mWallpaperManager.setStreamWithCrops(in, cropHints, true, which); + } + // And log the success + if ((which & FLAG_SYSTEM) > 0) { + mEventLogger.onSystemImageWallpaperRestored(); + } + if ((which & FLAG_LOCK) > 0) { + mEventLogger.onLockImageWallpaperRestored(); + } + } else { + logRestoreError(which, ERROR_NO_METADATA); + } + return; + } + // Parse the restored info file to find the crop hint. Note that this currently // relies on a priori knowledge of the wallpaper info file schema. Rect cropHint = parseCropHint(info, hintTag); @@ -501,6 +527,47 @@ public class WallpaperBackupAgent extends BackupAgent { return cropHint; } + private SparseArray<Rect> parseCropHints(File wallpaperInfo, String sectionTag) { + SparseArray<Rect> cropHints = new SparseArray<>(); + try (FileInputStream stream = new FileInputStream(wallpaperInfo)) { + XmlPullParser parser = Xml.resolvePullParser(stream); + int type; + do { + type = parser.next(); + if (type != XmlPullParser.START_TAG) continue; + String tag = parser.getName(); + if (!sectionTag.equals(tag)) continue; + for (Pair<Integer, String> pair: List.of( + new Pair<>(WallpaperManager.PORTRAIT, "Portrait"), + new Pair<>(WallpaperManager.LANDSCAPE, "Landscape"), + new Pair<>(WallpaperManager.SQUARE_PORTRAIT, "SquarePortrait"), + new Pair<>(WallpaperManager.SQUARE_LANDSCAPE, "SquareLandscape"))) { + Rect cropHint = new Rect( + getAttributeInt(parser, "cropLeft" + pair.second, 0), + getAttributeInt(parser, "cropTop" + pair.second, 0), + getAttributeInt(parser, "cropRight" + pair.second, 0), + getAttributeInt(parser, "cropBottom" + pair.second, 0)); + if (!cropHint.isEmpty()) cropHints.put(pair.first, cropHint); + } + if (cropHints.size() == 0) { + // migration case: the crops per screen orientation are not specified. + // use the old attributes to restore the crop for one screen orientation. + Rect cropHint = new Rect( + getAttributeInt(parser, "cropLeft", 0), + getAttributeInt(parser, "cropTop", 0), + getAttributeInt(parser, "cropRight", 0), + getAttributeInt(parser, "cropBottom", 0)); + if (!cropHint.isEmpty()) cropHints.put(ORIENTATION_UNKNOWN, cropHint); + } + } while (type != XmlPullParser.END_DOCUMENT); + } catch (Exception e) { + // Whoops; can't process the info file at all. Report failure. + Slog.w(TAG, "Failed to parse restored crops: " + e.getMessage()); + return null; + } + return cropHints; + } + private ComponentName parseWallpaperComponent(File wallpaperInfo, String sectionTag) { ComponentName name = null; try (FileInputStream stream = new FileInputStream(wallpaperInfo)) { diff --git a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java index fb521e11c083..053ed779a27a 100644 --- a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java +++ b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java @@ -31,6 +31,7 @@ import static com.android.wallpaperbackup.WallpaperEventLogger.WALLPAPER_IMG_LOC import static com.android.wallpaperbackup.WallpaperEventLogger.WALLPAPER_IMG_SYSTEM; import static com.android.wallpaperbackup.WallpaperEventLogger.WALLPAPER_LIVE_LOCK; import static com.android.wallpaperbackup.WallpaperEventLogger.WALLPAPER_LIVE_SYSTEM; +import static com.android.window.flags.Flags.multiCrop; import static com.google.common.truth.Truth.assertThat; @@ -60,6 +61,7 @@ import android.os.FileUtils; import android.os.ParcelFileDescriptor; import android.os.UserHandle; import android.service.wallpaper.WallpaperService; +import android.util.SparseArray; import android.util.Xml; import androidx.test.InstrumentationRegistry; @@ -711,8 +713,13 @@ public class WallpaperBackupAgentTest { @Test public void testOnRestore_throwsException_logsErrors() throws Exception { - when(mWallpaperManager.setStream(any(), any(), anyBoolean(), anyInt())).thenThrow( - new RuntimeException()); + if (!multiCrop()) { + when(mWallpaperManager.setStream(any(), any(), anyBoolean(), anyInt())) + .thenThrow(new RuntimeException()); + } else { + when(mWallpaperManager.setStreamWithCrops(any(), any(SparseArray.class), anyBoolean(), + anyInt())).thenThrow(new RuntimeException()); + } mockStagedWallpaperFile(SYSTEM_WALLPAPER_STAGE); mockStagedWallpaperFile(WALLPAPER_INFO_STAGE); mWallpaperBackupAgent.onCreate(USER_HANDLE, BackupAnnotations.BackupDestination.CLOUD, diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java index 2fbc3cd24d65..055970819e28 100644 --- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java +++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java @@ -140,8 +140,8 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // Widget-related data handled as part of this restore operation private byte[] mWidgetData; - // Number of apps restored in this pass - private int mCount; + // Number of apps attempted to restore in this pass + private int mRestoreAttemptedAppsCount; // When did we start? private long mStartRealtime; @@ -574,7 +574,8 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { Slog.v(TAG, "No more packages; finishing restore"); } int millis = (int) (SystemClock.elapsedRealtime() - mStartRealtime); - EventLog.writeEvent(EventLogTags.RESTORE_SUCCESS, mCount, millis); + EventLog.writeEvent( + EventLogTags.RESTORE_SUCCESS, mRestoreAttemptedAppsCount, millis); nextState = UnifiedRestoreState.FINAL; return; } @@ -582,7 +583,8 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { if (DEBUG) { Slog.i(TAG, "Next restore package: " + mRestoreDescription); } - sendOnRestorePackage(pkgName); + mRestoreAttemptedAppsCount++; + sendOnRestorePackage(mRestoreAttemptedAppsCount, pkgName); Metadata metaInfo = mPmAgent.getRestoredMetadata(pkgName); if (metaInfo == null) { @@ -810,7 +812,6 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // And then finally start the restore on this agent try { initiateOneRestore(mCurrentPackage, metaInfo.versionCode); - ++mCount; } catch (Exception e) { Slog.e(TAG, "Error when attempting restore: " + e.toString()); Bundle monitoringExtras = addRestoreOperationTypeToEvent(/* extras= */ null); @@ -1331,13 +1332,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { } // Tell the observer we're done - if (mObserver != null) { - try { - mObserver.restoreFinished(mStatus); - } catch (RemoteException e) { - Slog.d(TAG, "Restore observer died at restoreFinished"); - } - } + sendEndRestore(); // Clear any ongoing session timeout. backupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_SESSION_TIMEOUT); @@ -1651,10 +1646,10 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { } } - void sendOnRestorePackage(String name) { + void sendOnRestorePackage(int index, String name) { if (mObserver != null) { try { - mObserver.onUpdate(mCount, name); + mObserver.onUpdate(index, name); } catch (RemoteException e) { Slog.d(TAG, "Restore observer died in onUpdate"); mObserver = null; diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index 53c0184c6e29..e5a8c4fa22b7 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -101,7 +101,7 @@ class CompanionDeviceShellCommand extends ShellCommand { String deviceProfile = getNextArg(); final MacAddress macAddress = MacAddress.fromString(address); mService.createNewAssociation(userId, packageName, macAddress, - null, deviceProfile, false); + /* displayName= */ deviceProfile, deviceProfile, false); } break; diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index ef61498e16af..b6e114087f30 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -56,6 +56,7 @@ import android.os.Process; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.UserHandle; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.ExceptionUtils; import android.util.Slog; @@ -112,7 +113,7 @@ public class VirtualDeviceManagerService extends SystemService { Context.DEVICE_ID_DEFAULT + 1); @GuardedBy("mVirtualDeviceManagerLock") - private List<AssociationInfo> mActiveAssociations = new ArrayList<>(); + private ArrayMap<String, AssociationInfo> mActiveAssociations = new ArrayMap<>(); private final CompanionDeviceManager.OnAssociationsChangedListener mCdmAssociationListener = new CompanionDeviceManager.OnAssociationsChangedListener() { @@ -343,34 +344,29 @@ public class VirtualDeviceManagerService extends SystemService { @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) void onCdmAssociationsChanged(List<AssociationInfo> associations) { - List<AssociationInfo> vdmAssociations = new ArrayList<>(); - Set<Integer> activeAssociationIds = new HashSet<>(); + ArrayMap<String, AssociationInfo> vdmAssociations = new ArrayMap<>(); for (int i = 0; i < associations.size(); ++i) { AssociationInfo association = associations.get(i); - if (VIRTUAL_DEVICE_COMPANION_DEVICE_PROFILES.contains(association.getDeviceProfile())) { - vdmAssociations.add(association); - activeAssociationIds.add(association.getId()); + if (VIRTUAL_DEVICE_COMPANION_DEVICE_PROFILES.contains(association.getDeviceProfile()) + && !association.isRevoked()) { + String persistentId = + VirtualDeviceImpl.createPersistentDeviceId(association.getId()); + vdmAssociations.put(persistentId, association); } } Set<VirtualDeviceImpl> virtualDevicesToRemove = new HashSet<>(); - Set<String> removedPersistentDeviceIds = new HashSet<>(); + Set<String> removedPersistentDeviceIds; synchronized (mVirtualDeviceManagerLock) { - for (int i = 0; i < mActiveAssociations.size(); ++i) { - AssociationInfo associationInfo = mActiveAssociations.get(i); - if (!activeAssociationIds.contains(associationInfo.getId())) { - removedPersistentDeviceIds.add( - VirtualDeviceImpl.createPersistentDeviceId(associationInfo.getId())); - } - } + removedPersistentDeviceIds = mActiveAssociations.keySet(); + removedPersistentDeviceIds.removeAll(vdmAssociations.keySet()); + mActiveAssociations = vdmAssociations; for (int i = 0; i < mVirtualDevices.size(); i++) { VirtualDeviceImpl virtualDevice = mVirtualDevices.valueAt(i); - if (!activeAssociationIds.contains(virtualDevice.getAssociationId())) { + if (removedPersistentDeviceIds.contains(virtualDevice.getPersistentDeviceId())) { virtualDevicesToRemove.add(virtualDevice); } } - - mActiveAssociations = vdmAssociations; } for (VirtualDeviceImpl virtualDevice : virtualDevicesToRemove) { @@ -577,6 +573,16 @@ public class VirtualDeviceManagerService extends SystemService { return Context.DEVICE_ID_DEFAULT; } + @Override // Binder call + public @Nullable CharSequence getDisplayNameForPersistentDeviceId( + @NonNull String persistentDeviceId) { + final AssociationInfo associationInfo; + synchronized (mVirtualDeviceManagerLock) { + associationInfo = mActiveAssociations.get(persistentDeviceId); + } + return associationInfo == null ? null : associationInfo.getDisplayName(); + } + // Binder call @Override public boolean isValidVirtualDeviceId(int deviceId) { @@ -885,15 +891,9 @@ public class VirtualDeviceManagerService extends SystemService { @Override public @NonNull Set<String> getAllPersistentDeviceIds() { - Set<String> persistentIds = new ArraySet<>(); synchronized (mVirtualDeviceManagerLock) { - for (int i = 0; i < mActiveAssociations.size(); ++i) { - AssociationInfo associationInfo = mActiveAssociations.get(i); - persistentIds.add( - VirtualDeviceImpl.createPersistentDeviceId(associationInfo.getId())); - } + return Set.copyOf(mActiveAssociations.keySet()); } - return persistentIds; } @Override diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java index 0f964bb75944..73f3999f40ce 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java @@ -143,7 +143,11 @@ public class FaceService extends SystemService { return proto.getBytes(); } - @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) + @android.annotation.EnforcePermission( + anyOf = { + android.Manifest.permission.USE_BIOMETRIC_INTERNAL, + android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION + }) @Override // Binder call public List<FaceSensorPropertiesInternal> getSensorPropertiesInternal( String opPackageName) { @@ -285,6 +289,29 @@ public class FaceService extends SystemService { restricted, statsClient, isKeyguard); } + @android.annotation.EnforcePermission( + android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION) + @Override // Binder call + public long authenticateInBackground(final IBinder token, final long operationId, + final IFaceServiceReceiver receiver, final FaceAuthenticateOptions options) { + // TODO(b/152413782): If the sensor supports face detect and the device is encrypted or + // lockdown, something wrong happened. See similar path in FingerprintService. + + super.authenticateInBackground_enforcePermission(); + + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); + if (provider == null) { + Slog.w(TAG, "Null provider for authenticate"); + return -1; + } + options.setSensorId(provider.first); + + return provider.second.scheduleAuthenticate(token, operationId, + 0 /* cookie */, new ClientMonitorCallbackConverter(receiver), options, + false /* restricted */, BiometricsProtoEnums.CLIENT_UNKNOWN /* statsClient */, + true /* allowBackgroundAuthentication */); + } + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override // Binder call public long detectFace(final IBinder token, @@ -548,7 +575,11 @@ public class FaceService extends SystemService { return provider.getEnrolledFaces(sensorId, userId); } - @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) + @android.annotation.EnforcePermission( + anyOf = { + android.Manifest.permission.USE_BIOMETRIC_INTERNAL, + android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION + }) @Override // Binder call public boolean hasEnrolledFaces(int sensorId, int userId, String opPackageName) { super.hasEnrolledFaces_enforcePermission(); diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java index 1f59b57d2da9..c260f10b61a6 100644 --- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java +++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java @@ -17,6 +17,7 @@ package com.android.server.grammaticalinflection; import android.annotation.Nullable; +import android.content.res.Configuration; /** * System-server internal interface to the {@link android.app.GrammaticalInflectionManager}. @@ -37,5 +38,14 @@ public abstract class GrammaticalInflectionManagerInternal { * at the time this is called, to be referenced later when the app is installed. */ public abstract void stageAndApplyRestoredPayload(byte[] payload, int userId); + + /** + * Get the current system grammatical gender of privileged application. + * + * @return the value of grammatical gender + * + * @see Configuration#getGrammaticalGender + */ + public abstract @Configuration.GrammaticalGender int getSystemGrammaticalGender(int userId); } diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java index 68848a2ad426..6eb7e9559b8d 100644 --- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java +++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java @@ -19,9 +19,12 @@ package com.android.server.grammaticalinflection; import static android.app.Flags.systemTermsOfAddressEnabled; import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED; +import static com.android.server.grammaticalinflection.GrammaticalInflectionUtils.checkSystemGrammaticalGenderPermission; + import android.annotation.Nullable; import android.app.GrammaticalInflectionManager; import android.app.IGrammaticalInflectionManager; +import android.content.AttributionSource; import android.content.Context; import android.content.pm.PackageManagerInternal; import android.os.Binder; @@ -30,6 +33,7 @@ import android.os.Process; import android.os.ResultReceiver; import android.os.ShellCallback; import android.os.SystemProperties; +import android.permission.PermissionManager; import android.util.AtomicFile; import android.util.Log; import android.util.SparseIntArray; @@ -75,6 +79,8 @@ public class GrammaticalInflectionService extends SystemService { private PackageManagerInternal mPackageManagerInternal; private GrammaticalInflectionService.GrammaticalInflectionBinderService mBinderService; + private PermissionManager mPermissionManager; + private Context mContext; /** * Initializes the system service. @@ -88,11 +94,12 @@ public class GrammaticalInflectionService extends SystemService { */ public GrammaticalInflectionService(Context context) { super(context); + mContext = context; mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class); mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); - mBackupHelper = new GrammaticalInflectionBackupHelper( - this, context.getPackageManager()); + mBackupHelper = new GrammaticalInflectionBackupHelper(this, context.getPackageManager()); mBinderService = new GrammaticalInflectionBinderService(); + mPermissionManager = context.getSystemService(PermissionManager.class); } @Override @@ -112,7 +119,7 @@ public class GrammaticalInflectionService extends SystemService { } @Override - public void setSystemWideGrammaticalGender(int userId, int grammaticalGender) { + public void setSystemWideGrammaticalGender(int grammaticalGender, int userId) { checkCallerIsSystem(); checkSystemTermsOfAddressIsEnabled(); GrammaticalInflectionService.this.setSystemWideGrammaticalGender(grammaticalGender, @@ -120,16 +127,17 @@ public class GrammaticalInflectionService extends SystemService { } @Override - public int getSystemGrammaticalGender(int userId) { + public int getSystemGrammaticalGender(AttributionSource attributionSource, int userId) { checkSystemTermsOfAddressIsEnabled(); - return GrammaticalInflectionService.this.getSystemGrammaticalGender(userId); + return GrammaticalInflectionService.this.getSystemGrammaticalGender(attributionSource, + userId); } @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver) { - (new GrammaticalInflectionShellCommand(mBinderService)) + (new GrammaticalInflectionShellCommand(mBinderService, mContext.getAttributionSource())) .exec(this, in, out, err, args, callback, resultReceiver); } }; @@ -148,6 +156,13 @@ public class GrammaticalInflectionService extends SystemService { public void stageAndApplyRestoredPayload(byte[] payload, int userId) { mBackupHelper.stageAndApplyRestoredPayload(payload, userId); } + + @Override + public int getSystemGrammaticalGender(int userId) { + checkCallerIsSystem(); + return GrammaticalInflectionService.this.getSystemGrammaticalGender( + mContext.getAttributionSource(), userId); + } } protected int getApplicationGrammaticalGender(String appPackageName, int userId) { @@ -211,9 +226,24 @@ public class GrammaticalInflectionService extends SystemService { } } - // TODO(b/298591009): Add a new AppOp value for the apps that want to access the grammatical - // gender. - public int getSystemGrammaticalGender(int userId) { + public int getSystemGrammaticalGender(AttributionSource attributionSource, int userId) { + String packageName = attributionSource.getPackageName(); + if (packageName == null) { + Log.d(TAG, "Package name is null."); + return GRAMMATICAL_GENDER_NOT_SPECIFIED; + } + + int callingUid = Binder.getCallingUid(); + if (mPackageManagerInternal.getPackageUid(packageName, 0, userId) != callingUid) { + Log.d(TAG, + "Package " + packageName + " does not belong to the calling uid " + callingUid); + return GRAMMATICAL_GENDER_NOT_SPECIFIED; + } + + if (!checkSystemGrammaticalGenderPermission(mPermissionManager, attributionSource)) { + return GRAMMATICAL_GENDER_NOT_SPECIFIED; + } + synchronized (mLock) { final File file = getGrammaticalGenderFile(userId); if (!file.exists()) { diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java index d22372860ead..cdda69278b2c 100644 --- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java +++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java @@ -21,6 +21,7 @@ import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED import android.app.ActivityManager; import android.app.GrammaticalInflectionManager; import android.app.IGrammaticalInflectionManager; +import android.content.AttributionSource; import android.content.res.Configuration; import android.os.RemoteException; import android.os.ShellCommand; @@ -44,9 +45,12 @@ class GrammaticalInflectionShellCommand extends ShellCommand { } private final IGrammaticalInflectionManager mBinderService; + private AttributionSource mAttributionSource; - GrammaticalInflectionShellCommand(IGrammaticalInflectionManager grammaticalInflectionManager) { + GrammaticalInflectionShellCommand(IGrammaticalInflectionManager grammaticalInflectionManager, + AttributionSource attributionSource) { mBinderService = grammaticalInflectionManager; + mAttributionSource = attributionSource; } @Override @@ -115,7 +119,7 @@ class GrammaticalInflectionShellCommand extends ShellCommand { } while (true); try { - mBinderService.setSystemWideGrammaticalGender(userId, grammaticalGender); + mBinderService.setSystemWideGrammaticalGender(grammaticalGender, userId); } catch (RemoteException e) { getOutPrintWriter().println("Remote Exception: " + e); } @@ -141,7 +145,8 @@ class GrammaticalInflectionShellCommand extends ShellCommand { } while (true); try { - int grammaticalGender = mBinderService.getSystemGrammaticalGender(userId); + int grammaticalGender = mBinderService.getSystemGrammaticalGender(mAttributionSource, + userId); getOutPrintWriter().println(GRAMMATICAL_GENDER_MAP.get(grammaticalGender)); } catch (RemoteException e) { getOutPrintWriter().println("Remote Exception: " + e); diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionUtils.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionUtils.java new file mode 100644 index 000000000000..f056561f20e0 --- /dev/null +++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionUtils.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.grammaticalinflection; + +import static android.Manifest.permission.READ_SYSTEM_GRAMMATICAL_GENDER; + +import android.annotation.NonNull; +import android.content.AttributionSource; +import android.permission.PermissionManager; +import android.util.Log; + +/** + * Utility methods for system grammatical gender. + */ +public class GrammaticalInflectionUtils { + + private static final String TAG = "GrammaticalInflectionUtils"; + + public static boolean checkSystemGrammaticalGenderPermission( + @NonNull PermissionManager permissionManager, + @NonNull AttributionSource attributionSource) { + int permissionCheckResult = permissionManager.checkPermissionForDataDelivery( + READ_SYSTEM_GRAMMATICAL_GENDER, + attributionSource, /* message= */ null); + if (permissionCheckResult != PermissionManager.PERMISSION_GRANTED) { + Log.v(TAG, "AttributionSource: " + attributionSource + + " does not have READ_SYSTEM_GRAMMATICAL_GENDER permission."); + return false; + } + return true; + } +} diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 2ae040a69583..308d441fb871 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -69,6 +69,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OF import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_ON; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; import static android.app.Flags.lifetimeExtensionRefactor; +import static android.app.NotificationManager.zenModeFromInterruptionFilter; import static android.app.StatusBarManager.ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED; import static android.app.StatusBarManager.EXTRA_KM_PRIVATE_NOTIFS_ALLOWED; import static android.content.Context.BIND_ALLOW_WHITELIST_MANAGEMENT; @@ -5311,18 +5312,41 @@ public class NotificationManagerService extends SystemService { @Override public void requestInterruptionFilterFromListener(INotificationListener token, int interruptionFilter) throws RemoteException { - final int callingUid = Binder.getCallingUid(); - final boolean isSystemOrSystemUi = isCallerSystemOrSystemUi(); - final long identity = Binder.clearCallingIdentity(); - try { + if (android.app.Flags.modesApi()) { + final int callingUid = Binder.getCallingUid(); + ManagedServiceInfo info; synchronized (mNotificationLock) { - final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); - mZenModeHelper.requestFromListener(info.component, interruptionFilter, - callingUid, isSystemOrSystemUi); - updateInterruptionFilterLocked(); + info = mListeners.checkServiceTokenLocked(token); + } + + final int zenMode = zenModeFromInterruptionFilter(interruptionFilter, -1); + if (zenMode == -1) return; + if (!canManageGlobalZenPolicy(info.component.getPackageName(), callingUid)) { + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule( + info.component.getPackageName(), callingUid, zenMode); + } else { + int origin = computeZenOrigin(/* fromUser= */ false); + Binder.withCleanCallingIdentity(() -> { + mZenModeHelper.setManualZenMode(zenMode, /* conditionId= */ null, origin, + "listener:" + info.component.flattenToShortString(), + /* caller= */ info.component.getPackageName(), + callingUid); + }); + } + } else { + final int callingUid = Binder.getCallingUid(); + final boolean isSystemOrSystemUi = isCallerSystemOrSystemUi(); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mNotificationLock) { + final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); + mZenModeHelper.requestFromListener(info.component, interruptionFilter, + callingUid, isSystemOrSystemUi); + updateInterruptionFilterLocked(); + } + } finally { + Binder.restoreCallingIdentity(identity); } - } finally { - Binder.restoreCallingIdentity(identity); } } @@ -5358,10 +5382,10 @@ public class NotificationManagerService extends SystemService { @Override public void setZenMode(int mode, Uri conditionId, String reason, boolean fromUser) { enforceSystemOrSystemUI("INotificationManager.setZenMode"); - final int callingUid = Binder.getCallingUid(); - final long identity = Binder.clearCallingIdentity(); enforceUserOriginOnlyFromSystem(fromUser, "setZenMode"); + final int callingUid = Binder.getCallingUid(); + final long identity = Binder.clearCallingIdentity(); try { mZenModeHelper.setManualZenMode(mode, conditionId, computeZenOrigin(fromUser), reason, /* caller= */ null, callingUid); @@ -5554,7 +5578,7 @@ public class NotificationManagerService extends SystemService { @Override public void setInterruptionFilter(String pkg, int filter, boolean fromUser) { enforcePolicyAccess(pkg, "setInterruptionFilter"); - final int zen = NotificationManager.zenModeFromInterruptionFilter(filter, -1); + final int zen = zenModeFromInterruptionFilter(filter, -1); if (zen == -1) throw new IllegalArgumentException("Invalid filter: " + filter); final int callingUid = Binder.getCallingUid(); enforceUserOriginOnlyFromSystem(fromUser, "setInterruptionFilter"); diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 93ffd974bb80..153af13b61b4 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -359,6 +359,7 @@ public class ZenModeHelper { return NotificationManager.zenModeToInterruptionFilter(mZenMode); } + // TODO: b/310620812 - Remove when MODES_API is inlined (no more callers). public void requestFromListener(ComponentName name, int filter, int callingUid, boolean fromSystemOrSystemUi) { final int newZen = NotificationManager.zenModeFromInterruptionFilter(filter, -1); @@ -2051,7 +2052,10 @@ public class ZenModeHelper { /* optional LoggedZenMode zen_mode = 4 */ ROOT_CONFIG, /* optional string id = 5 */ "", // empty for root config /* optional int32 uid = 6 */ Process.SYSTEM_UID, // system owns root config - /* optional DNDPolicyProto policy = 7 */ config.toZenPolicy().toProto())); + /* optional DNDPolicyProto policy = 7 */ config.toZenPolicy().toProto(), + /* optional int32 rule_modified_fields = 8 */ 0, + /* optional int32 policy_modified_fields = 9 */ 0, + /* optional int32 device_effects_modified_fields = 10 */ 0)); if (config.manualRule != null) { ruleToProtoLocked(user, config.manualRule, true, events); } @@ -2093,7 +2097,11 @@ public class ZenModeHelper { /* optional android.stats.dnd.ZenMode zen_mode = 4 */ rule.zenMode, /* optional string id = 5 */ id, /* optional int32 uid = 6 */ getPackageUid(pkg, user), - /* optional DNDPolicyProto policy = 7 */ policyProto)); + /* optional DNDPolicyProto policy = 7 */ policyProto, + /* optional int32 rule_modified_fields = 8 */ rule.userModifiedFields, + /* optional int32 policy_modified_fields = 9 */ rule.zenPolicyUserModifiedFields, + /* optional int32 device_effects_modified_fields = 10 */ + rule.zenDeviceEffectsUserModifiedFields)); } private int getPackageUid(String pkg, int user) { diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 54055904d090..c94111c31ef4 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1023,15 +1023,17 @@ public class UserManagerService extends IUserManager.Stub { if (isAutoLockForPrivateSpaceEnabled()) { int mainUserId = getMainUserIdUnchecked(); + if (mainUserId != UserHandle.USER_NULL) { + mContext.getContentResolver().registerContentObserverAsUser( + Settings.Secure.getUriFor( + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK), false, + mPrivateSpaceAutoLockSettingsObserver, UserHandle.of(mainUserId)); - mContext.getContentResolver().registerContentObserverAsUser(Settings.Secure.getUriFor( - Settings.Secure.PRIVATE_SPACE_AUTO_LOCK), false, - mPrivateSpaceAutoLockSettingsObserver, UserHandle.of(mainUserId)); - - setOrUpdateAutoLockPreferenceForPrivateProfile( - Settings.Secure.getIntForUser(mContext.getContentResolver(), - Settings.Secure.PRIVATE_SPACE_AUTO_LOCK, - Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_NEVER, mainUserId)); + setOrUpdateAutoLockPreferenceForPrivateProfile( + Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK, + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_NEVER, mainUserId)); + } } markEphemeralUsersForRemoval(); diff --git a/services/core/java/com/android/server/utils/AnrTimer.java b/services/core/java/com/android/server/utils/AnrTimer.java index c15ac37565ff..743005a2d844 100644 --- a/services/core/java/com/android/server/utils/AnrTimer.java +++ b/services/core/java/com/android/server/utils/AnrTimer.java @@ -96,17 +96,11 @@ public class AnrTimer<V> implements AutoCloseable { private static boolean DEBUG = false; /** - * The trace tag. + * The trace tag is the same usd by ActivityManager. */ private static final long TRACE_TAG = Trace.TRACE_TAG_ACTIVITY_MANAGER; /** - * Enable tracing from the time a timer expires until it is accepted or discarded. This is - * used to diagnose long latencies in the client. - */ - private static final boolean ENABLE_TRACING = false; - - /** * Return true if the feature is enabled. By default, the value is take from the Flags class * but it can be changed for local testing. */ @@ -320,24 +314,21 @@ public class AnrTimer<V> implements AutoCloseable { } /** - * Start a trace on the timer. The trace is laid down in the AnrTimerTrack. + * Generate a trace point with full timer information. The meaning of milliseconds depends on + * the caller. */ - private void traceBegin(int timerId, int pid, int uid, String what) { - if (ENABLE_TRACING) { - final String label = formatSimple("%s(%d,%d,%s)", what, pid, uid, mLabel); - final int cookie = timerId; - Trace.asyncTraceForTrackBegin(TRACE_TAG, TRACK, label, cookie); - } + private void trace(String op, int timerId, int pid, int uid, long milliseconds) { + final String label = + formatSimple("%s(%d,%d,%d,%s,%d)", op, timerId, pid, uid, mLabel, milliseconds); + Trace.instantForTrack(TRACE_TAG, TRACK, label); } /** - * End a trace on the timer. + * Generate a trace point with just the timer ID. */ - private void traceEnd(int timerId) { - if (ENABLE_TRACING) { - final int cookie = timerId; - Trace.asyncTraceForTrackEnd(TRACE_TAG, TRACK, cookie); - } + private void trace(String op, int timerId) { + final String label = formatSimple("%s(%d)", op, timerId); + Trace.instantForTrack(TRACE_TAG, TRACK, label); } /** @@ -492,7 +483,7 @@ public class AnrTimer<V> implements AutoCloseable { return false; } nativeAnrTimerAccept(mNative, timer); - traceEnd(timer); + trace("accept", timer); return true; } } @@ -511,7 +502,7 @@ public class AnrTimer<V> implements AutoCloseable { return false; } nativeAnrTimerDiscard(mNative, timer); - traceEnd(timer); + trace("discard", timer); return true; } } @@ -629,13 +620,18 @@ public class AnrTimer<V> implements AutoCloseable { } /** - * The notifier that a timer has fired. The timerId and original pid/uid are supplied. This - * method is called from native code. This method takes mLock so that a timer cannot expire - * in the middle of another operation (like start or cancel). + * The notifier that a timer has fired. The timerId and original pid/uid are supplied. The + * elapsed time is the actual time since the timer was scheduled, which may be different from + * the original timeout if the timer was extended or if other delays occurred. This method + * takes mLock so that a timer cannot expire in the middle of another operation (like start or + * cancel). + * + * This method is called from native code. The function must return true if the expiration + * message is delivered to the upper layers and false if it could not be delivered. */ @Keep - private boolean expire(int timerId, int pid, int uid) { - traceBegin(timerId, pid, uid, "expired"); + private boolean expire(int timerId, int pid, int uid, long elapsedMs) { + trace("expired", timerId, pid, uid, elapsedMs); V arg = null; synchronized (mLock) { arg = mTimerArgMap.get(timerId); @@ -815,9 +811,4 @@ public class AnrTimer<V> implements AutoCloseable { /** Prod the native library to log a few statistics. */ private static native void nativeAnrTimerDump(long service, boolean verbose); - - // This is not a native method but it is a native interface, in the sense that it is called from - // the native layer to report timer expiration. The function must return true if the expiration - // message is delivered to the upper layers and false if it could not be delivered. - // private boolean expire(int timerId, int pid, int uid); } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java index b773ade09b89..51acc8e01cda 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java @@ -16,21 +16,28 @@ package com.android.server.wallpaper; +import static android.app.WallpaperManager.getOrientation; +import static android.app.WallpaperManager.getRotatedOrientation; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.server.wallpaper.WallpaperUtils.RECORD_FILE; import static com.android.server.wallpaper.WallpaperUtils.RECORD_LOCK_FILE; import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER; import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir; +import static com.android.window.flags.Flags.multiCrop; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.ImageDecoder; +import android.graphics.Point; import android.graphics.Rect; import android.os.FileUtils; import android.os.SELinux; +import android.text.TextUtils; import android.util.Slog; +import android.util.SparseArray; import android.view.DisplayInfo; +import android.view.View; import com.android.server.utils.TimingsTraceAndSlog; @@ -39,28 +46,334 @@ import libcore.io.IoUtils; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; /** * Helper file for wallpaper cropping - * Meant to have a single instance, only used by the WallpaperManagerService + * Meant to have a single instance, only used internally by system_server + * @hide */ -class WallpaperCropper { +public class WallpaperCropper { private static final String TAG = WallpaperCropper.class.getSimpleName(); private static final boolean DEBUG = false; private static final boolean DEBUG_CROP = true; + /** + * Maximum acceptable parallax. + * A value of 1 means "the additional width for parallax is at most 100% of the screen width" + */ + private static final float MAX_PARALLAX = 1f; + + /** + * We define three ways to adjust a crop. These modes are used depending on the situation: + * - When going from unfolded to folded, we want to remove content + * - When going from folded to unfolded, we want to add content + * - For a screen rotation, we want to keep the same amount of content + */ + private static final int ADD = 1; + private static final int REMOVE = 2; + private static final int BALANCE = 3; + + private final WallpaperDisplayHelper mWallpaperDisplayHelper; + /** + * Helpers exposed to the window manager part (WallpaperController) + */ + public interface WallpaperCropUtils { + + /** + * Equivalent to {@link #getCrop(Point, Point, SparseArray, boolean)} + */ + Rect getCrop(Point displaySize, Point bitmapSize, + SparseArray<Rect> suggestedCrops, boolean rtl); + } + WallpaperCropper(WallpaperDisplayHelper wallpaperDisplayHelper) { mWallpaperDisplayHelper = wallpaperDisplayHelper; } /** - * Once a new wallpaper has been written via setWallpaper(...), it needs to be cropped - * for display. + * Given the dimensions of the original wallpaper image, some optional suggested crops + * (either defined by the user, or coming from a backup), and whether the device is RTL, + * generate a crop for the current display. This is done through the following process: + * <ul> + * <li> If no suggested crops are provided, center the full image on the display. </li> + * <li> If there is a suggested crop the given displaySize, reuse the suggested crop and + * adjust it using {@link #getAdjustedCrop}. </li> + * <li> If there are suggested crops, but not for the orientation of the given displaySize, + * reuse one of the suggested crop for another orientation and adjust if using + * {@link #getAdjustedCrop}. </li> + * </ul> + * + * @param displaySize The dimensions of the surface where we want to render the wallpaper + * @param bitmapSize The dimensions of the wallpaper bitmap + * @param rtl Whether the device is right-to-left + * @param suggestedCrops An optional list of user-defined crops for some orientations. + * If there is a suggested crop for * - * This will generate the crop and write it in the file + * @return A Rect indicating how to crop the bitmap for the current display. + */ + public Rect getCrop(Point displaySize, Point bitmapSize, + SparseArray<Rect> suggestedCrops, boolean rtl) { + + // Case 1: if no crops are provided, center align the full image + if (suggestedCrops == null || suggestedCrops.size() == 0) { + Rect crop = new Rect(0, 0, displaySize.x, displaySize.y); + float scale = Math.min( + ((float) bitmapSize.x) / displaySize.x, + ((float) bitmapSize.y) / displaySize.y); + crop.scale(scale); + crop.offset((bitmapSize.x - crop.width()) / 2, + (bitmapSize.y - crop.height()) / 2); + return crop; + } + int orientation = getOrientation(displaySize); + + // Case 2: if the orientation exists in the suggested crops, adjust the suggested crop + Rect suggestedCrop = suggestedCrops.get(orientation); + if (suggestedCrop != null) { + if (suggestedCrop.left < 0 || suggestedCrop.top < 0 + || suggestedCrop.right > bitmapSize.x || suggestedCrop.bottom > bitmapSize.y) { + Slog.w(TAG, "invalid suggested crop: " + suggestedCrop); + Rect fullImage = new Rect(0, 0, bitmapSize.x, bitmapSize.y); + return getAdjustedCrop(fullImage, bitmapSize, displaySize, true, rtl, ADD); + } else { + return getAdjustedCrop(suggestedCrop, bitmapSize, displaySize, true, rtl, ADD); + } + } + + // Case 3: if we have the 90° rotated orientation in the suggested crops, reuse it and + // trying to preserve the zoom level and the center of the image + SparseArray<Point> defaultDisplaySizes = mWallpaperDisplayHelper.getDefaultDisplaySizes(); + int rotatedOrientation = getRotatedOrientation(orientation); + suggestedCrop = suggestedCrops.get(rotatedOrientation); + Point suggestedDisplaySize = defaultDisplaySizes.get(rotatedOrientation); + if (suggestedCrop != null) { + // only keep the visible part (without parallax) + Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); + return getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, BALANCE); + } + + // Case 4: if the device is a foldable, if we're looking for a folded orientation and have + // the suggested crop of the relative unfolded orientation, reuse it by removing content. + int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation); + suggestedCrop = suggestedCrops.get(unfoldedOrientation); + suggestedDisplaySize = defaultDisplaySizes.get(unfoldedOrientation); + if (suggestedCrop != null) { + // only keep the visible part (without parallax) + Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); + return getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, REMOVE); + } + + // Case 5: if the device is a foldable, if we're looking for an unfolded orientation and + // have the suggested crop of the relative folded orientation, reuse it by adding content. + int foldedOrientation = mWallpaperDisplayHelper.getFoldedOrientation(orientation); + suggestedCrop = suggestedCrops.get(foldedOrientation); + suggestedDisplaySize = defaultDisplaySizes.get(foldedOrientation); + if (suggestedCrop != null) { + // only keep the visible part (without parallax) + Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); + return getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, ADD); + } + + // Case 6: for a foldable device, try to combine case 3 + case 4 or 5: + // rotate, then fold or unfold + Point rotatedDisplaySize = defaultDisplaySizes.get(rotatedOrientation); + if (rotatedDisplaySize != null) { + int rotatedFolded = mWallpaperDisplayHelper.getFoldedOrientation(rotatedOrientation); + int rotateUnfolded = mWallpaperDisplayHelper.getUnfoldedOrientation(rotatedOrientation); + for (int suggestedOrientation : new int[]{rotatedFolded, rotateUnfolded}) { + suggestedCrop = suggestedCrops.get(suggestedOrientation); + if (suggestedCrop != null) { + Rect rotatedCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl); + SparseArray<Rect> rotatedCropMap = new SparseArray<>(); + rotatedCropMap.put(rotatedOrientation, rotatedCrop); + return getCrop(displaySize, bitmapSize, rotatedCropMap, rtl); + } + } + } + + // Case 7: could not properly reuse the suggested crops. Fall back to case 1. + Slog.w(TAG, "Could not find a proper default crop for display: " + displaySize + + ", bitmap size: " + bitmapSize + ", suggested crops: " + suggestedCrops + + ", orientation: " + orientation + ", rtl: " + rtl + + ", defaultDisplaySizes: " + defaultDisplaySizes); + return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl); + } + + /** + * Given a crop, a displaySize for the orientation of that crop, compute the visible part of the + * crop. This removes any additional width used for parallax. No-op if displaySize == null. + */ + private static Rect noParallax(Rect crop, Point displaySize, Point bitmapSize, boolean rtl) { + if (displaySize == null) return crop; + Rect adjustedCrop = getAdjustedCrop(crop, bitmapSize, displaySize, true, rtl, ADD); + // only keep the visible part (without parallax) + float suggestedDisplayRatio = 1f * displaySize.x / displaySize.y; + int widthToRemove = (int) (adjustedCrop.width() + - (((float) adjustedCrop.height()) * suggestedDisplayRatio) + 0.5f); + if (rtl) { + adjustedCrop.left += widthToRemove; + } else { + adjustedCrop.right -= widthToRemove; + } + return adjustedCrop; + } + + /** + * Adjust a given crop: + * <ul> + * <li>If parallax = true, make sure we have a parallax of at most {@link #MAX_PARALLAX}, + * by removing content from the right (or left if RTL) if necessary. + * </li> + * <li>If parallax = false, make sure we do not have additional width for parallax. If we + * have additional width for parallax, remove half of the additional width on both sides. + * </li> + * <li>Make sure the crop fills the screen, i.e. that the width/height ratio of the crop + * is at least the width/height ratio of the screen. If it is less, add width to the crop + * (if possible on both sides) to fill the screen. If not enough width available, remove + * height to the crop. + * </li> + * </ul> + */ + private static Rect getAdjustedCrop(Rect crop, Point bitmapSize, Point screenSize, + boolean parallax, boolean rtl, int mode) { + Rect adjustedCrop = new Rect(crop); + float cropRatio = ((float) crop.width()) / crop.height(); + float screenRatio = ((float) screenSize.x) / screenSize.y; + if (cropRatio >= screenRatio) { + if (!parallax) { + // rotate everything 90 degrees clockwise, compute the result, and rotate back + int newLeft = bitmapSize.y - crop.bottom; + int newRight = newLeft + crop.height(); + int newTop = crop.left; + int newBottom = newTop + crop.width(); + Rect rotatedCrop = new Rect(newLeft, newTop, newRight, newBottom); + Point rotatedBitmap = new Point(bitmapSize.y, bitmapSize.x); + Point rotatedScreen = new Point(screenSize.y, screenSize.x); + Rect rect = getAdjustedCrop(rotatedCrop, rotatedBitmap, rotatedScreen, false, rtl, + mode); + int resultLeft = rect.top; + int resultRight = resultLeft + rect.height(); + int resultTop = rotatedBitmap.x - rect.right; + int resultBottom = resultTop + rect.width(); + return new Rect(resultLeft, resultTop, resultRight, resultBottom); + } + float additionalWidthForParallax = cropRatio / screenRatio - 1f; + if (additionalWidthForParallax > MAX_PARALLAX) { + int widthToRemove = (int) Math.ceil( + (additionalWidthForParallax - MAX_PARALLAX) * screenRatio * crop.height()); + if (rtl) { + adjustedCrop.left += widthToRemove; + } else { + adjustedCrop.right -= widthToRemove; + } + } + } else { + int widthToAdd = mode == REMOVE ? 0 + : mode == ADD ? (int) (0.5 + crop.height() * screenRatio - crop.width()) + : (int) (0.5 + crop.height() - crop.width()); + int availableWidth = bitmapSize.x - crop.width(); + if (availableWidth >= widthToAdd) { + int widthToAddLeft = widthToAdd / 2; + int widthToAddRight = widthToAdd / 2 + widthToAdd % 2; + + if (crop.left < widthToAddLeft) { + widthToAddRight += (widthToAddLeft - crop.left); + widthToAddLeft = crop.left; + } else if (bitmapSize.x - crop.right < widthToAddRight) { + widthToAddLeft += (widthToAddRight - (bitmapSize.x - crop.right)); + widthToAddRight = bitmapSize.x - crop.right; + } + adjustedCrop.left -= widthToAddLeft; + adjustedCrop.right += widthToAddRight; + } else { + adjustedCrop.left = 0; + adjustedCrop.right = bitmapSize.x; + } + int heightToRemove = (int) (crop.height() - (adjustedCrop.width() / screenRatio)); + adjustedCrop.top += heightToRemove / 2 + heightToRemove % 2; + adjustedCrop.bottom -= heightToRemove / 2; + } + return adjustedCrop; + } + + /** + * To find the smallest sub-image that contains all the given crops. + * This is used in {@link #generateCrop(WallpaperData)} + * to determine how the file from {@link WallpaperData#getCropFile()} needs to be cropped. + * + * @param crops a list of rectangles + * @return the smallest rectangle that contains them all. + */ + public static Rect getTotalCrop(SparseArray<Rect> crops) { + int left = Integer.MAX_VALUE, top = Integer.MAX_VALUE; + int right = Integer.MIN_VALUE, bottom = Integer.MIN_VALUE; + for (int i = 0; i < crops.size(); i++) { + Rect rect = crops.valueAt(i); + left = Math.min(left, rect.left); + top = Math.min(top, rect.top); + right = Math.max(right, rect.right); + bottom = Math.max(bottom, rect.bottom); + } + return new Rect(left, top, right, bottom); + } + + /** + * The crops stored in {@link WallpaperData#mCropHints} are relative to the original image. + * This computes the crops relative to the sub-image that will actually be rendered on a window. + */ + SparseArray<Rect> getRelativeCropHints(WallpaperData wallpaper) { + SparseArray<Rect> result = new SparseArray<>(); + 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); + result.put(wallpaper.mCropHints.keyAt(i), adjustedRect); + } + return result; + } + + /** + * Inverse operation of {@link #getRelativeCropHints} + */ + static List<Rect> getOriginalCropHints( + WallpaperData wallpaper, List<Rect> relativeCropHints) { + List<Rect> result = new ArrayList<>(); + for (Rect crop : relativeCropHints) { + Rect originalRect = new Rect(crop); + originalRect.scale(wallpaper.mSampleSize); + originalRect.offset(wallpaper.cropHint.left, wallpaper.cropHint.right); + result.add(originalRect); + } + return result; + } + + /** + * Given some suggested crops, find cropHints for all orientations of the default display. + */ + SparseArray<Rect> getDefaultCrops(SparseArray<Rect> suggestedCrops, Point bitmapSize) { + SparseArray<Rect> result = new SparseArray<>(); + // add missing cropHints for all orientation of the default display + SparseArray<Point> defaultDisplaySizes = mWallpaperDisplayHelper.getDefaultDisplaySizes(); + boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) + == View.LAYOUT_DIRECTION_RTL; + for (int i = 0; i < defaultDisplaySizes.size(); i++) { + int orientation = defaultDisplaySizes.keyAt(i); + Point displaySize = defaultDisplaySizes.valueAt(i); + Rect newCrop = getCrop(displaySize, bitmapSize, suggestedCrops, rtl); + result.put(orientation, newCrop); + } + return result; + } + + /** + * Once a new wallpaper has been written via setWallpaper(...), it needs to be cropped + * for display. This will generate the crop and write it in the file. */ void generateCrop(WallpaperData wallpaper) { TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG); @@ -75,27 +388,47 @@ class WallpaperCropper { // Only generate crop for default display. final WallpaperDisplayHelper.DisplayData wpData = mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY); - final Rect cropHint = new Rect(wallpaper.cropHint); final DisplayInfo displayInfo = mWallpaperDisplayHelper.getDisplayInfo(DEFAULT_DISPLAY); - if (DEBUG) { - Slog.v(TAG, "Generating crop for new wallpaper(s): 0x" - + Integer.toHexString(wallpaper.mWhich) - + " to " + wallpaper.getCropFile().getName() - + " crop=(" + cropHint.width() + 'x' + cropHint.height() - + ") dim=(" + wpData.mWidth + 'x' + wpData.mHeight + ')'); - } - // Analyse the source; needed in multiple cases BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(wallpaper.getWallpaperFile().getAbsolutePath(), options); if (options.outWidth <= 0 || options.outHeight <= 0) { Slog.w(TAG, "Invalid wallpaper data"); - success = false; } else { boolean needCrop = false; boolean needScale; + boolean multiCrop = multiCrop() && wallpaper.mSupportsMultiCrop; + + Point bitmapSize = new Point(options.outWidth, options.outHeight); + + final Rect cropHint; + if (multiCrop) { + SparseArray<Rect> defaultDisplayCrops = + getDefaultCrops(wallpaper.mCropHints, bitmapSize); + // adapt the entries in wallpaper.mCropHints for the actual display + SparseArray<Rect> updatedCropHints = new SparseArray<>(); + for (int i = 0; i < wallpaper.mCropHints.size(); i++) { + Rect defaultCrop = defaultDisplayCrops.valueAt(i); + if (defaultCrop != null) { + updatedCropHints.put(defaultDisplayCrops.keyAt(i), defaultCrop); + } + } + wallpaper.mCropHints = updatedCropHints; + cropHint = getTotalCrop(defaultDisplayCrops); + wallpaper.cropHint.set(cropHint); + } else { + cropHint = new Rect(wallpaper.cropHint); + } + + if (DEBUG) { + Slog.v(TAG, "Generating crop for new wallpaper(s): 0x" + + Integer.toHexString(wallpaper.mWhich) + + " to " + wallpaper.getCropFile().getName() + + " crop=(" + cropHint.width() + 'x' + cropHint.height() + + ") dim=(" + wpData.mWidth + 'x' + wpData.mHeight + ')'); + } // Empty crop means use the full image if (cropHint.isEmpty()) { @@ -128,7 +461,7 @@ class WallpaperCropper { || cropHint.width() > GLHelper.getMaxTextureSize(); //make sure screen aspect ratio is preserved if width is scaled under screen size - if (needScale) { + if (needScale && !multiCrop) { final float scaleByHeight = (float) wpData.mHeight / (float) cropHint.height(); final int newWidth = (int) (cropHint.width() * scaleByHeight); if (newWidth < displayInfo.logicalWidth) { @@ -171,7 +504,7 @@ class WallpaperCropper { BufferedOutputStream bos = null; try { // This actually downsamples only by powers of two, but that's okay; we do - // a proper scaling blit later. This is to minimize transient RAM use. + // a proper scaling a bit later. This is to minimize transient RAM use. // We calculate the largest power-of-two under the actual ratio rather than // just let the decode take care of it because we also want to remap where the // cropHint rectangle lies in the decoded [super]rect. @@ -185,19 +518,31 @@ class WallpaperCropper { final Rect estimateCrop = new Rect(cropHint); estimateCrop.scale(1f / options.inSampleSize); - final float hRatio = (float) wpData.mHeight / estimateCrop.height(); + float hRatio = (float) wpData.mHeight / estimateCrop.height(); + if (multiCrop) { + // make sure the crop height is at most the display largest dimension + hRatio = (float) mWallpaperDisplayHelper.getDefaultDisplayLargestDimension() + / estimateCrop.height(); + hRatio = Math.min(hRatio, 1f); + } final int destHeight = (int) (estimateCrop.height() * hRatio); final int destWidth = (int) (estimateCrop.width() * hRatio); // We estimated an invalid crop, try to adjust the cropHint to get a valid one. if (destWidth > GLHelper.getMaxTextureSize()) { - int newHeight = (int) (wpData.mHeight / hRatio); - int newWidth = (int) (wpData.mWidth / hRatio); - if (DEBUG) { - Slog.v(TAG, "Invalid crop dimensions, trying to adjust."); + Slog.w(TAG, "Invalid crop dimensions, trying to adjust."); + } + if (multiCrop) { + // clear custom crop guidelines, fallback to system default + wallpaper.mCropHints.clear(); + generateCropInternal(wallpaper); + return; } + int newHeight = (int) (wpData.mHeight / hRatio); + int newWidth = (int) (wpData.mWidth / hRatio); + estimateCrop.set(cropHint); estimateCrop.left += (cropHint.width() - newWidth) / 2; estimateCrop.top += (cropHint.height() - newHeight) / 2; @@ -210,8 +555,8 @@ class WallpaperCropper { // We've got the safe cropHint; now we want to scale it properly to // the desired rectangle. // That's a height-biased operation: make it fit the hinted height. - final int safeHeight = (int) (estimateCrop.height() * hRatio); - final int safeWidth = (int) (estimateCrop.width() * hRatio); + final int safeHeight = (int) (estimateCrop.height() * hRatio + 0.5f); + final int safeWidth = (int) (estimateCrop.width() * hRatio + 0.5f); if (DEBUG_CROP) { Slog.v(TAG, "Decode parameters:"); @@ -248,6 +593,12 @@ class WallpaperCropper { // We are safe to create final crop with safe dimensions now. final Bitmap finalCrop = Bitmap.createScaledBitmap(cropped, safeWidth, safeHeight, true); + + if (multiCrop) { + wallpaper.mSampleSize = + ((float) cropHint.height()) / finalCrop.getHeight(); + } + if (DEBUG) { Slog.v(TAG, "Final extract:"); Slog.v(TAG, " dims: w=" + wpData.mWidth diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java index 5c867017f4e0..02594d2d8d22 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperData.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java @@ -17,6 +17,7 @@ package com.android.server.wallpaper; import static android.app.WallpaperManager.FLAG_LOCK; +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER; import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_CROP; @@ -26,6 +27,7 @@ import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir; import android.app.IWallpaperManagerCallback; import android.app.WallpaperColors; +import android.app.WallpaperManager.ScreenOrientation; import android.app.WallpaperManager.SetWallpaperFlags; import android.content.ComponentName; import android.graphics.Rect; @@ -126,10 +128,16 @@ class WallpaperData { RemoteCallbackList<IWallpaperManagerCallback> callbacks = new RemoteCallbackList<>(); /** - * The crop hint supplied for displaying a subset of the source image + * Defines which part of the {@link #getWallpaperFile()} image is in the {@link #getCropFile()}. */ final Rect cropHint = new Rect(0, 0, 0, 0); + /** + * How much the crop is sub-sampled. A value > 1 means that the image quality was reduced. + * This is the ratio between the cropHint height and the actual {@link #getCropFile()} height. + */ + float mSampleSize = 1f; + // Describes the context of a call to WallpaperManagerService#bindWallpaperComponentLocked enum BindSource { UNKNOWN, @@ -156,6 +164,23 @@ class WallpaperData { private final SparseArray<File> mWallpaperFiles = new SparseArray<>(); private final SparseArray<File> mCropFiles = new SparseArray<>(); + /** + * Mapping of {@link ScreenOrientation} -> crop hint. The crop hints are relative to the + * original image stored in {@link #getWallpaperFile()}. + * Only used when multi crop flag is enabled. + */ + SparseArray<Rect> mCropHints = new SparseArray<>(); + + /** + * cropHints will be ignored if this flag is false + */ + boolean mSupportsMultiCrop; + + /** + * The phone orientation when the wallpaper was set. Only relevant for image wallpapers + */ + int mOrientationWhenSet = ORIENTATION_UNKNOWN; + WallpaperData(int userId, @SetWallpaperFlags int wallpaperType) { this.userId = userId; this.mWhich = wallpaperType; @@ -176,6 +201,10 @@ class WallpaperData { this.mWhich = source.mWhich; this.wallpaperId = source.wallpaperId; this.cropHint.set(source.cropHint); + if (source.mCropHints != null) { + this.mCropHints = source.mCropHints.clone(); + } + this.mSupportsMultiCrop = source.mSupportsMultiCrop; this.allowBackup = source.allowBackup; this.primaryColors = source.primaryColors; this.mWallpaperDimAmount = source.mWallpaperDimAmount; diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java index de98df55c3ea..88e9672cd0a1 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java @@ -18,6 +18,7 @@ package com.android.server.wallpaper; import static android.app.WallpaperManager.FLAG_LOCK; import static android.app.WallpaperManager.FLAG_SYSTEM; +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.server.wallpaper.WallpaperDisplayHelper.DisplayData; @@ -26,9 +27,11 @@ import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_CROP; import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_INFO; import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir; import static com.android.server.wallpaper.WallpaperUtils.makeWallpaperIdLocked; +import static com.android.window.flags.Flags.multiCrop; import android.annotation.Nullable; import android.app.WallpaperColors; +import android.app.WallpaperManager; import android.app.WallpaperManager.SetWallpaperFlags; import android.app.backup.WallpaperBackupHelper; import android.content.ComponentName; @@ -36,7 +39,9 @@ import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Color; +import android.graphics.Rect; import android.os.FileUtils; +import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.util.Xml; @@ -60,14 +65,16 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; +import java.util.List; import java.util.Map; /** * Helper for the wallpaper loading / saving / xml parsing * Only meant to be used lock held by WallpaperManagerService * Only meant to be instantiated once by WallpaperManagerService + * @hide */ -class WallpaperDataParser { +public class WallpaperDataParser { private static final String TAG = WallpaperDataParser.class.getSimpleName(); private static final boolean DEBUG = false; @@ -132,6 +139,7 @@ class WallpaperDataParser { */ public WallpaperLoadingResult loadSettingsLocked(int userId, boolean keepDimensionHints, boolean migrateFromOld, @SetWallpaperFlags int which) { + // TODO(b/270726737) remove the "keepDimensionHints" arg when removing the multi crop flag JournaledFile journal = makeJournaledFile(userId); FileInputStream stream = null; File file = journal.chooseForRead(); @@ -174,7 +182,9 @@ class WallpaperDataParser { WallpaperData wallpaperToParse = "wp".equals(tag) ? wallpaper : lockWallpaper; - parseWallpaperAttributes(parser, wallpaperToParse, keepDimensionHints); + if (!multiCrop()) { + parseWallpaperAttributes(parser, wallpaperToParse, keepDimensionHints); + } String comp = parser.getAttributeValue(null, "component"); wallpaperToParse.nextWallpaperComponent = comp != null @@ -186,6 +196,10 @@ class WallpaperDataParser { wallpaperToParse.nextWallpaperComponent = mImageWallpaper; } + if (multiCrop()) { + parseWallpaperAttributes(parser, wallpaperToParse, keepDimensionHints); + } + if (DEBUG) { Slog.v(TAG, "mWidth:" + wpdData.mWidth); Slog.v(TAG, "mHeight:" + wpdData.mHeight); @@ -300,20 +314,48 @@ class WallpaperDataParser { wallpaper.wallpaperId = makeWallpaperIdLocked(); } - final DisplayData wpData = mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY); - - if (!keepDimensionHints) { - wpData.mWidth = parser.getAttributeInt(null, "width"); - wpData.mHeight = parser.getAttributeInt(null, "height"); + Rect totalCropHint = new Rect( + getAttributeInt(parser, "totalCropLeft", 0), + getAttributeInt(parser, "totalCropTop", 0), + getAttributeInt(parser, "totalCropRight", 0), + getAttributeInt(parser, "totalCropBottom", 0)); + wallpaper.mSupportsMultiCrop = multiCrop() && ( + parser.getAttributeBoolean(null, "supportsMultiCrop", false) + || mImageWallpaper.equals(wallpaper.wallpaperComponent)); + if (wallpaper.mSupportsMultiCrop) { + wallpaper.mCropHints = new SparseArray<>(); + for (Pair<Integer, String> pair: screenDimensionPairs()) { + Rect cropHint = new Rect( + parser.getAttributeInt(null, "cropLeft" + pair.second, 0), + parser.getAttributeInt(null, "cropTop" + pair.second, 0), + parser.getAttributeInt(null, "cropRight" + pair.second, 0), + parser.getAttributeInt(null, "cropBottom" + pair.second, 0)); + if (!cropHint.isEmpty()) wallpaper.mCropHints.put(pair.first, cropHint); + } + if (wallpaper.mCropHints.size() == 0) { + // migration case: the crops per screen orientation are not specified. + // use the old attributes to find the crop for one screen orientation. + Integer orientation = totalCropHint.width() < totalCropHint.height() + ? WallpaperManager.PORTRAIT : WallpaperManager.LANDSCAPE; + if (!totalCropHint.isEmpty()) wallpaper.mCropHints.put(orientation, totalCropHint); + } else { + wallpaper.cropHint.set(totalCropHint); + } + } else { + wallpaper.cropHint.set(totalCropHint); + } + final DisplayData wpData = mWallpaperDisplayHelper + .getDisplayDataOrCreate(DEFAULT_DISPLAY); + if (!keepDimensionHints && !multiCrop()) { + wpData.mWidth = parser.getAttributeInt(null, "width", 0); + wpData.mHeight = parser.getAttributeInt(null, "height", 0); + } + if (!multiCrop()) { + wpData.mPadding.left = getAttributeInt(parser, "paddingLeft", 0); + wpData.mPadding.top = getAttributeInt(parser, "paddingTop", 0); + wpData.mPadding.right = getAttributeInt(parser, "paddingRight", 0); + wpData.mPadding.bottom = getAttributeInt(parser, "paddingBottom", 0); } - wallpaper.cropHint.left = getAttributeInt(parser, "cropLeft", 0); - wallpaper.cropHint.top = getAttributeInt(parser, "cropTop", 0); - wallpaper.cropHint.right = getAttributeInt(parser, "cropRight", 0); - wallpaper.cropHint.bottom = getAttributeInt(parser, "cropBottom", 0); - wpData.mPadding.left = getAttributeInt(parser, "paddingLeft", 0); - wpData.mPadding.top = getAttributeInt(parser, "paddingTop", 0); - wpData.mPadding.right = getAttributeInt(parser, "paddingRight", 0); - wpData.mPadding.bottom = getAttributeInt(parser, "paddingBottom", 0); wallpaper.mWallpaperDimAmount = getAttributeFloat(parser, "dimAmount", 0f); BindSource bindSource; try { @@ -365,11 +407,11 @@ class WallpaperDataParser { wallpaper.allowBackup = parser.getAttributeBoolean(null, "backup", false); } - private int getAttributeInt(TypedXmlPullParser parser, String name, int defValue) { + private static int getAttributeInt(TypedXmlPullParser parser, String name, int defValue) { return parser.getAttributeInt(null, name, defValue); } - private float getAttributeFloat(TypedXmlPullParser parser, String name, float defValue) { + private static float getAttributeFloat(TypedXmlPullParser parser, String name, float defValue) { return parser.getAttributeFloat(null, name, defValue); } @@ -412,28 +454,66 @@ class WallpaperDataParser { if (DEBUG) { Slog.v(TAG, "writeWallpaperAttributes id=" + wallpaper.wallpaperId); } - final DisplayData wpdData = mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY); out.startTag(null, tag); out.attributeInt(null, "id", wallpaper.wallpaperId); - out.attributeInt(null, "width", wpdData.mWidth); - out.attributeInt(null, "height", wpdData.mHeight); - out.attributeInt(null, "cropLeft", wallpaper.cropHint.left); - out.attributeInt(null, "cropTop", wallpaper.cropHint.top); - out.attributeInt(null, "cropRight", wallpaper.cropHint.right); - out.attributeInt(null, "cropBottom", wallpaper.cropHint.bottom); + out.attributeBoolean(null, "supportsMultiCrop", wallpaper.mSupportsMultiCrop); - if (wpdData.mPadding.left != 0) { - out.attributeInt(null, "paddingLeft", wpdData.mPadding.left); - } - if (wpdData.mPadding.top != 0) { - out.attributeInt(null, "paddingTop", wpdData.mPadding.top); - } - if (wpdData.mPadding.right != 0) { - out.attributeInt(null, "paddingRight", wpdData.mPadding.right); - } - if (wpdData.mPadding.bottom != 0) { - out.attributeInt(null, "paddingBottom", wpdData.mPadding.bottom); + if (multiCrop() && wallpaper.mSupportsMultiCrop) { + if (wallpaper.mCropHints == null) { + Slog.e(TAG, "cropHints should not be null when saved"); + wallpaper.mCropHints = new SparseArray<>(); + } + for (Pair<Integer, String> pair : screenDimensionPairs()) { + Rect cropHint = wallpaper.mCropHints.get(pair.first); + if (cropHint == null) continue; + out.attributeInt(null, "cropLeft" + pair.second, cropHint.left); + out.attributeInt(null, "cropTop" + pair.second, cropHint.top); + out.attributeInt(null, "cropRight" + pair.second, cropHint.right); + out.attributeInt(null, "cropBottom" + pair.second, cropHint.bottom); + + // to support back compatibility in B&R, save the crops for one orientation in the + // legacy "cropLeft", "cropTop", "cropRight", "cropBottom" entries + int orientationToPutInLegacyCrop = wallpaper.mOrientationWhenSet; + if (mWallpaperDisplayHelper.isFoldable()) { + int unfoldedOrientation = mWallpaperDisplayHelper + .getUnfoldedOrientation(orientationToPutInLegacyCrop); + if (unfoldedOrientation != ORIENTATION_UNKNOWN) { + orientationToPutInLegacyCrop = unfoldedOrientation; + } + } + if (pair.first == orientationToPutInLegacyCrop) { + out.attributeInt(null, "cropLeft", cropHint.left); + out.attributeInt(null, "cropTop", cropHint.top); + out.attributeInt(null, "cropRight", cropHint.right); + out.attributeInt(null, "cropBottom", cropHint.bottom); + } + } + out.attributeInt(null, "totalCropLeft", wallpaper.cropHint.left); + out.attributeInt(null, "totalCropTop", wallpaper.cropHint.top); + out.attributeInt(null, "totalCropRight", wallpaper.cropHint.right); + out.attributeInt(null, "totalCropBottom", wallpaper.cropHint.bottom); + } else if (!multiCrop()) { + final DisplayData wpdData = + mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY); + out.attributeInt(null, "width", wpdData.mWidth); + out.attributeInt(null, "height", wpdData.mHeight); + out.attributeInt(null, "cropLeft", wallpaper.cropHint.left); + out.attributeInt(null, "cropTop", wallpaper.cropHint.top); + out.attributeInt(null, "cropRight", wallpaper.cropHint.right); + out.attributeInt(null, "cropBottom", wallpaper.cropHint.bottom); + if (wpdData.mPadding.left != 0) { + out.attributeInt(null, "paddingLeft", wpdData.mPadding.left); + } + if (wpdData.mPadding.top != 0) { + out.attributeInt(null, "paddingTop", wpdData.mPadding.top); + } + if (wpdData.mPadding.right != 0) { + out.attributeInt(null, "paddingRight", wpdData.mPadding.right); + } + if (wpdData.mPadding.bottom != 0) { + out.attributeInt(null, "paddingBottom", wpdData.mPadding.bottom); + } } out.attributeFloat(null, "dimAmount", wallpaper.mWallpaperDimAmount); @@ -564,4 +644,12 @@ class WallpaperDataParser { } return false; } + + private static List<Pair<Integer, String>> screenDimensionPairs() { + return List.of( + new Pair<>(WallpaperManager.PORTRAIT, "Portrait"), + new Pair<>(WallpaperManager.LANDSCAPE, "Landscape"), + new Pair<>(WallpaperManager.SQUARE_PORTRAIT, "SquarePortrait"), + new Pair<>(WallpaperManager.SQUARE_LANDSCAPE, "SquareLandscape")); + } } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java index f48178c5b9f7..19fd9a90518d 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java @@ -16,19 +16,31 @@ package com.android.server.wallpaper; +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; +import static android.app.WallpaperManager.getRotatedOrientation; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.window.flags.Flags.multiCrop; + +import android.app.WallpaperManager; +import android.graphics.Point; import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.os.Binder; import android.os.Debug; +import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.view.Display; import android.view.DisplayInfo; +import android.view.WindowManager; +import android.view.WindowMetrics; import com.android.server.wm.WindowManagerInternal; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; import java.util.function.Consumer; /** @@ -50,12 +62,55 @@ class WallpaperDisplayHelper { private final SparseArray<DisplayData> mDisplayDatas = new SparseArray<>(); private final DisplayManager mDisplayManager; private final WindowManagerInternal mWindowManagerInternal; + private final SparseArray<Point> mDefaultDisplaySizes = new SparseArray<>(); + + // related orientations pairs for foldable (folded orientation, unfolded orientation) + private final List<Pair<Integer, Integer>> mFoldableOrientationPairs = new ArrayList<>(); + + private boolean mIsFoldable; WallpaperDisplayHelper( DisplayManager displayManager, - WindowManagerInternal windowManagerInternal) { + WindowManager windowManager, + WindowManagerInternal windowManagerInternal, + boolean isFoldable) { mDisplayManager = displayManager; mWindowManagerInternal = windowManagerInternal; + mIsFoldable = isFoldable; + if (!multiCrop()) return; + Set<WindowMetrics> metrics = windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY); + boolean populateOrientationPairs = isFoldable && metrics.size() == 2; + float surface = 0; + int firstOrientation = -1; + for (WindowMetrics metric: metrics) { + Rect bounds = metric.getBounds(); + Point displaySize = new Point(bounds.width(), bounds.height()); + Point reversedDisplaySize = new Point(displaySize.y, displaySize.x); + for (Point point : List.of(displaySize, reversedDisplaySize)) { + int orientation = WallpaperManager.getOrientation(point); + // don't add an entry if there is already a larger display of the same orientation + Point display = mDefaultDisplaySizes.get(orientation); + if (display == null || display.x * display.y < point.x * point.y) { + mDefaultDisplaySizes.put(orientation, point); + } + } + if (populateOrientationPairs) { + int orientation = WallpaperManager.getOrientation(displaySize); + float newSurface = displaySize.x * displaySize.y * metric.getDensity(); + if (surface <= 0) { + surface = newSurface; + firstOrientation = orientation; + } else { + Pair<Integer, Integer> pair = (newSurface > surface) + ? new Pair<>(firstOrientation, orientation) + : new Pair<>(orientation, firstOrientation); + Pair<Integer, Integer> rotatedPair = new Pair<>( + getRotatedOrientation(pair.first), getRotatedOrientation(pair.second)); + mFoldableOrientationPairs.add(pair); + mFoldableOrientationPairs.add(rotatedPair); + } + } + } } DisplayData getDisplayDataOrCreate(int displayId) { @@ -68,6 +123,12 @@ class WallpaperDisplayHelper { return wpdData; } + int getDefaultDisplayCurrentOrientation() { + Point displaySize = new Point(); + mDisplayManager.getDisplay(DEFAULT_DISPLAY).getSize(displaySize); + return WallpaperManager.getOrientation(displaySize); + } + void removeDisplayData(int displayId) { mDisplayDatas.remove(displayId); } @@ -133,4 +194,46 @@ class WallpaperDisplayHelper { boolean isValidDisplay(int displayId) { return mDisplayManager.getDisplay(displayId) != null; } + + SparseArray<Point> getDefaultDisplaySizes() { + return mDefaultDisplaySizes; + } + + /** Return the number of pixel of the largest dimension of the default display */ + int getDefaultDisplayLargestDimension() { + int result = -1; + for (int i = 0; i < mDefaultDisplaySizes.size(); i++) { + Point size = mDefaultDisplaySizes.valueAt(i); + result = Math.max(result, Math.max(size.x, size.y)); + } + return result; + } + + boolean isFoldable() { + return mIsFoldable; + } + + /** + * If a given orientation corresponds to an unfolded orientation on foldable, return the + * corresponding folded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the + * device is not a foldable. + */ + int getFoldedOrientation(int orientation) { + for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) { + if (pair.second.equals(orientation)) return pair.first; + } + return ORIENTATION_UNKNOWN; + } + + /** + * If a given orientation corresponds to a folded orientation on foldable, return the + * corresponding unfolded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the + * device is not a foldable. + */ + int getUnfoldedOrientation(int orientation) { + for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) { + if (pair.first.equals(orientation)) return pair.second; + } + return ORIENTATION_UNKNOWN; + } } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 26584315f15c..8c27bb80d337 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -22,6 +22,7 @@ import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREG import static android.app.WallpaperManager.COMMAND_REAPPLY; import static android.app.WallpaperManager.FLAG_LOCK; import static android.app.WallpaperManager.FLAG_SYSTEM; +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.ParcelFileDescriptor.MODE_CREATE; @@ -74,6 +75,7 @@ import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.hardware.display.DisplayManager; @@ -103,12 +105,15 @@ import android.service.wallpaper.IWallpaperService; import android.service.wallpaper.WallpaperService; import android.system.ErrnoException; import android.system.Os; +import android.text.TextUtils; import android.util.EventLog; import android.util.IntArray; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.view.Display; +import android.view.View; +import android.view.WindowManager; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; @@ -137,6 +142,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; @@ -189,8 +195,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } private final Object mLock = new Object(); - /** True to support different crops for different display dimensions */ - private final boolean mIsMultiCropEnabled; /** Tracks wallpaper being migrated from system+lock to lock when setting static wp. */ WallpaperDestinationChangeHandler mPendingMigrationViaStatic; @@ -804,6 +808,12 @@ public class WallpaperManagerService extends IWallpaperManager.Stub null /* options */); mWindowManagerInternal.setWallpaperShowWhenLocked( mToken, (wallpaper.mWhich & FLAG_LOCK) != 0); + if (multiCrop() && wallpaper.mSupportsMultiCrop) { + mWindowManagerInternal.setWallpaperCropHints(mToken, + mWallpaperCropper.getRelativeCropHints(wallpaper)); + } else { + mWindowManagerInternal.setWallpaperCropHints(mToken, new SparseArray<>()); + } final DisplayData wpdData = mWallpaperDisplayHelper.getDisplayDataOrCreate(mDisplayId); try { @@ -1479,10 +1489,15 @@ public class WallpaperManagerService extends IWallpaperManager.Stub mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); mIPackageManager = AppGlobals.getPackageManager(); mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); - DisplayManager dm = mContext.getSystemService(DisplayManager.class); - dm.registerDisplayListener(mDisplayListener, null /* handler */); - mWallpaperDisplayHelper = new WallpaperDisplayHelper(dm, mWindowManagerInternal); + DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); + displayManager.registerDisplayListener(mDisplayListener, null /* handler */); + WindowManager windowManager = mContext.getSystemService(WindowManager.class); + boolean isFoldable = mContext.getResources() + .getIntArray(R.array.config_foldedDeviceStates).length > 0; + mWallpaperDisplayHelper = new WallpaperDisplayHelper( + displayManager, windowManager, mWindowManagerInternal, isFoldable); mWallpaperCropper = new WallpaperCropper(mWallpaperDisplayHelper); + mWindowManagerInternal.setWallpaperCropUtils(mWallpaperCropper::getCrop); mActivityManager = mContext.getSystemService(ActivityManager.class); if (mContext.getResources().getBoolean( @@ -1522,7 +1537,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub mColorsChangedListeners = new SparseArray<>(); mWallpaperDataParser = new WallpaperDataParser(mContext, mWallpaperDisplayHelper, mWallpaperCropper); - mIsMultiCropEnabled = multiCrop(); LocalServices.addService(WallpaperManagerInternal.class, new LocalService()); } @@ -2199,6 +2213,66 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } } + @Override + public List<Rect> getBitmapCrops(List<Point> displaySizes, @SetWallpaperFlags int which, + boolean originalBitmap, 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 || !wallpaper.mSupportsMultiCrop) return null; + SparseArray<Rect> relativeSuggestedCrops = + mWallpaperCropper.getRelativeCropHints(wallpaper); + Point croppedBitmapSize = + new Point(wallpaper.cropHint.width(), wallpaper.cropHint.height()); + SparseArray<Rect> relativeDefaultCrops = + mWallpaperCropper.getDefaultCrops(relativeSuggestedCrops, croppedBitmapSize); + SparseArray<Rect> adjustedRelativeSuggestedCrops = new SparseArray<>(); + for (int i = 0; i < relativeDefaultCrops.size(); i++) { + int key = relativeDefaultCrops.keyAt(i); + if (relativeSuggestedCrops.contains(key)) { + adjustedRelativeSuggestedCrops.put(key, relativeDefaultCrops.get(key)); + } + } + List<Rect> result = new ArrayList<>(); + boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) + == View.LAYOUT_DIRECTION_RTL; + for (Point displaySize : displaySizes) { + result.add(mWallpaperCropper.getCrop( + displaySize, croppedBitmapSize, adjustedRelativeSuggestedCrops, rtl)); + } + if (originalBitmap) result = WallpaperCropper.getOriginalCropHints(wallpaper, result); + return result; + } + } + + @Override + public List<Rect> getFutureBitmapCrops(Point bitmapSize, List<Point> displaySizes, + int[] screenOrientations, List<Rect> crops) { + SparseArray<Rect> cropMap = getCropMap(screenOrientations, crops, ORIENTATION_UNKNOWN); + SparseArray<Rect> defaultCrops = mWallpaperCropper.getDefaultCrops(cropMap, bitmapSize); + List<Rect> result = new ArrayList<>(); + boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) + == View.LAYOUT_DIRECTION_RTL; + for (Point displaySize : displaySizes) { + result.add(mWallpaperCropper.getCrop(displaySize, bitmapSize, defaultCrops, rtl)); + } + return result; + } + + @Override + public Rect getBitmapCrop(Point bitmapSize, int[] screenOrientations, List<Rect> crops) { + if (!multiCrop()) { + throw new UnsupportedOperationException( + "This method should only be called with the multi crop flag enabled"); + } + SparseArray<Rect> cropMap = getCropMap(screenOrientations, crops, ORIENTATION_UNKNOWN); + SparseArray<Rect> defaultCrops = mWallpaperCropper.getDefaultCrops(cropMap, bitmapSize); + return WallpaperCropper.getTotalCrop(defaultCrops); + } + private boolean hasPermission(String permission) { return mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED; } @@ -2755,8 +2829,18 @@ public class WallpaperManagerService extends IWallpaperManager.Stub @Override public ParcelFileDescriptor setWallpaper(String name, String callingPackage, - Rect cropHint, boolean allowBackup, Bundle extras, int which, - IWallpaperManagerCallback completion, int userId) { + int[] screenOrientations, List<Rect> crops, boolean allowBackup, + Bundle extras, int which, IWallpaperManagerCallback completion, int userId) { + + if (DEBUG) { + Slog.d(TAG, "setWallpaper: name = " + name + ", callingPackage = " + callingPackage + + ", screenOrientations = " + + (screenOrientations == null ? null + : Arrays.stream(screenOrientations).boxed().toList()) + + ", crops = " + crops + + ", allowBackup = " + allowBackup); + } + userId = ActivityManager.handleIncomingUser(getCallingPid(), getCallingUid(), userId, false /* all */, true /* full */, "changing wallpaper", null /* pkg */); checkPermission(android.Manifest.permission.SET_WALLPAPER); @@ -2771,10 +2855,17 @@ public class WallpaperManagerService extends IWallpaperManager.Stub return null; } + int currentOrientation = mWallpaperDisplayHelper.getDefaultDisplayCurrentOrientation(); + SparseArray<Rect> cropMap = !multiCrop() ? null + : getCropMap(screenOrientations, crops, currentOrientation); + Rect cropHint = multiCrop() || crops == null ? null : crops.get(0); + final boolean fromForegroundApp = !multiCrop() ? false + : isFromForegroundApp(callingPackage); + // "null" means the no-op crop, preserving the full input image - if (cropHint == null) { + if (cropHint == null && !multiCrop()) { cropHint = new Rect(0, 0, 0, 0); - } else { + } else if (!multiCrop()) { if (cropHint.width() < 0 || cropHint.height() < 0 || cropHint.left < 0 || cropHint.top < 0) { @@ -2814,10 +2905,14 @@ public class WallpaperManagerService extends IWallpaperManager.Stub wallpaper.mSystemWasBoth = systemIsBoth; wallpaper.mWhich = which; wallpaper.setComplete = completion; - wallpaper.fromForegroundApp = isFromForegroundApp(callingPackage); - wallpaper.cropHint.set(cropHint); + wallpaper.fromForegroundApp = multiCrop() ? fromForegroundApp + : isFromForegroundApp(callingPackage); + if (!multiCrop()) wallpaper.cropHint.set(cropHint); + if (multiCrop()) wallpaper.mSupportsMultiCrop = true; + if (multiCrop()) wallpaper.mCropHints = cropMap; wallpaper.allowBackup = allowBackup; wallpaper.mWallpaperDimAmount = getWallpaperDimAmount(); + wallpaper.mOrientationWhenSet = currentOrientation; } return pfd; } finally { @@ -2826,11 +2921,47 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } } + private SparseArray<Rect> getCropMap(int[] screenOrientations, List<Rect> crops, + int currentOrientation) { + if ((crops == null ^ screenOrientations == null) + || (crops != null && crops.size() != screenOrientations.length)) { + throw new IllegalArgumentException( + "Illegal crops/orientations lists: must both be null, or both the same size"); + } + SparseArray<Rect> cropMap = new SparseArray<>(); + boolean unknown = false; + if (crops != null && crops.size() != 0) { + for (int i = 0; i < crops.size(); i++) { + Rect crop = crops.get(i); + int width = crop.width(), height = crop.height(); + if (width < 0 || height < 0 || crop.left < 0 || crop.top < 0) { + throw new IllegalArgumentException("Invalid crop rect supplied: " + crop); + } + int orientation = screenOrientations[i]; + if (orientation == ORIENTATION_UNKNOWN) { + if (currentOrientation == ORIENTATION_UNKNOWN) { + throw new IllegalArgumentException( + "Invalid orientation: " + ORIENTATION_UNKNOWN); + } + unknown = true; + orientation = currentOrientation; + } + cropMap.put(orientation, crop); + } + } + if (unknown && cropMap.size() > 1) { + throw new IllegalArgumentException("Invalid crops supplied: the UNKNOWN screen " + + "orientation should only be used in a singleton map (in which case it" + + "represents the current orientation of the default display)"); + } + return cropMap; + } + private void migrateStaticSystemToLockWallpaperLocked(int userId) { WallpaperData sysWP = mWallpaperMap.get(userId); if (sysWP == null) { if (DEBUG) { - Slog.i(TAG, "No system wallpaper? Not tracking for lock-only"); + Slog.i(TAG, "No system wallpaper? Not tracking for lock-only"); } return; } @@ -2839,6 +2970,10 @@ public class WallpaperManagerService extends IWallpaperManager.Stub WallpaperData lockWP = new WallpaperData(userId, FLAG_LOCK); lockWP.wallpaperId = sysWP.wallpaperId; lockWP.cropHint.set(sysWP.cropHint); + lockWP.mSupportsMultiCrop = sysWP.mSupportsMultiCrop; + if (sysWP.mCropHints != null) { + lockWP.mCropHints = sysWP.mCropHints.clone(); + } lockWP.allowBackup = sysWP.allowBackup; lockWP.primaryColors = sysWP.primaryColors; lockWP.mWallpaperDimAmount = sysWP.mWallpaperDimAmount; @@ -2956,6 +3091,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub final long ident = Binder.clearCallingIdentity(); try { + newWallpaper.mSupportsMultiCrop = mImageWallpaper.equals(name); newWallpaper.imageWallpaperPending = false; newWallpaper.mWhich = which; newWallpaper.mSystemWasBoth = systemIsBoth; @@ -3428,11 +3564,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub return (wallpaper != null) ? wallpaper.allowBackup : false; } - @Override - public boolean isMultiCropEnabled() { - return mIsMultiCropEnabled; - } - private void onDisplayReadyInternal(int displayId) { synchronized (mLock) { if (mLastWallpaper == null) { diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index a9f0554b2bec..d68f932400a2 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -34,6 +34,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import static com.android.server.wm.WindowManagerService.H.WALLPAPER_DRAW_PENDING_TIMEOUT; +import static com.android.window.flags.Flags.multiCrop; import android.annotation.Nullable; import android.content.res.Resources; @@ -48,6 +49,7 @@ import android.os.SystemClock; import android.util.ArraySet; import android.util.MathUtils; import android.util.Slog; +import android.util.SparseArray; import android.view.Display; import android.view.DisplayInfo; import android.view.SurfaceControl; @@ -60,6 +62,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLogImpl; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.ToBooleanFunction; +import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; import java.io.PrintWriter; import java.util.ArrayList; @@ -73,6 +76,7 @@ import java.util.function.Consumer; class WallpaperController { private static final String TAG = TAG_WITH_CLASS_NAME ? "WallpaperController" : TAG_WM; private WindowManagerService mService; + private WallpaperCropUtils mWallpaperCropUtils = null; private DisplayContent mDisplayContent; private final ArrayList<WallpaperWindowToken> mWallpaperTokens = new ArrayList<>(); @@ -240,9 +244,8 @@ class WallpaperController { mMinWallpaperScale = resources.getFloat(com.android.internal.R.dimen.config_wallpaperMinScale); mMaxWallpaperScale = resources.getFloat(R.dimen.config_wallpaperMaxScale); - mShouldOffsetWallpaperCenter = - resources.getBoolean( - com.android.internal.R.bool.config_offsetWallpaperToCenterOfLargestDisplay); + mShouldOffsetWallpaperCenter = resources.getBoolean( + com.android.internal.R.bool.config_offsetWallpaperToCenterOfLargestDisplay); } void resetLargestDisplay(Display display) { @@ -266,7 +269,7 @@ class WallpaperController { } @Nullable private Point findLargestDisplaySize() { - if (!mShouldOffsetWallpaperCenter) { + if (!mShouldOffsetWallpaperCenter || multiCrop()) { return null; } Point largestDisplaySize = new Point(); @@ -284,6 +287,10 @@ class WallpaperController { return largestDisplaySize; } + void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) { + mWallpaperCropUtils = wallpaperCropUtils; + } + WindowState getWallpaperTarget() { return mWallpaperTarget; } @@ -357,26 +364,92 @@ class WallpaperController { boolean updateWallpaperOffset(WindowState wallpaperWin, boolean sync) { // Size of the display the wallpaper is rendered on. final Rect lastWallpaperBounds = wallpaperWin.getParentFrame(); - // Full size of the wallpaper (usually larger than bounds above to parallax scroll when - // swiping through Launcher pages). - final Rect wallpaperFrame = wallpaperWin.getFrame(); + int screenWidth = lastWallpaperBounds.width(); + int screenHeight = lastWallpaperBounds.height(); + float screenRatio = ((float) screenWidth) / screenHeight; + Point screenSize = new Point(screenWidth, screenHeight); + WallpaperWindowToken token = wallpaperWin.mToken.asWallpaperToken(); - final int diffWidth = wallpaperFrame.width() - lastWallpaperBounds.width(); - final int diffHeight = wallpaperFrame.height() - lastWallpaperBounds.height(); - if ((wallpaperWin.mAttrs.flags & WindowManager.LayoutParams.FLAG_SCALED) != 0 - && Math.abs(diffWidth) > 1 && Math.abs(diffHeight) > 1) { - Slog.d(TAG, "Skip wallpaper offset with inconsistent orientation, bounds=" - + lastWallpaperBounds + " frame=" + wallpaperFrame); - // With FLAG_SCALED, the requested size should at least make the frame match one of - // side. If both sides contain differences, the client side may not have updated the - // latest size according to the current orientation. So skip calculating the offset to - // avoid the wallpaper not filling the screen. - return false; + /* + * TODO(b/270726737) adapt comments once flag gets removed and multiCrop is always true + * Size of the wallpaper. May have more width/height ratio than the screen for parallax. + * + * If multiCrop is true, we use a map, cropHints, defining which sub-area of the wallpaper + * to show for a given screen orientation. In this case, wallpaperFrame represents the + * sub-area of WallpaperWin to show for the current screen size. + * + * If multiCrop is false, don't show a custom sub-area of the wallpaper. Just show the + * whole wallpaperWin if possible, and center and zoom if necessary. + */ + final Rect wallpaperFrame; + + /* + * The values cropZoom, cropOffsetX and cropOffsetY are only used if multiCrop is true. + * Zoom and offsets to be applied in order to show wallpaperFrame on screen. + */ + final float cropZoom; + final int cropOffsetX; + final int cropOffsetY; + + /* + * Difference of width/height between the wallpaper and the screen. + * This is the additional room that we have to apply offsets (i.e. parallax). + */ + final int diffWidth; + final int diffHeight; + + /* + * zoom, offsetX and offsetY are not related to cropping the wallpaper: + * - zoom is used to apply an additional zoom (e.g. for launcher animations). + * - offsetX, offsetY are used to apply an offset to the wallpaper (e.g. parallax effect). + */ + final float zoom; + int offsetX; + int offsetY; + + if (multiCrop()) { + if (mWallpaperCropUtils == null) { + Slog.e(TAG, "Update wallpaper offsets before the system is ready. Aborting"); + return false; + } + Point bitmapSize = new Point( + wallpaperWin.mRequestedWidth, wallpaperWin.mRequestedHeight); + SparseArray<Rect> cropHints = token.getCropHints(); + wallpaperFrame = mWallpaperCropUtils.getCrop( + screenSize, bitmapSize, cropHints, wallpaperWin.isRtl()); + + cropZoom = wallpaperFrame.isEmpty() ? 1f + : ((float) screenHeight) / wallpaperFrame.height() / wallpaperWin.mVScale; + + // A positive x / y offset shifts the wallpaper to the right / bottom respectively. + cropOffsetX = -wallpaperFrame.left + + (int) ((cropZoom - 1f) * wallpaperFrame.height() * screenRatio / 2f); + cropOffsetY = -wallpaperFrame.top + + (int) ((cropZoom - 1f) * wallpaperFrame.height() / 2f); + + diffWidth = (int) (wallpaperFrame.width() * wallpaperWin.mHScale) - screenWidth; + diffHeight = (int) (wallpaperFrame.height() * wallpaperWin.mVScale) - screenHeight; + } else { + wallpaperFrame = wallpaperWin.getFrame(); + cropZoom = 1f; + cropOffsetX = 0; + cropOffsetY = 0; + diffWidth = wallpaperFrame.width() - screenWidth; + diffHeight = wallpaperFrame.height() - screenHeight; + + if ((wallpaperWin.mAttrs.flags & WindowManager.LayoutParams.FLAG_SCALED) != 0 + && Math.abs(diffWidth) > 1 && Math.abs(diffHeight) > 1) { + Slog.d(TAG, "Skip wallpaper offset with inconsistent orientation, bounds=" + + lastWallpaperBounds + " frame=" + wallpaperFrame); + // With FLAG_SCALED, the requested size should at least make the frame match one of + // side. If both sides contain differences, the client side may not have updated the + // latest size according to the current orientation. So skip calculating the offset + // to avoid the wallpaper not filling the screen. + return false; + } } - int newXOffset = 0; - int newYOffset = 0; boolean rawChanged = false; // Set the default wallpaper x-offset to either edge of the screen (depending on RTL), to // match the behavior of most Launchers @@ -396,17 +469,17 @@ class WallpaperController { int displayOffset = getDisplayWidthOffset(availw, lastWallpaperBounds, wallpaperWin.isRtl()); availw -= displayOffset; - int offset = availw > 0 ? -(int)(availw * wpx + .5f) : 0; + offsetX = availw > 0 ? -(int) (availw * wpx + .5f) : 0; if (token.mWallpaperDisplayOffsetX != Integer.MIN_VALUE) { // if device is LTR, then offset wallpaper to the left (the wallpaper is drawn // always starting from the left of the screen). - offset += token.mWallpaperDisplayOffsetX; + offsetX += token.mWallpaperDisplayOffsetX; } else if (!wallpaperWin.isRtl()) { // In RTL the offset is calculated so that the wallpaper ends up right aligned (see // offset above). - offset -= displayOffset; + offsetX -= displayOffset; } - newXOffset = offset; + offsetX += cropOffsetX * wallpaperWin.mHScale; if (wallpaperWin.mWallpaperX != wpx || wallpaperWin.mWallpaperXStep != wpxs) { wallpaperWin.mWallpaperX = wpx; @@ -416,11 +489,11 @@ class WallpaperController { float wpy = token.mWallpaperY >= 0 ? token.mWallpaperY : 0.5f; float wpys = token.mWallpaperYStep >= 0 ? token.mWallpaperYStep : -1.0f; - offset = diffHeight > 0 ? -(int) (diffHeight * wpy + .5f) : 0; + offsetY = diffHeight > 0 ? -(int) (diffHeight * wpy + .5f) : 0; if (token.mWallpaperDisplayOffsetY != Integer.MIN_VALUE) { - offset += token.mWallpaperDisplayOffsetY; + offsetY += token.mWallpaperDisplayOffsetY; } - newYOffset = offset; + offsetY += cropOffsetY * wallpaperWin.mVScale; if (wallpaperWin.mWallpaperY != wpy || wallpaperWin.mWallpaperYStep != wpys) { wallpaperWin.mWallpaperY = wpy; @@ -432,10 +505,10 @@ class WallpaperController { wallpaperWin.mWallpaperZoomOut = mLastWallpaperZoomOut; rawChanged = true; } - - boolean changed = wallpaperWin.setWallpaperOffset(newXOffset, newYOffset, - wallpaperWin.mShouldScaleWallpaper - ? zoomOutToScale(wallpaperWin.mWallpaperZoomOut) : 1); + zoom = wallpaperWin.mShouldScaleWallpaper + ? zoomOutToScale(wallpaperWin.mWallpaperZoomOut) : 1f; + final float totalZoom = zoom * cropZoom; + boolean changed = wallpaperWin.setWallpaperOffset(offsetX, offsetY, totalZoom); if (rawChanged && (wallpaperWin.mAttrs.privateFlags & WindowManager.LayoutParams.PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS) != 0) { @@ -496,7 +569,7 @@ class WallpaperController { * display). */ private int getDisplayWidthOffset(int availWidth, Rect displayFrame, boolean isRtl) { - if (!mShouldOffsetWallpaperCenter) { + if (!mShouldOffsetWallpaperCenter || multiCrop()) { return 0; } if (mLargestDisplaySize == null) { diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java index 15bd6078dc2d..1bcd882b5d64 100644 --- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java +++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java @@ -25,9 +25,11 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.annotation.Nullable; +import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; +import android.util.SparseArray; import android.view.animation.Animation; import com.android.internal.protolog.common.ProtoLog; @@ -49,6 +51,12 @@ class WallpaperWindowToken extends WindowToken { int mWallpaperDisplayOffsetX = Integer.MIN_VALUE; int mWallpaperDisplayOffsetY = Integer.MIN_VALUE; + /** + * Map from {@link android.app.WallpaperManager.ScreenOrientation} to crop rectangles. + * Crop rectangles represent the part of the wallpaper displayed for each screen orientation. + */ + private SparseArray<Rect> mCropHints = new SparseArray<>(); + WallpaperWindowToken(WindowManagerService service, IBinder token, boolean explicit, DisplayContent dc, boolean ownerCanManageAppTokens) { this(service, token, explicit, dc, ownerCanManageAppTokens, null /* options */); @@ -98,6 +106,14 @@ class WallpaperWindowToken extends WindowToken { return mShowWhenLocked; } + void setCropHints(SparseArray<Rect> cropHints) { + mCropHints = cropHints.clone(); + } + + SparseArray<Rect> getCropHints() { + return mCropHints; + } + void sendWindowWallpaperCommand( String action, int x, int y, int z, Bundle extras, boolean sync) { for (int wallpaperNdx = mChildren.size() - 1; wallpaperNdx >= 0; wallpaperNdx--) { diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index f2a58e54bfbe..d0b9a6ec8775 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -34,6 +34,7 @@ import android.os.IBinder; import android.os.Message; import android.util.ArraySet; import android.util.Pair; +import android.util.SparseArray; import android.view.ContentRecordingSession; import android.view.Display; import android.view.IInputFilter; @@ -52,6 +53,7 @@ import android.window.ScreenCapture; import com.android.internal.policy.KeyInterceptionInfo; import com.android.server.input.InputManagerService; import com.android.server.policy.WindowManagerPolicy; +import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; import com.android.server.wm.SensitiveContentPackages.PackageInfo; import java.lang.annotation.Retention; @@ -699,6 +701,21 @@ public abstract class WindowManagerInternal { public abstract void setWallpaperShowWhenLocked(IBinder windowToken, boolean showWhenLocked); /** + * Sets the crop hints of a {@link WallpaperWindowToken}. Only effective for image wallpapers. + * + * @param windowToken wallpaper token previously added via {@link #addWindowToken} + * @param cropHints a map that represents which part of the wallpaper should be shown, for + * each type of {@link android.app.WallpaperManager.ScreenOrientation}. + */ + public abstract void setWallpaperCropHints(IBinder windowToken, SparseArray<Rect> cropHints); + + /** + * Transmits the {@link WallpaperCropUtils} instance to {@link WallpaperController}. + * {@link WallpaperCropUtils} contains the helpers to properly position the wallpaper. + */ + public abstract void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils); + + /** * Returns {@code true} if a Window owned by {@code uid} has focus. */ public abstract boolean isUidFocused(int uid); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 6c833565119f..f8ac8da710c8 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -153,6 +153,7 @@ import static com.android.server.wm.WindowManagerServiceDumpProto.INPUT_METHOD_W import static com.android.server.wm.WindowManagerServiceDumpProto.POLICY; import static com.android.server.wm.WindowManagerServiceDumpProto.ROOT_WINDOW_CONTAINER; import static com.android.server.wm.WindowManagerServiceDumpProto.WINDOW_FRAMES_VALID; +import static com.android.window.flags.Flags.multiCrop; import android.Manifest; import android.Manifest.permission; @@ -238,6 +239,7 @@ import android.util.EventLog; import android.util.MergedConfiguration; import android.util.Pair; import android.util.Slog; +import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.TimeUtils; @@ -342,6 +344,7 @@ import com.android.server.policy.WindowManagerPolicy; import com.android.server.policy.WindowManagerPolicy.ScreenOffListener; import com.android.server.power.ShutdownThread; import com.android.server.utils.PriorityDump; +import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; import dalvik.annotation.optimization.NeverCompile; @@ -8128,6 +8131,25 @@ public class WindowManagerService extends IWindowManager.Stub } @Override + public void setWallpaperCropHints(IBinder binder, SparseArray<Rect> cropHints) { + synchronized (mGlobalLock) { + final WindowToken token = mRoot.getWindowToken(binder); + if (token == null || token.asWallpaperToken() == null) { + ProtoLog.w(WM_ERROR, + "setWallpaperCropHints: non-existent wallpaper token: %s", binder); + return; + } + token.asWallpaperToken().setCropHints(cropHints); + } + } + + @Override + public void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) { + mRoot.getDisplayContent(DEFAULT_DISPLAY).mWallpaperController + .setWallpaperCropUtils(wallpaperCropUtils); + } + + @Override public boolean isUidFocused(int uid) { synchronized (mGlobalLock) { for (int i = mRoot.getChildCount() - 1; i >= 0; i--) { @@ -9366,7 +9388,8 @@ public class WindowManagerService extends IWindowManager.Stub final long origId = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { - if (!mAtmService.isCallerRecents(callingUid)) { + if (!mAtmService.isCallerRecents(callingUid) + && (!multiCrop() || callingUid != SYSTEM_UID)) { Slog.e(TAG, "Unable to verify uid for getPossibleDisplayInfo" + " on uid " + callingUid); return new ArrayList<>(); diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp index 1e48aced0041..8ca5333a65bd 100644 --- a/services/core/jni/com_android_server_utils_AnrTimer.cpp +++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp @@ -113,8 +113,10 @@ class AnrTimerService { static const timer_id_t NOTIMER = 0; // A notifier is called with a timer ID, the timer's tag, and the client's cookie. The pid - // and uid that were originally assigned to the timer are passed as well. - using notifier_t = bool (*)(timer_id_t, int pid, int uid, void* cookie, jweak object); + // and uid that were originally assigned to the timer are passed as well. The elapsed time + // is the time since the timer was scheduled. + using notifier_t = bool (*)(timer_id_t, int pid, int uid, nsecs_t elapsed, + void* cookie, jweak object); enum Status { Invalid, @@ -278,6 +280,9 @@ class AnrTimerService::Timer { // The state of this timer. Status status; + // The time at which the timer was started. + nsecs_t started; + // The scheduled timeout. This is an absolute time. It may be extended. nsecs_t scheduled; @@ -297,6 +302,7 @@ class AnrTimerService::Timer { timeout(0), extend(false), status(Invalid), + started(0), scheduled(0), extended(false) { } @@ -310,6 +316,7 @@ class AnrTimerService::Timer { timeout(0), extend(false), status(Invalid), + started(0), scheduled(0), extended(false) { } @@ -322,7 +329,8 @@ class AnrTimerService::Timer { timeout(timeout), extend(extend), status(Running), - scheduled(now() + timeout), + started(now()), + scheduled(started + timeout), extended(false) { if (extend && pid != 0) { initial.fill(pid); @@ -714,6 +722,7 @@ void AnrTimerService::expire(timer_id_t timerId) { // Save the timer attributes for the notification int pid = 0; int uid = 0; + nsecs_t elapsed = 0; bool expired = false; { AutoMutex _l(lock_); @@ -727,11 +736,14 @@ void AnrTimerService::expire(timer_id_t timerId) { // accept or discard). insert(t); } + pid = t.pid; + uid = t.uid; + elapsed = now() - t.started; } // Deliver the notification outside of the lock. if (expired) { - if (!notifier_(timerId, pid, uid, notifierCookie_, notifierObject_)) { + if (!notifier_(timerId, pid, uid, elapsed, notifierCookie_, notifierObject_)) { AutoMutex _l(lock_); // Notification failed, which means the listener will never call accept() or // discard(). Do not reinsert the timer. @@ -804,7 +816,7 @@ struct AnrArgs { static AnrArgs gAnrArgs; // The cookie is the address of the AnrArgs object to which the notification should be sent. -static bool anrNotify(AnrTimerService::timer_id_t timerId, int pid, int uid, +static bool anrNotify(AnrTimerService::timer_id_t timerId, int pid, int uid, nsecs_t elapsed, void* cookie, jweak jtimer) { AutoMutex _l(gAnrLock); AnrArgs* target = reinterpret_cast<AnrArgs* >(cookie); @@ -816,7 +828,8 @@ static bool anrNotify(AnrTimerService::timer_id_t timerId, int pid, int uid, jboolean r = false; jobject timer = env->NewGlobalRef(jtimer); if (timer != nullptr) { - r = env->CallBooleanMethod(timer, target->func, timerId, pid, uid); + // Convert the elsapsed time from ns (native) to ms (Java) + r = env->CallBooleanMethod(timer, target->func, timerId, pid, uid, ns2ms(elapsed)); env->DeleteGlobalRef(timer); } target->vm->DetachCurrentThread(); @@ -909,7 +922,7 @@ int register_android_server_utils_AnrTimer(JNIEnv* env) jclass service = FindClassOrDie(env, className); gAnrArgs.clazz = MakeGlobalRefOrDie(env, service); - gAnrArgs.func = env->GetMethodID(gAnrArgs.clazz, "expire", "(III)Z"); + gAnrArgs.func = env->GetMethodID(gAnrArgs.clazz, "expire", "(IIIJ)Z"); env->GetJavaVM(&gAnrArgs.vm); nativeSupportEnabled = NATIVE_SUPPORT; diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index 0089d4cafaad..5e5181bdfeeb 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -109,6 +109,7 @@ <uses-permission android:name="android.permission.UPDATE_LOCK_TASK_PACKAGES" /> <uses-permission android:name="android.permission.ACCESS_CONTEXT_HUB" /> <uses-permission android:name="android.permission.USE_BIOMETRIC_INTERNAL" /> + <uses-permission android:name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" /> <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" /> <uses-permission android:name="android.permission.MANAGE_ROLE_HOLDERS" /> diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java index 3aaac2e9cf1b..c8a5583de0b2 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java @@ -16,6 +16,7 @@ package com.android.server.biometrics.sensors.face; +import static android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; import static android.hardware.face.FaceSensorProperties.TYPE_UNKNOWN; @@ -234,6 +235,26 @@ public class FaceServiceTest { } @Test + public void testAuthenticateInBackground() throws Exception { + FaceAuthenticateOptions faceAuthenticateOptions = new FaceAuthenticateOptions.Builder() + .build(); + initService(); + mFaceService.mServiceWrapper.registerAuthenticators(List.of()); + waitForRegistration(); + + mContext.getTestablePermissions().setPermission( + USE_BIOMETRIC_INTERNAL, PackageManager.PERMISSION_DENIED); + mContext.getTestablePermissions().setPermission( + USE_BACKGROUND_FACE_AUTHENTICATION, PackageManager.PERMISSION_GRANTED); + + final long operationId = 5; + mFaceService.mServiceWrapper.authenticateInBackground(mToken, operationId, + mFaceServiceReceiver, faceAuthenticateOptions); + + assertThat(faceAuthenticateOptions.getSensorId()).isEqualTo(ID_DEFAULT); + } + + @Test public void testOptionsForDetect() throws Exception { FaceAuthenticateOptions faceAuthenticateOptions = new FaceAuthenticateOptions.Builder() .setOpPackageName(ComponentName.unflattenFromString(OP_PACKAGE_NAME) diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index d71844b00b3b..9ff29d208dc0 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -829,6 +829,40 @@ public class VirtualDeviceManagerServiceTest { } @Test + public void getDisplayNameForPersistentDeviceId_nonExistentPeristentId_returnsNull() { + assertThat(mVdm.getDisplayNameForPersistentDeviceId("nonExistentPersistentId")).isNull(); + } + + @Test + public void getDisplayNameForPersistentDeviceId_defaultDevicePeristentId_returnsNull() { + assertThat(mVdm.getDisplayNameForPersistentDeviceId( + VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)) + .isNull(); + } + + @Test + public void getDisplayNameForPersistentDeviceId_validVirtualDevice_returnsCorrectId() { + mVdms.onCdmAssociationsChanged(List.of(mAssociationInfo)); + CharSequence persistentIdDisplayName = + mVdm.getDisplayNameForPersistentDeviceId(mDeviceImpl.getPersistentDeviceId()); + assertThat(persistentIdDisplayName.toString()) + .isEqualTo(mAssociationInfo.getDisplayName().toString()); + } + + @Test + public void getDisplayNameForPersistentDeviceId_noVirtualDevice_returnsCorrectId() { + CharSequence displayName = "New display name for the new association"; + mVdms.onCdmAssociationsChanged(List.of( + createAssociationInfo(2, AssociationRequest.DEVICE_PROFILE_APP_STREAMING, + displayName))); + + CharSequence persistentIdDisplayName = + mVdm.getDisplayNameForPersistentDeviceId( + VirtualDeviceImpl.createPersistentDeviceId(2)); + assertThat(persistentIdDisplayName.toString()).isEqualTo(displayName.toString()); + } + + @Test public void onAppsOnVirtualDeviceChanged_singleVirtualDevice_listenersNotified() { ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(UID_1, UID_2)); mLocalService.registerAppsOnVirtualDeviceListener(mAppsOnVirtualDeviceListener); @@ -1994,8 +2028,14 @@ public class VirtualDeviceManagerServiceTest { } private AssociationInfo createAssociationInfo(int associationId, String deviceProfile) { + return createAssociationInfo( + associationId, deviceProfile, /* displayName= */ deviceProfile); + } + + private AssociationInfo createAssociationInfo(int associationId, String deviceProfile, + CharSequence displayName) { return new AssociationInfo(associationId, /* userId= */ 0, /* packageName=*/ null, - /* tag= */ null, MacAddress.BROADCAST_ADDRESS, /* displayName= */ "", deviceProfile, + /* tag= */ null, MacAddress.BROADCAST_ADDRESS, displayName, deviceProfile, /* associatedDevice= */ null, /* selfManaged= */ true, /* notifyOnDeviceNearby= */ false, /* revoked= */ false, /* pending= */ false, /* timeApprovedMs= */0, /* lastTimeConnectedMs= */0, /* systemDataSyncFlags= */ -1); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 723ac15fb50f..9c2cba8ecf96 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -224,6 +224,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.os.WorkSource; import android.permission.PermissionManager; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.platform.test.rule.DeniedDevices; @@ -13978,6 +13979,58 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void requestInterruptionFilterFromListener_fromApp_doesNotSetGlobalZen() + throws Exception { + mService.setCallerIsNormalPackage(); + mService.mZenModeHelper = mock(ZenModeHelper.class); + ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class); + when(mListeners.checkServiceTokenLocked(any())).thenReturn(info); + info.component = new ComponentName("pkg", "cls"); + + mBinderService.requestInterruptionFilterFromListener(mock(INotificationListener.class), + INTERRUPTION_FILTER_PRIORITY); + + verify(mService.mZenModeHelper).applyGlobalZenModeAsImplicitZenRule(eq("pkg"), eq(mUid), + eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS)); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void requestInterruptionFilterFromListener_fromSystem_setsGlobalZen() + throws Exception { + mService.isSystemUid = true; + mService.mZenModeHelper = mock(ZenModeHelper.class); + ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class); + when(mListeners.checkServiceTokenLocked(any())).thenReturn(info); + info.component = new ComponentName("pkg", "cls"); + + mBinderService.requestInterruptionFilterFromListener(mock(INotificationListener.class), + INTERRUPTION_FILTER_PRIORITY); + + verify(mService.mZenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), + eq(null), eq(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), anyString(), + eq("pkg"), eq(mUid)); + } + + @Test + @DisableFlags(android.app.Flags.FLAG_MODES_API) + public void requestInterruptionFilterFromListener_flagOff_callsRequestFromListener() + throws Exception { + mService.setCallerIsNormalPackage(); + mService.mZenModeHelper = mock(ZenModeHelper.class); + ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class); + when(mListeners.checkServiceTokenLocked(any())).thenReturn(info); + info.component = new ComponentName("pkg", "cls"); + + mBinderService.requestInterruptionFilterFromListener(mock(INotificationListener.class), + INTERRUPTION_FILTER_PRIORITY); + + verify(mService.mZenModeHelper).requestFromListener(eq(info.component), + eq(INTERRUPTION_FILTER_PRIORITY), eq(mUid), /* fromSystemOrSystemUi= */ eq(false)); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) public void updateAutomaticZenRule_implicitRuleWithoutCPS_disallowedFromApp() throws Exception { setUpRealZenTest(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index edc876aab388..248683836336 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -1185,6 +1185,28 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testProtoWithAutoRuleWithModifiedFields() throws Exception { + setupZenConfig(); + mZenModeHelper.mConfig.automaticRules = new ArrayMap<>(); + ZenRule rule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS, CUSTOM_RULE_ID); + rule.userModifiedFields = AutomaticZenRule.FIELD_NAME; + rule.zenPolicyUserModifiedFields = ZenPolicy.FIELD_PRIORITY_CATEGORY_MEDIA; + rule.zenDeviceEffectsUserModifiedFields = ZenDeviceEffects.FIELD_GRAYSCALE; + mZenModeHelper.mConfig.automaticRules.put(rule.id, rule); + + List<StatsEvent> events = new ArrayList<>(); + mZenModeHelper.pullRules(events); + + assertThat(events).hasSize(2); // Global config + 1 automatic rule + DNDModeProto ruleProto = StatsEventTestUtils.convertToAtom(events.get(1)).getDndModeRule(); + assertThat(ruleProto.getRuleModifiedFields()).isEqualTo(rule.userModifiedFields); + assertThat(ruleProto.getPolicyModifiedFields()).isEqualTo(rule.zenPolicyUserModifiedFields); + assertThat(ruleProto.getDeviceEffectsModifiedFields()).isEqualTo( + rule.zenDeviceEffectsUserModifiedFields); + } + + @Test public void ruleUidsCached() throws Exception { setupZenConfig(); // one enabled automatic rule diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java index 6216acbfe465..73d386a328f5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java @@ -32,6 +32,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; +import static com.android.window.flags.Flags.multiCrop; import static com.google.common.truth.Truth.assertThat; @@ -39,6 +40,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; @@ -484,6 +486,7 @@ public class WallpaperControllerTests extends WindowTestsBase { @Test public void testUpdateWallpaperOffset_resize_shouldCenterEnabled() { + assumeFalse(multiCrop()); final DisplayContent dc = new TestDisplayContent.Builder(mAtm, INITIAL_WIDTH, INITIAL_HEIGHT).build(); dc.mWallpaperController.setShouldOffsetWallpaperCenter(true); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 7ae5a1156d07..114b9c3a68f2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -119,6 +119,7 @@ import android.window.TransitionRequestInfo; import com.android.internal.policy.AttributeCache; import com.android.internal.util.ArrayUtils; import com.android.internal.util.test.FakeSettingsProvider; +import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; import com.android.server.wm.DisplayWindowSettings.SettingsProvider.SettingsEntry; import org.junit.After; @@ -286,6 +287,18 @@ class WindowTestsBase extends SystemServiceTestsBase { mAtm.mWindowManager.mLetterboxConfiguration .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(false); + // Setup WallpaperController crop utils with a simple center-align strategy + WallpaperCropUtils cropUtils = (displaySize, bitmapSize, suggestedCrops, rtl) -> { + Rect crop = new Rect(0, 0, displaySize.x, displaySize.y); + crop.scale(Math.min( + ((float) bitmapSize.x) / displaySize.x, + ((float) bitmapSize.y) / displaySize.y)); + crop.offset((bitmapSize.x - crop.width()) / 2, (bitmapSize.y - crop.height()) / 2); + return crop; + }; + mDisplayContent.mWallpaperController.setWallpaperCropUtils(cropUtils); + mDefaultDisplay.mWallpaperController.setWallpaperCropUtils(cropUtils); + checkDeviceSpecificOverridesNotApplied(); } diff --git a/telecomm/java/android/telecom/Response.java b/telecomm/java/android/telecom/Response.java deleted file mode 100644 index ce7a7612786b..000000000000 --- a/telecomm/java/android/telecom/Response.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2014 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.telecom; - -/** - * @hide - */ -public interface Response<IN, OUT> { - - /** - * Provide a set of results. - * - * @param request The original request. - * @param result The results. - */ - void onResult(IN request, OUT... result); - - /** - * Indicates the inability to provide results. - * - * @param request The original request. - * @param code An integer code indicating the reason for failure. - * @param msg A message explaining the reason for failure. - */ - void onError(IN request, int code, String msg); -} diff --git a/telephony/java/android/telephony/CarrierInfo.java b/telephony/java/android/telephony/CarrierInfo.java new file mode 100644 index 000000000000..da77a45b998f --- /dev/null +++ b/telephony/java/android/telephony/CarrierInfo.java @@ -0,0 +1,236 @@ +/* + * 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; + +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.telephony.Rlog; + +import java.util.ArrayList; +import java.util.List; + +/** + * CarrierInfo that is used to represent the carrier lock information details. + * + * @hide + */ +public final class CarrierInfo implements Parcelable { + + /** + * Used to create a {@link CarrierInfo} from a {@link Parcel}. + * + * @hide + */ + public static final @android.annotation.NonNull Creator<CarrierInfo> CREATOR = + new Creator<CarrierInfo>() { + /** + * Create a new instance of the Parcelable class, instantiating it + * from the given Parcel whose data had previously been written by + * {@link Parcelable#writeToParcel Parcelable.writeToParcel()}. + * + * @param source The Parcel to read the object's data from. + * @return Returns a new instance of the Parcelable class. + */ + @Override + public CarrierInfo createFromParcel(Parcel source) { + return new CarrierInfo(source); + } + + /** + * Create a new array of the Parcelable class. + * + * @param size Size of the array. + * @return Returns an array of the Parcelable class, with every entry + * initialized to null. + */ + @Override + public CarrierInfo[] newArray(int size) { + return new CarrierInfo[size]; + } + + }; + @NonNull + private String mMcc; + @NonNull + private String mMnc; + @Nullable + private String mSpn; + @Nullable + private String mGid1; + @Nullable + private String mGid2; + @Nullable + private String mImsiPrefix; + /** Ehplmn is String combination of MCC,MNC */ + @Nullable + private List<String> mEhplmn; + @Nullable + private String mIccid; + @Nullable + private String mImpi; + + /** @hide */ + @NonNull + public String getMcc() { + return mMcc; + } + + /** @hide */ + @NonNull + public String getMnc() { + return mMnc; + } + + /** @hide */ + @Nullable + public String getSpn() { + return mSpn; + } + + /** @hide */ + @Nullable + public String getGid1() { + return mGid1; + } + + /** @hide */ + @Nullable + public String getGid2() { + return mGid2; + } + + /** @hide */ + @Nullable + public String getImsiPrefix() { + return mImsiPrefix; + } + + /** @hide */ + @Nullable + public String getIccid() { + return mIccid; + } + + /** @hide */ + @Nullable + public String getImpi() { + return mImpi; + } + + /** + * Returns the list of EHPLMN. + * + * @return List of String that represent Ehplmn. + * @hide + */ + @NonNull + public List<String> getEhplmn() { + return mEhplmn; + } + + /** @hide */ + public CarrierInfo(@NonNull String mcc, @NonNull String mnc, @Nullable String spn, + @Nullable String gid1, @Nullable String gid2, @Nullable String imsi, + @Nullable String iccid, @Nullable String impi, @Nullable List<String> plmnArrayList) { + mMcc = mcc; + mMnc = mnc; + mSpn = spn; + mGid1 = gid1; + mGid2 = gid2; + mImsiPrefix = imsi; + mIccid = iccid; + mImpi = impi; + mEhplmn = plmnArrayList; + } + + /** + * 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. + * @hide + */ + @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}. + * @hide + */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mMcc); + dest.writeString8(mMnc); + dest.writeString8(mSpn); + dest.writeString8(mGid1); + dest.writeString8(mGid2); + dest.writeString8(mImsiPrefix); + dest.writeString8(mIccid); + dest.writeString8(mImpi); + dest.writeStringList(mEhplmn); + } + + /** @hide */ + public CarrierInfo(Parcel in) { + mEhplmn = new ArrayList<String>(); + mMcc = in.readString8(); + mMnc = in.readString8(); + mSpn = in.readString8(); + mGid1 = in.readString8(); + mGid2 = in.readString8(); + mImsiPrefix = in.readString8(); + mIccid = in.readString8(); + mImpi = in.readString8(); + in.readStringList(mEhplmn); + } + + + /** @hide */ + @android.annotation.NonNull + @Override + public String toString() { + return "CarrierInfo MCC = " + mMcc + " MNC = " + mMnc + " SPN = " + mSpn + " GID1 = " + + mGid1 + " GID2 = " + mGid2 + " IMSI = " + getPrintableImsi() + " ICCID = " + + SubscriptionInfo.getPrintableId(mIccid) + " IMPI = " + mImpi + " EHPLMN = [ " + + getEhplmn_toString() + " ]"; + } + + private String getEhplmn_toString() { + return String.join(" ", mEhplmn); + } + + private String getPrintableImsi() { + boolean enablePiiLog = Rlog.isLoggable("CarrierInfo", Log.VERBOSE); + return ((mImsiPrefix != null && mImsiPrefix.length() > 6) ? mImsiPrefix.substring(0, 6) + + Rlog.pii(enablePiiLog, mImsiPrefix.substring(6)) : mImsiPrefix); + } +} diff --git a/telephony/java/android/telephony/CarrierRestrictionRules.java b/telephony/java/android/telephony/CarrierRestrictionRules.java index cc768bc00250..2b0d6261886f 100644 --- a/telephony/java/android/telephony/CarrierRestrictionRules.java +++ b/telephony/java/android/telephony/CarrierRestrictionRules.java @@ -84,13 +84,75 @@ public final class CarrierRestrictionRules implements Parcelable { /** The same configuration is applied to all SIM slots independently. */ public static final int MULTISIM_POLICY_NONE = 0; - /** Any SIM card can be used as far as one SIM card matching the configuration is present. */ + /** + * Indicates that any SIM card can be used as far as one valid card is present in the device. + * For the modem, a SIM card is valid when its content (i.e. MCC, MNC, GID, SPN) matches the + * carrier restriction configuration. + */ public static final int MULTISIM_POLICY_ONE_VALID_SIM_MUST_BE_PRESENT = 1; + /** + * Indicates that the SIM lock policy applies uniformly to all sim slots. + * @hide + */ + public static final int MULTISIM_POLICY_APPLY_TO_ALL_SLOTS = 2; + + /** + * The SIM lock configuration applies exclusively to sim slot 1, leaving + * all other sim slots unlocked irrespective of the SIM card in slot 1 + * @hide + */ + public static final int MULTISIM_POLICY_APPLY_TO_ONLY_SLOT_1 = 3; + + /** + * Valid sim cards must be present on sim slot1 in order + * to use other sim slots. + * @hide + */ + public static final int MULTISIM_POLICY_VALID_SIM_MUST_PRESENT_ON_SLOT_1 = 4; + + /** + * Valid sim card must be present on slot1 and it must be in full service + * in order to use other sim slots. + * @hide + */ + public static final int MULTISIM_POLICY_ACTIVE_SERVICE_ON_SLOT_1_TO_UNBLOCK_OTHER_SLOTS = 5; + + /** + * Valid sim card be present on any slot and it must be in full service + * in order to use other sim slots. + * @hide + */ + public static final int MULTISIM_POLICY_ACTIVE_SERVICE_ON_ANY_SLOT_TO_UNBLOCK_OTHER_SLOTS = 6; + + /** + * Valid sim cards must be present on all slots. If any SIM cards become + * invalid then device would set other SIM cards as invalid as well. + * @hide + */ + public static final int MULTISIM_POLICY_ALL_SIMS_MUST_BE_VALID = 7; + + /** + * In case there is no match policy listed above. + * @hide + */ + public static final int MULTISIM_POLICY_SLOT_POLICY_OTHER = 8; + + + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "MULTISIM_POLICY_", - value = {MULTISIM_POLICY_NONE, MULTISIM_POLICY_ONE_VALID_SIM_MUST_BE_PRESENT}) + value = {MULTISIM_POLICY_NONE, + MULTISIM_POLICY_ONE_VALID_SIM_MUST_BE_PRESENT, + MULTISIM_POLICY_APPLY_TO_ALL_SLOTS, + MULTISIM_POLICY_APPLY_TO_ONLY_SLOT_1, + MULTISIM_POLICY_VALID_SIM_MUST_PRESENT_ON_SLOT_1, + MULTISIM_POLICY_ACTIVE_SERVICE_ON_SLOT_1_TO_UNBLOCK_OTHER_SLOTS, + MULTISIM_POLICY_ACTIVE_SERVICE_ON_ANY_SLOT_TO_UNBLOCK_OTHER_SLOTS, + MULTISIM_POLICY_ALL_SIMS_MUST_BE_VALID, + MULTISIM_POLICY_SLOT_POLICY_OTHER + }) public @interface MultiSimPolicy {} /** @hide */ @@ -104,6 +166,8 @@ public final class CarrierRestrictionRules implements Parcelable { private List<CarrierIdentifier> mAllowedCarriers; private List<CarrierIdentifier> mExcludedCarriers; + private List<CarrierInfo> mAllowedCarrierInfo; + private List<CarrierInfo> mExcludedCarrierInfo; @CarrierRestrictionDefault private int mCarrierRestrictionDefault; @MultiSimPolicy @@ -114,6 +178,8 @@ public final class CarrierRestrictionRules implements Parcelable { private CarrierRestrictionRules() { mAllowedCarriers = new ArrayList<CarrierIdentifier>(); mExcludedCarriers = new ArrayList<CarrierIdentifier>(); + mAllowedCarrierInfo = new ArrayList<CarrierInfo>(); + mExcludedCarrierInfo = new ArrayList<CarrierInfo>(); mCarrierRestrictionDefault = CARRIER_RESTRICTION_DEFAULT_NOT_ALLOWED; mMultiSimPolicy = MULTISIM_POLICY_NONE; mCarrierRestrictionStatus = TelephonyManager.CARRIER_RESTRICTION_STATUS_UNKNOWN; @@ -122,12 +188,17 @@ public final class CarrierRestrictionRules implements Parcelable { private CarrierRestrictionRules(Parcel in) { mAllowedCarriers = new ArrayList<CarrierIdentifier>(); mExcludedCarriers = new ArrayList<CarrierIdentifier>(); - + mAllowedCarrierInfo = new ArrayList<CarrierInfo>(); + mExcludedCarrierInfo = new ArrayList<CarrierInfo>(); in.readTypedList(mAllowedCarriers, CarrierIdentifier.CREATOR); in.readTypedList(mExcludedCarriers, CarrierIdentifier.CREATOR); mCarrierRestrictionDefault = in.readInt(); mMultiSimPolicy = in.readInt(); mCarrierRestrictionStatus = in.readInt(); + if (Flags.carrierRestrictionRulesEnhancement()) { + in.readTypedList(mAllowedCarrierInfo, CarrierInfo.CREATOR); + in.readTypedList(mExcludedCarrierInfo, CarrierInfo.CREATOR); + } } /** @@ -165,6 +236,25 @@ public final class CarrierRestrictionRules implements Parcelable { } /** + * Retrieves list of excluded carrierInfos + * + * @return the list of excluded carrierInfos + * @hide + */ + public @NonNull List<CarrierInfo> getExcludedCarriersInfoList() { + return mExcludedCarrierInfo; + } + + /** + * Retrieves list of excluded carrierInfos + * + * @return the list of excluded carrierInfos + * @hide + */ + public @NonNull List<CarrierInfo> getAllowedCarriersInfoList() { + return mAllowedCarrierInfo; + } + /** * Retrieves the default behavior of carrier restrictions */ public @CarrierRestrictionDefault int getDefaultCarrierRestriction() { @@ -326,6 +416,10 @@ public final class CarrierRestrictionRules implements Parcelable { out.writeInt(mCarrierRestrictionDefault); out.writeInt(mMultiSimPolicy); out.writeInt(mCarrierRestrictionStatus); + if (Flags.carrierRestrictionRulesEnhancement()) { + out.writeTypedList(mAllowedCarrierInfo); + out.writeTypedList(mExcludedCarrierInfo); + } } /** @@ -357,7 +451,16 @@ public final class CarrierRestrictionRules implements Parcelable { public String toString() { return "CarrierRestrictionRules(allowed:" + mAllowedCarriers + ", excluded:" + mExcludedCarriers + ", default:" + mCarrierRestrictionDefault - + ", multisim policy:" + mMultiSimPolicy + ")"; + + ", multisim policy:" + mMultiSimPolicy + getCarrierInfoList() + ")"; + } + + private String getCarrierInfoList() { + if (Flags.carrierRestrictionRulesEnhancement()) { + return ", allowedCarrierInfoList:" + mAllowedCarrierInfo + + ", excludedCarrierInfoList:" + mExcludedCarrierInfo; + } else { + return ""; + } } /** @@ -382,6 +485,12 @@ public final class CarrierRestrictionRules implements Parcelable { mRules.mAllowedCarriers.clear(); mRules.mExcludedCarriers.clear(); mRules.mCarrierRestrictionDefault = CARRIER_RESTRICTION_DEFAULT_ALLOWED; + if (Flags.carrierRestrictionRulesEnhancement()) { + mRules.mCarrierRestrictionStatus = + TelephonyManager.CARRIER_RESTRICTION_STATUS_NOT_RESTRICTED; + mRules.mAllowedCarrierInfo.clear(); + mRules.mExcludedCarrierInfo.clear(); + } return this; } @@ -439,5 +548,29 @@ public final class CarrierRestrictionRules implements Parcelable { mRules.mCarrierRestrictionStatus = carrierRestrictionStatus; return this; } + + /** + * Set list of allowed carrierInfo + * + * @param allowedCarrierInfo list of allowed CarrierInfo + * @hide + */ + public @NonNull Builder setAllowedCarrierInfo( + @NonNull List<CarrierInfo> allowedCarrierInfo) { + mRules.mAllowedCarrierInfo = new ArrayList<CarrierInfo>(allowedCarrierInfo); + return this; + } + + /** + * Set list of allowed carrierInfo + * + * @param excludedCarrierInfo list of allowed CarrierInfo + * @hide + */ + public @NonNull Builder setExcludedCarrierInfo( + @NonNull List<CarrierInfo> excludedCarrierInfo) { + mRules.mExcludedCarrierInfo = new ArrayList<CarrierInfo>(excludedCarrierInfo); + return this; + } } } diff --git a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt index b0ca4d230e12..79d3a10a34cb 100644 --- a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt +++ b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt @@ -17,7 +17,10 @@ package com.android.server.wm.flicker.rotation import android.platform.test.annotations.Presubmit +import android.tools.common.flicker.subject.layers.LayerTraceEntrySubject import android.tools.common.traces.component.ComponentNameMatcher +import android.tools.common.traces.component.IComponentMatcher +import android.tools.common.traces.surfaceflinger.Display import android.tools.device.apphelpers.StandardAppHelper import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest @@ -57,9 +60,8 @@ abstract class RotationTransition(flicker: LegacyFlickerTest) : BaseTest(flicker @Test open fun appLayerRotates_StartingPos() { flicker.assertLayersStart { - this.entry.displays.map { display -> - this.visibleRegion(testApp).coversExactly(display.layerStackSpace) - } + val display = getDisplay(testApp) + this.visibleRegion(testApp).coversAtLeast(display.layerStackSpace) } } @@ -68,12 +70,20 @@ abstract class RotationTransition(flicker: LegacyFlickerTest) : BaseTest(flicker @Test open fun appLayerRotates_EndingPos() { flicker.assertLayersEnd { - this.entry.displays.map { display -> - this.visibleRegion(testApp).coversExactly(display.layerStackSpace) - } + val display = getDisplay(testApp) + this.visibleRegion(testApp).coversAtLeast(display.layerStackSpace) } } + private fun LayerTraceEntrySubject.getDisplay(componentMatcher: IComponentMatcher): Display { + val stackId = this.layer { + componentMatcher.layerMatchesAnyOf(it) && it.isVisible + }?.layer?.stackId ?: -1 + + return this.entry.displays.firstOrNull { it.layerStackId == stackId } + ?: error("Unable to find visible layer for $componentMatcher") + } + override fun cujCompleted() { super.cujCompleted() appLayerRotates_StartingPos() |