diff options
202 files changed, 4059 insertions, 1744 deletions
diff --git a/Android.bp b/Android.bp index d6b303f62428..af312bf833e5 100644 --- a/Android.bp +++ b/Android.bp @@ -425,6 +425,7 @@ java_defaults { "sounddose-aidl-java", "modules-utils-expresslog", "perfetto_trace_javastream_protos_jarjar", + "libaconfig_java_proto_nano", ], } diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp index 13d6ae5be3e8..6cfd2e0ce833 100644 --- a/api/StubLibraries.bp +++ b/api/StubLibraries.bp @@ -44,8 +44,8 @@ non_updatable_exportable_droidstubs { removed_api_file: ":non-updatable-removed.txt", }, last_released: { - api_file: ":android-non-updatable.api.public.latest", - removed_api_file: ":android-non-updatable-removed.api.public.latest", + api_file: ":android-non-updatable.api.combined.public.latest", + removed_api_file: ":android-non-updatable-removed.api.combined.public.latest", baseline_file: ":android-non-updatable-incompatibilities.api.public.latest", }, api_lint: { @@ -124,8 +124,8 @@ non_updatable_exportable_droidstubs { removed_api_file: ":non-updatable-system-removed.txt", }, last_released: { - api_file: ":android-non-updatable.api.system.latest", - removed_api_file: ":android-non-updatable-removed.api.system.latest", + api_file: ":android-non-updatable.api.combined.system.latest", + removed_api_file: ":android-non-updatable-removed.api.combined.system.latest", baseline_file: ":android-non-updatable-incompatibilities.api.system.latest", }, api_lint: { @@ -263,8 +263,8 @@ non_updatable_exportable_droidstubs { removed_api_file: ":non-updatable-module-lib-removed.txt", }, last_released: { - api_file: ":android-non-updatable.api.module-lib.latest", - removed_api_file: ":android-non-updatable-removed.api.module-lib.latest", + api_file: ":android-non-updatable.api.combined.module-lib.latest", + removed_api_file: ":android-non-updatable-removed.api.combined.module-lib.latest", baseline_file: ":android-non-updatable-incompatibilities.api.module-lib.latest", }, api_lint: { diff --git a/core/api/current.txt b/core/api/current.txt index 16de8fe7066c..6c5a5c9e8389 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -26611,7 +26611,7 @@ package android.media.session { public abstract static class MediaController.Callback { ctor public MediaController.Callback(); - method public void onAudioInfoChanged(android.media.session.MediaController.PlaybackInfo); + method public void onAudioInfoChanged(@NonNull android.media.session.MediaController.PlaybackInfo); method public void onExtrasChanged(@Nullable android.os.Bundle); method public void onMetadataChanged(@Nullable android.media.MediaMetadata); method public void onPlaybackStateChanged(@Nullable android.media.session.PlaybackState); diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 9ea55f5ff84c..c6a1546fb931 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -103,7 +103,8 @@ public class ActivityOptions extends ComponentOptions { @IntDef(prefix = {"MODE_BACKGROUND_ACTIVITY_START_"}, value = { MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED, MODE_BACKGROUND_ACTIVITY_START_ALLOWED, - MODE_BACKGROUND_ACTIVITY_START_DENIED}) + MODE_BACKGROUND_ACTIVITY_START_DENIED, + MODE_BACKGROUND_ACTIVITY_START_COMPAT}) public @interface BackgroundActivityStartMode {} /** * No explicit value chosen. The system will decide whether to grant privileges. @@ -117,6 +118,13 @@ public class ActivityOptions extends ComponentOptions { * Deny the {@link PendingIntent} to use the background activity start privileges. */ public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED = 2; + /** + * Special behavior for compatibility. + * Similar to {@link #MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED} + * + * @hide + */ + public static final int MODE_BACKGROUND_ACTIVITY_START_COMPAT = -1; /** * The package name that created the options. diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index d4812dd612c3..76c1ed619510 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -18,6 +18,7 @@ package android.app; import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN; import static android.app.ConfigurationController.createNewConfigAndUpdateIfNotNull; +import static android.app.Flags.skipBgMemTrimOnFgApp; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.servertransaction.ActivityLifecycleItem.ON_CREATE; @@ -7078,6 +7079,11 @@ public final class ActivityThread extends ClientTransactionHandler if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Trimming memory to level: " + level); try { + if (skipBgMemTrimOnFgApp() + && mLastProcessState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND + && level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { + return; + } if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) { PropertyInvalidatedCache.onTrimMemory(); } diff --git a/core/java/android/app/ComponentOptions.java b/core/java/android/app/ComponentOptions.java index 397477d72a9a..0e8e2e30c26f 100644 --- a/core/java/android/app/ComponentOptions.java +++ b/core/java/android/app/ComponentOptions.java @@ -18,6 +18,7 @@ package android.app; import static android.app.ActivityOptions.BackgroundActivityStartMode; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; @@ -54,7 +55,7 @@ public class ComponentOptions { public static final String KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION = "android.pendingIntent.backgroundActivityAllowedByPermission"; - private @Nullable Boolean mPendingIntentBalAllowed = null; + private Integer mPendingIntentBalAllowed = MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; private boolean mPendingIntentBalAllowedByPermission = false; ComponentOptions() { @@ -65,12 +66,9 @@ public class ComponentOptions { // results they want, which is their loss. opts.setDefusable(true); - boolean pendingIntentBalAllowedIsSetExplicitly = - opts.containsKey(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED); - if (pendingIntentBalAllowedIsSetExplicitly) { - mPendingIntentBalAllowed = - opts.getBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED); - } + mPendingIntentBalAllowed = + opts.getInt(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, + MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED); setPendingIntentBackgroundActivityLaunchAllowedByPermission( opts.getBoolean( KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, false)); @@ -85,7 +83,8 @@ public class ComponentOptions { * @hide */ @Deprecated public void setPendingIntentBackgroundActivityLaunchAllowed(boolean allowed) { - mPendingIntentBalAllowed = allowed; + mPendingIntentBalAllowed = allowed ? MODE_BACKGROUND_ACTIVITY_START_ALLOWED + : MODE_BACKGROUND_ACTIVITY_START_DENIED; } /** @@ -98,11 +97,8 @@ public class ComponentOptions { * @hide */ @Deprecated public boolean isPendingIntentBackgroundActivityLaunchAllowed() { - if (mPendingIntentBalAllowed == null) { - // cannot return null, so return the value used up to API level 33 for compatibility - return true; - } - return mPendingIntentBalAllowed; + // cannot return all detail, so return the value used up to API level 33 for compatibility + return mPendingIntentBalAllowed != MODE_BACKGROUND_ACTIVITY_START_DENIED; } /** @@ -119,16 +115,15 @@ public class ComponentOptions { @BackgroundActivityStartMode int state) { switch (state) { case MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED: - mPendingIntentBalAllowed = null; - break; - case MODE_BACKGROUND_ACTIVITY_START_ALLOWED: - mPendingIntentBalAllowed = true; - break; case MODE_BACKGROUND_ACTIVITY_START_DENIED: - mPendingIntentBalAllowed = false; + case MODE_BACKGROUND_ACTIVITY_START_COMPAT: + case MODE_BACKGROUND_ACTIVITY_START_ALLOWED: + mPendingIntentBalAllowed = state; break; default: - throw new IllegalArgumentException(state + " is not valid"); + // Assume that future values are some variant of allowing the start. + mPendingIntentBalAllowed = MODE_BACKGROUND_ACTIVITY_START_ALLOWED; + break; } return this; } @@ -141,13 +136,7 @@ public class ComponentOptions { * @see #setPendingIntentBackgroundActivityStartMode(int) */ public @BackgroundActivityStartMode int getPendingIntentBackgroundActivityStartMode() { - if (mPendingIntentBalAllowed == null) { - return MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; - } else if (mPendingIntentBalAllowed) { - return MODE_BACKGROUND_ACTIVITY_START_ALLOWED; - } else { - return MODE_BACKGROUND_ACTIVITY_START_DENIED; - } + return mPendingIntentBalAllowed; } /** @@ -170,8 +159,8 @@ public class ComponentOptions { /** @hide */ public Bundle toBundle() { Bundle b = new Bundle(); - if (mPendingIntentBalAllowed != null) { - b.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, mPendingIntentBalAllowed); + if (mPendingIntentBalAllowed != MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) { + b.putInt(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, mPendingIntentBalAllowed); } if (mPendingIntentBalAllowedByPermission) { b.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig index bb24fd19315e..fa646a76768c 100644 --- a/core/java/android/app/activity_manager.aconfig +++ b/core/java/android/app/activity_manager.aconfig @@ -70,3 +70,14 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "backstage_power" + name: "skip_bg_mem_trim_on_fg_app" + description: "Skip background memory trim event on foreground processes." + is_fixed_read_only: true + bug: "308927629" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 4154e667360b..ad1ae9881a0c 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -115,6 +115,16 @@ flag { } flag { + name: "hsum_unlock_notification_fix" + namespace: "enterprise" + description: "Using the right userId when starting the work profile unlock flow " + bug: "327350831" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "dumpsys_policy_engine_migration_enabled" namespace: "enterprise" description: "Update DumpSys to include information about migrated APIs in DPE" @@ -318,6 +328,13 @@ flag { } flag { + name: "backup_connected_apps_settings" + namespace: "enterprise" + description: "backup and restore connected work and personal apps user settings across devices" + bug: "175067666" +} + +flag { name: "headless_single_user_compatibility_fix" namespace: "enterprise" description: "Fix for compatibility issue introduced from using single_user mode on pre-Android V builds" @@ -336,3 +353,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "onboarding_consentless_bugreports" + namespace: "enterprise" + description: "Allow subsequent bugreports to skip user consent within a time frame" + bug: "340439309" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/content/IntentSender.java b/core/java/android/content/IntentSender.java index b074e8b0c31b..2e252c12c51d 100644 --- a/core/java/android/content/IntentSender.java +++ b/core/java/android/content/IntentSender.java @@ -60,6 +60,10 @@ import android.util.AndroidException; * {@link android.app.PendingIntent#getIntentSender() PendingIntent.getIntentSender()}. */ public class IntentSender implements Parcelable { + private static final Bundle SEND_INTENT_DEFAULT_OPTIONS = + ActivityOptions.makeBasic().setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT).toBundle(); + @UnsupportedAppUsage private final IIntentSender mTarget; IBinder mWhitelistToken; @@ -161,7 +165,8 @@ public class IntentSender implements Parcelable { */ public void sendIntent(Context context, int code, Intent intent, OnFinished onFinished, Handler handler) throws SendIntentException { - sendIntent(context, code, intent, onFinished, handler, null, null /* options */); + sendIntent(context, code, intent, onFinished, handler, null, + SEND_INTENT_DEFAULT_OPTIONS); } /** @@ -194,7 +199,7 @@ public class IntentSender implements Parcelable { OnFinished onFinished, Handler handler, String requiredPermission) throws SendIntentException { sendIntent(context, code, intent, onFinished, handler, requiredPermission, - null /* options */); + SEND_INTENT_DEFAULT_OPTIONS); } /** diff --git a/core/java/android/content/pm/CrossProfileApps.java b/core/java/android/content/pm/CrossProfileApps.java index 8220313a9197..57ee622de910 100644 --- a/core/java/android/content/pm/CrossProfileApps.java +++ b/core/java/android/content/pm/CrossProfileApps.java @@ -326,6 +326,7 @@ public class CrossProfileApps { * @return whether the specified user is a profile. */ @FlaggedApi(FLAG_ALLOW_QUERYING_PROFILE_TYPE) + @SuppressWarnings("UserHandleName") public boolean isProfile(@NonNull UserHandle userHandle) { // Note that this is not a security check, but rather a check for correct use. // The actual security check is performed by UserManager. @@ -343,6 +344,7 @@ public class CrossProfileApps { * @return whether the specified user is a managed profile. */ @FlaggedApi(FLAG_ALLOW_QUERYING_PROFILE_TYPE) + @SuppressWarnings("UserHandleName") public boolean isManagedProfile(@NonNull UserHandle userHandle) { // Note that this is not a security check, but rather a check for correct use. // The actual security check is performed by UserManager. diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index 885f4c5e8ec4..982224b026bc 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -806,7 +806,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration * * <aside class="note"><b>Note:</b> If the app targets * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} - * or after, The width measurement reflects the window size without excluding insets. + * or after, the width measurement reflects the window size without excluding insets. * Otherwise, the measurement excludes window insets even when the app is displayed edge to edge * using {@link android.view.Window#setDecorFitsSystemWindows(boolean) * Window#setDecorFitsSystemWindows(boolean)}.</aside> diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index e2b409fe35f1..7f3c49dbb580 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -1571,8 +1571,7 @@ public class CameraDeviceImpl extends CameraDevice } // Allow RAW formats, even when not advertised. - if (inputFormat == ImageFormat.RAW_PRIVATE || inputFormat == ImageFormat.RAW10 - || inputFormat == ImageFormat.RAW12 || inputFormat == ImageFormat.RAW_SENSOR) { + if (isRawFormat(inputFormat)) { return true; } @@ -1642,6 +1641,11 @@ public class CameraDeviceImpl extends CameraDevice } } + // Allow RAW formats, even when not advertised. + if (Flags.multiResRawReprocessing() && isRawFormat(inputFormat)) { + return; + } + if (validFormat == false) { throw new IllegalArgumentException("multi-resolution input format " + inputFormat + " is not valid"); @@ -2584,6 +2588,11 @@ public class CameraDeviceImpl extends CameraDevice return mCharacteristics; } + private boolean isRawFormat(int format) { + return (format == ImageFormat.RAW_PRIVATE || format == ImageFormat.RAW10 + || format == ImageFormat.RAW12 || format == ImageFormat.RAW_SENSOR); + } + /** * Listener for binder death. * diff --git a/core/java/android/os/BugreportManager.java b/core/java/android/os/BugreportManager.java index 960e84d671e7..a818df5f0a8e 100644 --- a/core/java/android/os/BugreportManager.java +++ b/core/java/android/os/BugreportManager.java @@ -252,7 +252,8 @@ public final class BugreportManager { params.getMode(), params.getFlags(), dsListener, - isScreenshotRequested); + isScreenshotRequested, + /* skipUserConsent = */ false); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (FileNotFoundException e) { @@ -313,6 +314,7 @@ public final class BugreportManager { bugreportFd.getFileDescriptor(), bugreportFile, /* keepBugreportOnRetrieval = */ false, + /* skipUserConsent = */ false, dsListener); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 2f0d63401e33..80d356614932 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -368,17 +368,18 @@ public class UserManager { public static final String DISALLOW_WIFI_TETHERING = "no_wifi_tethering"; /** - * Specifies if a user is disallowed from being granted admin privileges. + * Restricts a user's ability to possess or grant admin privileges. * - * <p>This restriction limits ability of other admin users to grant admin - * privileges to selected user. + * <p>When set to <code>true</code>, this prevents the user from: + * <ul> + * <li>Becoming an admin</li> + * <li>Giving other users admin privileges</li> + * </ul> * - * <p>This restriction has no effect in a mode that does not allow multiple admins. + * <p>This restriction is only effective in environments where multiple admins are allowed. * - * <p>The default value is <code>false</code>. + * <p>Key for user restrictions. Type: Boolean. Default: <code>false</code>. * - * <p>Key for user restrictions. - * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) * @see DevicePolicyManager#clearUserRestriction(ComponentName, String) * @see #getUserRestrictions() diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 4f5b67c34845..3738c266641b 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -17032,6 +17032,28 @@ public final class Settings { */ public static final String ENABLE_BACK_ANIMATION = "enable_back_animation"; + /** + * An allow list of packages for which the user has granted the permission to communicate + * across profiles. + * + * @hide + */ + @Readable + @FlaggedApi(android.app.admin.flags.Flags.FLAG_BACKUP_CONNECTED_APPS_SETTINGS) + public static final String CONNECTED_APPS_ALLOWED_PACKAGES = + "connected_apps_allowed_packages"; + + /** + * A block list of packages for which the user has denied the permission to communicate + * across profiles. + * + * @hide + */ + @Readable + @FlaggedApi(android.app.admin.flags.Flags.FLAG_BACKUP_CONNECTED_APPS_SETTINGS) + public static final String CONNECTED_APPS_DISALLOWED_PACKAGES = + "connected_apps_disallowed_packages"; + /** @hide */ public static String zenModeToString(int mode) { if (mode == ZEN_MODE_IMPORTANT_INTERRUPTIONS) return "ZEN_MODE_IMPORTANT_INTERRUPTIONS"; if (mode == ZEN_MODE_ALARMS) return "ZEN_MODE_ALARMS"; diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 38ab590b3c46..71066ac7ac39 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -433,7 +433,8 @@ public class DreamService extends Service implements Window.Callback { mTrackingConfirmKey = event.getKeyCode(); } case KeyEvent.ACTION_UP -> { - if (mTrackingConfirmKey != event.getKeyCode()) { + if (mTrackingConfirmKey == null + || mTrackingConfirmKey != event.getKeyCode()) { return true; } diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index cce4f7bf7b1f..a78a417300ab 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -310,6 +310,7 @@ public class DynamicLayout extends Layout { * @see Layout#getUseBoundsForWidth() * @see Layout.Builder#setUseBoundsForWidth(boolean) */ + @SuppressLint("MissingGetterMatchingBuilder") // The base class `Layout` has a getter. @NonNull @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) public Builder setUseBoundsForWidth(boolean useBoundsForWidth) { diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index 3dd3a9ea8baf..95460a3575eb 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -447,6 +447,7 @@ public class StaticLayout extends Layout { * @see Layout#getUseBoundsForWidth() * @see Layout.Builder#setUseBoundsForWidth(boolean) */ + @SuppressLint("MissingGetterMatchingBuilder") // The base class `Layout` has a getter. @NonNull @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) public Builder setUseBoundsForWidth(boolean useBoundsForWidth) { diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 4475418e1e57..6464239eb2fc 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -916,6 +916,12 @@ public final class Display { * {@code getWindowManager()} or {@code getSystemService(Context.WINDOW_SERVICE)}), the * size of the current app window is returned. As a result, in multi-window mode, the * returned size can be smaller than the size of the device screen. + * The returned window size can vary depending on API level: + * <ul> + * <li>API level 35 and above, the window size will be returned. + * <li>API level 34 and below, the window size minus system decoration areas and + * display cutout is returned. + * </ul> * <li>If size is requested from a non-activity context (for example, the application * context, where the WindowManager is accessed by * {@code getApplicationContext().getSystemService(Context.WINDOW_SERVICE)}), the @@ -924,9 +930,10 @@ public final class Display { * <li>API level 29 and below — The size of the entire display (based on * current rotation) minus system decoration areas is returned. * <li>API level 30 and above — The size of the top running activity in the - * current process is returned. If the current process has no running - * activities, the size of the device default display, including system - * decoration areas, is returned. + * current process is returned, system decoration areas exclusion follows the + * behavior defined above, based on the caller's API level. If the current + * process has no running activities, the size of the device default display, + * including system decoration areas, is returned. * </ul> * </ul> * diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index d7f2b01f46ea..fd10a1f5f710 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -303,7 +303,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } /** Not running an animation. */ - @VisibleForTesting + @VisibleForTesting(visibility = PACKAGE) public static final int ANIMATION_TYPE_NONE = -1; /** Running animation will show insets */ @@ -317,7 +317,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation public static final int ANIMATION_TYPE_USER = 2; /** Running animation will resize insets */ - @VisibleForTesting + @VisibleForTesting(visibility = PACKAGE) public static final int ANIMATION_TYPE_RESIZE = 3; @Retention(RetentionPolicy.SOURCE) @@ -1714,7 +1714,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation mImeSourceConsumer.onWindowFocusLost(); } - @VisibleForTesting + @VisibleForTesting(visibility = PACKAGE) public @AnimationType int getAnimationType(@InsetsType int type) { for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner; diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index fdb2a6ee1791..6c670f5d6934 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -17,6 +17,7 @@ package android.view; import static android.view.InsetsController.ANIMATION_TYPE_NONE; +import static android.view.InsetsController.ANIMATION_TYPE_RESIZE; import static android.view.InsetsController.AnimationType; import static android.view.InsetsController.DEBUG; import static android.view.InsetsSourceConsumerProto.ANIMATION_STATE; @@ -31,6 +32,7 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility.PACK import android.annotation.IntDef; import android.annotation.Nullable; +import android.graphics.Point; import android.graphics.Rect; import android.util.Log; import android.util.proto.ProtoOutputStream; @@ -179,10 +181,11 @@ public class InsetsSourceConsumer { mController.notifyVisibilityChanged(); } - // If we have a new leash, make sure visibility is up-to-date, even though we - // didn't want to run an animation above. - if (mController.getAnimationType(mType) == ANIMATION_TYPE_NONE) { - applyRequestedVisibilityToControl(); + // If there is no animation controlling the leash, make sure the visibility and the + // position is up-to-date. + final int animType = mController.getAnimationType(mType); + if (animType == ANIMATION_TYPE_NONE || animType == ANIMATION_TYPE_RESIZE) { + applyRequestedVisibilityAndPositionToControl(); } // Remove the surface that owned by last control when it lost. @@ -371,21 +374,27 @@ public class InsetsSourceConsumer { if (DEBUG) Log.d(TAG, "updateSource: " + newSource); } - private void applyRequestedVisibilityToControl() { - if (mSourceControl == null || mSourceControl.getLeash() == null) { + private void applyRequestedVisibilityAndPositionToControl() { + if (mSourceControl == null) { + return; + } + final SurfaceControl leash = mSourceControl.getLeash(); + if (leash == null) { return; } final boolean requestedVisible = (mController.getRequestedVisibleTypes() & mType) != 0; + final Point surfacePosition = mSourceControl.getSurfacePosition(); try (Transaction t = mTransactionSupplier.get()) { if (DEBUG) Log.d(TAG, "applyRequestedVisibilityToControl: " + requestedVisible); if (requestedVisible) { - t.show(mSourceControl.getLeash()); + t.show(leash); } else { - t.hide(mSourceControl.getLeash()); + t.hide(leash); } // Ensure the alpha value is aligned with the actual requested visibility. - t.setAlpha(mSourceControl.getLeash(), requestedVisible ? 1 : 0); + t.setAlpha(leash, requestedVisible ? 1 : 0); + t.setPosition(leash, surfacePosition.x, surfacePosition.y); t.apply(); } onPerceptible(requestedVisible); diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index f22e8f583e1a..0f54940ba0e5 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -781,7 +781,7 @@ public interface WindowManager extends ViewManager { * <p> * The metrics describe the size of the area the window would occupy with * {@link LayoutParams#MATCH_PARENT MATCH_PARENT} width and height, and the {@link WindowInsets} - * such a window would have. + * such a window would have. The {@link WindowInsets} are not deducted from the bounds. * <p> * The value of this is based on the <b>current</b> windowing state of the system. * @@ -811,7 +811,7 @@ public interface WindowManager extends ViewManager { * <p> * The metrics describe the size of the largest potential area the window might occupy with * {@link LayoutParams#MATCH_PARENT MATCH_PARENT} width and height, and the {@link WindowInsets} - * such a window would have. + * such a window would have. The {@link WindowInsets} are not deducted from the bounds. * <p> * Note that this might still be smaller than the size of the physical display if certain areas * of the display are not available to windows created in this {@link Context}. @@ -4264,11 +4264,9 @@ public interface WindowManager extends ViewManager { * no letterbox is applied."/> * * <p> - * A cutout in the corner is considered to be on the short edge: <br/> - * <img src="{@docRoot}reference/android/images/display_cutout/short_edge/fullscreen_corner_no_letterbox.png" - * height="720" - * alt="Screenshot of a fullscreen activity on a display with a cutout in the corner in - * portrait, no letterbox is applied."/> + * A cutout in the corner can be considered to be on different edge in different device + * rotations. This behavior may vary from device to device. Use this flag is possible to + * letterbox your app if the display cutout is at corner. * * <p> * On the other hand, should the cutout be on the long edge of the display, a letterbox will diff --git a/core/java/android/view/WindowMetrics.java b/core/java/android/view/WindowMetrics.java index 26298bc645ad..8bcc9de118e2 100644 --- a/core/java/android/view/WindowMetrics.java +++ b/core/java/android/view/WindowMetrics.java @@ -101,9 +101,13 @@ public final class WindowMetrics { * Returns the bounds of the area associated with this window or {@code UiContext}. * <p> * <b>Note that the size of the reported bounds can have different size than - * {@link Display#getSize(Point)}.</b> This method reports the window size including all system - * bar areas, while {@link Display#getSize(Point)} reports the area excluding navigation bars - * and display cutout areas. The value reported by {@link Display#getSize(Point)} can be + * {@link Display#getSize(Point)} based on your target API level and calling context.</b> + * This method reports the window size including all system + * bar areas, while {@link Display#getSize(Point)} can report the area excluding navigation bars + * and display cutout areas depending on the calling context and target SDK level. Please refer + * to {@link Display#getSize(Point)} for details. + * <p> + * The value reported by {@link Display#getSize(Point)} excluding system decoration areas can be * obtained by using: * <pre class="prettyprint"> * final WindowMetrics metrics = windowManager.getCurrentWindowMetrics(); diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index da2bf9d7ab38..4de3a7b5ae4b 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -124,6 +124,7 @@ flag { namespace: "accessibility" name: "add_type_window_control" is_exported: true + is_fixed_read_only: true description: "adds new TYPE_WINDOW_CONTROL to AccessibilityWindowInfo for detecting Window Decorations" bug: "320445550" } diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java index 163e43a009e7..6fe0784a2fa5 100644 --- a/core/java/android/window/BackProgressAnimator.java +++ b/core/java/android/window/BackProgressAnimator.java @@ -114,7 +114,6 @@ public class BackProgressAnimator { * dispatches as the progress animation updates. */ public void onBackStarted(BackMotionEvent event, ProgressCallback callback) { - reset(); mLastBackEvent = event; mCallback = callback; mBackAnimationInProgress = true; diff --git a/core/java/android/window/RemoteTransition.java b/core/java/android/window/RemoteTransition.java index 4cc7ec598dbf..15b3c4490f94 100644 --- a/core/java/android/window/RemoteTransition.java +++ b/core/java/android/window/RemoteTransition.java @@ -22,15 +22,12 @@ import android.app.IApplicationThread; import android.os.IBinder; import android.os.Parcelable; -import com.android.internal.util.DataClass; - /** * Represents a remote transition animation and information required to run it (eg. the app thread * that needs to be boosted). * @hide */ -@DataClass(genToString = true, genSetters = true, genAidl = true) -public class RemoteTransition implements Parcelable { +public final class RemoteTransition implements Parcelable { /** The actual remote-transition interface used to run the transition animation. */ private @NonNull IRemoteTransition mRemoteTransition; @@ -41,12 +38,18 @@ public class RemoteTransition implements Parcelable { /** A name for this that can be used for debugging. */ private @Nullable String mDebugName; - /** Constructs with no app thread (animation runs in shell). */ + /** + * Constructs with no app thread (animation runs in shell). + * @hide + */ public RemoteTransition(@NonNull IRemoteTransition remoteTransition) { this(remoteTransition, null /* appThread */, null /* debugName */); } - /** Constructs with no app thread (animation runs in shell). */ + /** + * Constructs with no app thread (animation runs in shell). + * @hide + */ public RemoteTransition(@NonNull IRemoteTransition remoteTransition, @Nullable String debugName) { this(remoteTransition, null /* appThread */, debugName); @@ -57,21 +60,6 @@ public class RemoteTransition implements Parcelable { return mRemoteTransition.asBinder(); } - - - // Code below generated by codegen v1.0.23. - // - // DO NOT MODIFY! - // CHECKSTYLE:OFF Generated code - // - // To regenerate run: - // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/window/RemoteTransition.java - // - // To exclude the generated code from IntelliJ auto-formatting enable (one-time): - // Settings > Editor > Code Style > Formatter Control - //@formatter:off - - /** * Creates a new RemoteTransition. * @@ -81,8 +69,8 @@ public class RemoteTransition implements Parcelable { * The application thread that will be running the remote transition. * @param debugName * A name for this that can be used for debugging. + * @hide */ - @DataClass.Generated.Member public RemoteTransition( @NonNull IRemoteTransition remoteTransition, @Nullable IApplicationThread appThread, @@ -98,16 +86,16 @@ public class RemoteTransition implements Parcelable { /** * The actual remote-transition interface used to run the transition animation. + * @hide */ - @DataClass.Generated.Member public @NonNull IRemoteTransition getRemoteTransition() { return mRemoteTransition; } /** * The application thread that will be running the remote transition. + * @hide */ - @DataClass.Generated.Member public @Nullable IApplicationThread getAppThread() { return mAppThread; } @@ -115,15 +103,14 @@ public class RemoteTransition implements Parcelable { /** * A name for this that can be used for debugging. */ - @DataClass.Generated.Member public @Nullable String getDebugName() { return mDebugName; } /** * The actual remote-transition interface used to run the transition animation. + * @hide */ - @DataClass.Generated.Member public @NonNull RemoteTransition setRemoteTransition(@NonNull IRemoteTransition value) { mRemoteTransition = value; com.android.internal.util.AnnotationValidations.validate( @@ -133,8 +120,8 @@ public class RemoteTransition implements Parcelable { /** * The application thread that will be running the remote transition. + * @hide */ - @DataClass.Generated.Member public @NonNull RemoteTransition setAppThread(@NonNull IApplicationThread value) { mAppThread = value; return this; @@ -143,14 +130,12 @@ public class RemoteTransition implements Parcelable { /** * A name for this that can be used for debugging. */ - @DataClass.Generated.Member public @NonNull RemoteTransition setDebugName(@NonNull String value) { mDebugName = value; return this; } @Override - @DataClass.Generated.Member public String toString() { // You can override field toString logic by defining methods like: // String fieldNameToString() { ... } @@ -163,7 +148,6 @@ public class RemoteTransition implements Parcelable { } @Override - @DataClass.Generated.Member public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { // You can override field parcelling by defining methods like: // void parcelFieldName(Parcel dest, int flags) { ... } @@ -178,12 +162,10 @@ public class RemoteTransition implements Parcelable { } @Override - @DataClass.Generated.Member public int describeContents() { return 0; } /** @hide */ @SuppressWarnings({"unchecked", "RedundantCast"}) - @DataClass.Generated.Member protected RemoteTransition(@NonNull android.os.Parcel in) { // You can override field unparcelling by defining methods like: // static FieldType unparcelFieldName(Parcel in) { ... } @@ -198,11 +180,8 @@ public class RemoteTransition implements Parcelable { NonNull.class, null, mRemoteTransition); this.mAppThread = appThread; this.mDebugName = debugName; - - // onConstructed(); // You can define this method to get a callback } - @DataClass.Generated.Member public static final @NonNull Parcelable.Creator<RemoteTransition> CREATOR = new Parcelable.Creator<RemoteTransition>() { @Override @@ -215,17 +194,4 @@ public class RemoteTransition implements Parcelable { return new RemoteTransition(in); } }; - - @DataClass.Generated( - time = 1678926409863L, - codegenVersion = "1.0.23", - sourceFile = "frameworks/base/core/java/android/window/RemoteTransition.java", - inputSignatures = "private @android.annotation.NonNull android.window.IRemoteTransition mRemoteTransition\nprivate @android.annotation.Nullable android.app.IApplicationThread mAppThread\nprivate @android.annotation.Nullable java.lang.String mDebugName\npublic @android.annotation.Nullable android.os.IBinder asBinder()\nclass RemoteTransition extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genSetters=true, genAidl=true)") - @Deprecated - private void __metadata() {} - - - //@formatter:on - // End of generated code - } diff --git a/core/java/android/window/TaskSnapshot.java b/core/java/android/window/TaskSnapshot.java index a2e3d40e5c6b..f0144cbf0f4a 100644 --- a/core/java/android/window/TaskSnapshot.java +++ b/core/java/android/window/TaskSnapshot.java @@ -33,6 +33,8 @@ import android.os.SystemClock; import android.view.Surface; import android.view.WindowInsetsController; +import com.android.window.flags.Flags; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -334,7 +336,8 @@ public class TaskSnapshot implements Parcelable { */ public synchronized void removeReference(@ReferenceFlags int usage) { mInternalReferences &= ~usage; - if (mInternalReferences == 0 && mSnapshot != null && !mSnapshot.isClosed()) { + if (Flags.releaseSnapshotAggressively() && mInternalReferences == 0 && mSnapshot != null + && !mSnapshot.isClosed()) { mSnapshot.close(); } } diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index bcae571201a8..4ffd88093531 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -54,6 +54,8 @@ import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowManager; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -69,6 +71,7 @@ public final class TransitionInfo implements Parcelable { * Modes are only a sub-set of all the transit-types since they are per-container * @hide */ + @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = { "TRANSIT_" }, value = { TRANSIT_NONE, TRANSIT_OPEN, @@ -102,11 +105,11 @@ public final class TransitionInfo implements Parcelable { /** The container is the display. */ public static final int FLAG_IS_DISPLAY = 1 << 5; + // TODO(b/194540864): Once we can include all windows in transition, then replace this with + // something like FLAG_IS_SYSTEM_ALERT instead. Then we can do mixed rotations. /** * Only for IS_DISPLAY containers. Is set if the display has system alert windows. This is * used to prevent seamless rotation. - * TODO(b/194540864): Once we can include all windows in transition, then replace this with - * something like FLAG_IS_SYSTEM_ALERT instead. Then we can do mixed rotations. */ public static final int FLAG_DISPLAY_HAS_ALERT_WINDOWS = 1 << 7; @@ -173,6 +176,7 @@ public final class TransitionInfo implements Parcelable { public static final int FLAGS_IS_OCCLUDED_NO_ANIMATION = FLAG_IS_OCCLUDED | FLAG_NO_ANIMATION; /** @hide */ + @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = { "FLAG_" }, value = { FLAG_NONE, FLAG_SHOW_WALLPAPER, @@ -267,11 +271,11 @@ public final class TransitionInfo implements Parcelable { } /** @see #getRoot */ - public void addRoot(Root other) { + public void addRoot(@NonNull Root other) { mRoots.add(other); } - public void setAnimationOptions(AnimationOptions options) { + public void setAnimationOptions(@Nullable AnimationOptions options) { mOptions = options; } @@ -336,6 +340,7 @@ public final class TransitionInfo implements Parcelable { return mRoots.get(0).mLeash; } + @Nullable public AnimationOptions getAnimationOptions() { return mOptions; } @@ -601,7 +606,7 @@ public final class TransitionInfo implements Parcelable { * Updates the callsites of all the surfaces in this transition, which aids in the debugging of * lingering surfaces. */ - public void setUnreleasedWarningCallSiteForAllSurfaces(String callsite) { + public void setUnreleasedWarningCallSiteForAllSurfaces(@Nullable String callsite) { for (int i = mChanges.size() - 1; i >= 0; --i) { mChanges.get(i).getLeash().setUnreleasedWarningCallSite(callsite); } @@ -613,6 +618,7 @@ public final class TransitionInfo implements Parcelable { * the caller's references. Use this only if you need to "send" this to a local function which * assumes it is being called from a remote caller. */ + @NonNull public TransitionInfo localRemoteCopy() { final TransitionInfo out = new TransitionInfo(mType, mFlags); out.mTrack = mTrack; @@ -891,7 +897,7 @@ public final class TransitionInfo implements Parcelable { return mTaskInfo; } - public boolean getAllowEnterPip() { + public boolean isAllowEnterPip() { return mAllowEnterPip; } @@ -1042,6 +1048,7 @@ public final class TransitionInfo implements Parcelable { } /** Represents animation options during a transition */ + @SuppressWarnings("UserHandleName") public static final class AnimationOptions implements Parcelable { private int mType; @@ -1061,7 +1068,7 @@ public final class TransitionInfo implements Parcelable { mType = type; } - public AnimationOptions(Parcel in) { + private AnimationOptions(Parcel in) { mType = in.readInt(); mEnterResId = in.readInt(); mExitResId = in.readInt(); @@ -1076,14 +1083,17 @@ public final class TransitionInfo implements Parcelable { } /** Make basic customized animation for a package */ - public static AnimationOptions makeCommonAnimOptions(String packageName) { + @NonNull + public static AnimationOptions makeCommonAnimOptions(@NonNull String packageName) { AnimationOptions options = new AnimationOptions(ANIM_FROM_STYLE); options.mPackageName = packageName; return options; } + /** Make custom animation from the content of LayoutParams */ + @NonNull public static AnimationOptions makeAnimOptionsFromLayoutParameters( - WindowManager.LayoutParams lp) { + @NonNull WindowManager.LayoutParams lp) { AnimationOptions options = new AnimationOptions(ANIM_FROM_STYLE); options.mPackageName = lp.packageName; options.mAnimations = lp.windowAnimations; @@ -1091,7 +1101,7 @@ public final class TransitionInfo implements Parcelable { } /** Add customized window animations */ - public void addOptionsFromLayoutParameters(WindowManager.LayoutParams lp) { + public void addOptionsFromLayoutParameters(@NonNull WindowManager.LayoutParams lp) { mAnimations = lp.windowAnimations; } @@ -1111,8 +1121,11 @@ public final class TransitionInfo implements Parcelable { customTransition.addCustomActivityTransition(enterResId, exitResId, backgroundColor); } - public static AnimationOptions makeCustomAnimOptions(String packageName, int enterResId, - int exitResId, @ColorInt int backgroundColor, boolean overrideTaskTransition) { + /** Make options for a custom animation based on anim resources */ + @NonNull + public static AnimationOptions makeCustomAnimOptions(@NonNull String packageName, + int enterResId, int exitResId, @ColorInt int backgroundColor, + boolean overrideTaskTransition) { AnimationOptions options = new AnimationOptions(ANIM_CUSTOM); options.mPackageName = packageName; options.mEnterResId = enterResId; @@ -1122,6 +1135,8 @@ public final class TransitionInfo implements Parcelable { return options; } + /** Make options for a clip-reveal animation. */ + @NonNull public static AnimationOptions makeClipRevealAnimOptions(int startX, int startY, int width, int height) { AnimationOptions options = new AnimationOptions(ANIM_CLIP_REVEAL); @@ -1129,6 +1144,8 @@ public final class TransitionInfo implements Parcelable { return options; } + /** Make options for a scale-up animation. */ + @NonNull public static AnimationOptions makeScaleUpAnimOptions(int startX, int startY, int width, int height) { AnimationOptions options = new AnimationOptions(ANIM_SCALE_UP); @@ -1136,7 +1153,9 @@ public final class TransitionInfo implements Parcelable { return options; } - public static AnimationOptions makeThumbnailAnimOptions(HardwareBuffer srcThumb, + /** Make options for a thumbnail-scaling animation. */ + @NonNull + public static AnimationOptions makeThumbnailAnimOptions(@NonNull HardwareBuffer srcThumb, int startX, int startY, boolean scaleUp) { AnimationOptions options = new AnimationOptions( scaleUp ? ANIM_THUMBNAIL_SCALE_UP : ANIM_THUMBNAIL_SCALE_DOWN); @@ -1145,11 +1164,15 @@ public final class TransitionInfo implements Parcelable { return options; } + /** Make options for an animation that spans activities of different profiles. */ + @NonNull public static AnimationOptions makeCrossProfileAnimOptions() { AnimationOptions options = new AnimationOptions(ANIM_OPEN_CROSS_PROFILE_APPS); return options; } + /** Make options designating this as a scene-transition animation. */ + @NonNull public static AnimationOptions makeSceneTransitionAnimOptions() { AnimationOptions options = new AnimationOptions(ANIM_SCENE_TRANSITION); return options; @@ -1175,14 +1198,17 @@ public final class TransitionInfo implements Parcelable { return mOverrideTaskTransition; } + @Nullable public String getPackageName() { return mPackageName; } + @NonNull public Rect getTransitionBounds() { return mTransitionBounds; } + @Nullable public HardwareBuffer getThumbnail() { return mThumbnail; } @@ -1192,12 +1218,13 @@ public final class TransitionInfo implements Parcelable { } /** Return customized activity transition if existed. */ + @Nullable public CustomActivityTransition getCustomActivityTransition(boolean open) { return open ? mCustomActivityOpenTransition : mCustomActivityCloseTransition; } @Override - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mType); dest.writeInt(mEnterResId); dest.writeInt(mExitResId); @@ -1247,6 +1274,7 @@ public final class TransitionInfo implements Parcelable { } @Override + @NonNull public String toString() { final StringBuilder sb = new StringBuilder(32); sb.append("{t=").append(typeToString(mType)); @@ -1261,7 +1289,7 @@ public final class TransitionInfo implements Parcelable { } /** Customized activity transition. */ - public static class CustomActivityTransition implements Parcelable { + public static final class CustomActivityTransition implements Parcelable { private int mCustomEnterResId; private int mCustomExitResId; private int mCustomBackgroundColor; @@ -1302,7 +1330,7 @@ public final class TransitionInfo implements Parcelable { } @Override - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mCustomEnterResId); dest.writeInt(mCustomExitResId); dest.writeInt(mCustomBackgroundColor); diff --git a/core/java/android/window/TransitionRequestInfo.java b/core/java/android/window/TransitionRequestInfo.java index bd54e14bc996..cc225767bd49 100644 --- a/core/java/android/window/TransitionRequestInfo.java +++ b/core/java/android/window/TransitionRequestInfo.java @@ -113,7 +113,7 @@ public final class TransitionRequestInfo implements Parcelable { /** Requested change to a display. */ @DataClass(genToString = true, genSetters = true, genBuilder = false, genConstructor = false) - public static class DisplayChange implements Parcelable { + public static final class DisplayChange implements Parcelable { private final int mDisplayId; @Nullable private Rect mStartAbsBounds = null; @Nullable private Rect mEndAbsBounds = null; diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index f94766ebb57e..0590c407d7e4 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -85,3 +85,10 @@ flag { description: "Matches the App Header density to that of the app window, instead of SysUI's" bug: "332414819" } + +flag { + name: "enable_themed_app_headers" + namespace: "lse_desktop_experience" + description: "Makes the App Header style adapt to the system's and app's light/dark theme" + bug: "328668781" +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index ee3e34f2b9e2..f08f5b8fddbe 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -163,4 +163,12 @@ flag { metadata { purpose: PURPOSE_BUGFIX } +} + +flag { + name: "release_snapshot_aggressively" + namespace: "windowing_frontend" + description: "Actively release task snapshot memory" + bug: "238206323" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 84715aa80edb..17adee4cc49e 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -917,7 +917,7 @@ public class ResolverActivity extends Activity implements mSystemWindowInsets = insets.getSystemWindowInsets(); mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, - mSystemWindowInsets.right, 0); + mSystemWindowInsets.right, mSystemWindowInsets.bottom); resetButtonBar(); @@ -946,7 +946,7 @@ public class ResolverActivity extends Activity implements if (mSystemWindowInsets != null) { mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, - mSystemWindowInsets.right, 0); + mSystemWindowInsets.right, mSystemWindowInsets.bottom); } } diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java new file mode 100644 index 000000000000..f306b0b02677 --- /dev/null +++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java @@ -0,0 +1,244 @@ +/* + * 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.internal.pm.pkg.component; + +import static com.android.internal.pm.pkg.parsing.ParsingUtils.ANDROID_RES_NAMESPACE; + +import android.aconfig.nano.Aconfig; +import android.aconfig.nano.Aconfig.parsed_flag; +import android.aconfig.nano.Aconfig.parsed_flags; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.res.Flags; +import android.content.res.XmlResourceParser; +import android.os.Environment; +import android.os.Process; +import android.util.ArrayMap; +import android.util.Slog; +import android.util.Xml; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.modules.utils.TypedXmlPullParser; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * A class that manages a cache of all device feature flags and their default + override values. + * This class performs a very similar job to the one in {@code SettingsProvider}, with an important + * difference: this is a part of system server and is available for the server startup. Package + * parsing happens at the startup when {@code SettingsProvider} isn't available yet, so we need an + * own copy of the code here. + * @hide + */ +public class AconfigFlags { + private static final String LOG_TAG = "AconfigFlags"; + + private static final List<String> sTextProtoFilesOnDevice = List.of( + "/system/etc/aconfig_flags.pb", + "/system_ext/etc/aconfig_flags.pb", + "/product/etc/aconfig_flags.pb", + "/vendor/etc/aconfig_flags.pb"); + + private final ArrayMap<String, Boolean> mFlagValues = new ArrayMap<>(); + + public AconfigFlags() { + if (!Flags.manifestFlagging()) { + Slog.v(LOG_TAG, "Feature disabled, skipped all loading"); + return; + } + for (String fileName : sTextProtoFilesOnDevice) { + try (var inputStream = new FileInputStream(fileName)) { + loadAconfigDefaultValues(inputStream.readAllBytes()); + } catch (IOException e) { + Slog.e(LOG_TAG, "Failed to read Aconfig values from " + fileName, e); + } + } + if (Process.myUid() == Process.SYSTEM_UID) { + // Server overrides are only accessible to the system, no need to even try loading them + // in user processes. + loadServerOverrides(); + } + } + + private void loadServerOverrides() { + // Reading the proto files is enough for READ_ONLY flags but if it's a READ_WRITE flag + // (which you can check with `flag.getPermission() == flag_permission.READ_WRITE`) then we + // also need to check if there is a value pushed from the server in the file + // `/data/system/users/0/settings_config.xml`. It will be in a <setting> node under the + // root <settings> node with "name" attribute == "flag_namespace/flag_package.flag_name". + // The "value" attribute will be true or false. + // + // The "name" attribute could also be "<namespace>/flag_namespace?flag_package.flag_name" + // (prefixed with "staged/" or "device_config_overrides/" and a different separator between + // namespace and name). This happens when a flag value is overridden either with a pushed + // one from the server, or from the local command. + // When the device reboots during package parsing, the staged value will still be there and + // only later it will become a regular/non-staged value after SettingsProvider is + // initialized. + // + // In all cases, when there is more than one value, the priority is: + // device_config_overrides > staged > default + // + + final var settingsFile = new File(Environment.getUserSystemDirectory(0), + "settings_config.xml"); + try (var inputStream = new FileInputStream(settingsFile)) { + TypedXmlPullParser parser = Xml.resolvePullParser(inputStream); + if (parser.next() != XmlPullParser.END_TAG && "settings".equals(parser.getName())) { + final var flagPriority = new ArrayMap<String, Integer>(); + final int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + if (!"setting".equals(parser.getName())) { + continue; + } + String name = parser.getAttributeValue(null, "name"); + final String value = parser.getAttributeValue(null, "value"); + if (name == null || value == null) { + continue; + } + // A non-boolean setting is definitely not an Aconfig flag value. + if (!"false".equalsIgnoreCase(value) && !"true".equalsIgnoreCase(value)) { + continue; + } + final var overridePrefix = "device_config_overrides/"; + final var stagedPrefix = "staged/"; + String separator = "/"; + String prefix = "default"; + int priority = 0; + if (name.startsWith(overridePrefix)) { + prefix = overridePrefix; + name = name.substring(overridePrefix.length()); + separator = ":"; + priority = 20; + } else if (name.startsWith(stagedPrefix)) { + prefix = stagedPrefix; + name = name.substring(stagedPrefix.length()); + separator = "*"; + priority = 10; + } + final String flagPackageAndName = parseFlagPackageAndName(name, separator); + if (flagPackageAndName == null) { + continue; + } + // We ignore all settings that aren't for flags. We'll know they are for flags + // if they correspond to flags read from the proto files. + if (!mFlagValues.containsKey(flagPackageAndName)) { + continue; + } + Slog.d(LOG_TAG, "Found " + prefix + + " Aconfig flag value for " + flagPackageAndName + " = " + value); + final Integer currentPriority = flagPriority.get(flagPackageAndName); + if (currentPriority != null && currentPriority >= priority) { + Slog.i(LOG_TAG, "Skipping " + prefix + " flag " + flagPackageAndName + + " because of the existing one with priority " + currentPriority); + continue; + } + flagPriority.put(flagPackageAndName, priority); + mFlagValues.put(flagPackageAndName, Boolean.parseBoolean(value)); + } + } + } catch (IOException | XmlPullParserException e) { + Slog.e(LOG_TAG, "Failed to read Aconfig values from settings_config.xml", e); + } + } + + private static String parseFlagPackageAndName(String fullName, String separator) { + int index = fullName.indexOf(separator); + if (index < 0) { + return null; + } + return fullName.substring(index + 1); + } + + private void loadAconfigDefaultValues(byte[] fileContents) throws IOException { + parsed_flags parsedFlags = parsed_flags.parseFrom(fileContents); + for (parsed_flag flag : parsedFlags.parsedFlag) { + String flagPackageAndName = flag.package_ + "." + flag.name; + boolean flagValue = (flag.state == Aconfig.ENABLED); + Slog.v(LOG_TAG, "Read Aconfig default flag value " + + flagPackageAndName + " = " + flagValue); + mFlagValues.put(flagPackageAndName, flagValue); + } + } + + /** + * Get the flag value, or null if the flag doesn't exist. + * @param flagPackageAndName Full flag name formatted as 'package.flag' + * @return the current value of the given Aconfig flag, or null if there is no such flag + */ + @Nullable + public Boolean getFlagValue(@NonNull String flagPackageAndName) { + Boolean value = mFlagValues.get(flagPackageAndName); + Slog.d(LOG_TAG, "Aconfig flag value for " + flagPackageAndName + " = " + value); + return value; + } + + /** + * Check if the element in {@code parser} should be skipped because of the feature flag. + * @param parser XML parser object currently parsing an element + * @return true if the element is disabled because of its feature flag + */ + public boolean skipCurrentElement(@NonNull XmlResourceParser parser) { + if (!Flags.manifestFlagging()) { + return false; + } + String featureFlag = parser.getAttributeValue(ANDROID_RES_NAMESPACE, "featureFlag"); + if (featureFlag == null) { + return false; + } + featureFlag = featureFlag.strip(); + boolean negated = false; + if (featureFlag.startsWith("!")) { + negated = true; + featureFlag = featureFlag.substring(1).strip(); + } + final Boolean flagValue = getFlagValue(featureFlag); + if (flagValue == null) { + Slog.w(LOG_TAG, "Skipping element " + parser.getName() + + " due to unknown feature flag " + featureFlag); + return true; + } + // Skip if flag==false && attr=="flag" OR flag==true && attr=="!flag" (negated) + if (flagValue == negated) { + Slog.v(LOG_TAG, "Skipping element " + parser.getName() + + " behind feature flag " + featureFlag + " = " + flagValue); + return true; + } + return false; + } + + /** + * Add Aconfig flag values for testing flagging of manifest entries. + * @param flagValues A map of flag name -> value. + */ + @VisibleForTesting + public void addFlagValuesForTesting(@NonNull Map<String, Boolean> flagValues) { + mFlagValues.putAll(flagValues); + } +} diff --git a/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java b/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java index db08005c833e..8858f9492890 100644 --- a/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java @@ -61,6 +61,9 @@ public class ComponentParseUtils { if (type != XmlPullParser.START_TAG) { continue; } + if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { + continue; + } final ParseResult result; if ("meta-data".equals(parser.getName())) { diff --git a/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java b/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java index 0b045919fb13..bb015812c225 100644 --- a/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java +++ b/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java @@ -27,6 +27,7 @@ import android.util.ArraySet; import com.android.internal.R; import com.android.internal.pm.pkg.parsing.ParsingPackage; +import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -80,6 +81,9 @@ public class InstallConstraintsTagParser { } return input.success(prefixes); } else if (type == XmlPullParser.START_TAG) { + if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { + continue; + } if (parser.getName().equals(TAG_FINGERPRINT_PREFIX)) { ParseResult<String> parsedPrefix = readFingerprintPrefixValue(input, res, parser); diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java index 9f71d88c24bc..55baa532b434 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java @@ -393,6 +393,9 @@ public class ParsedActivityUtils { if (type != XmlPullParser.START_TAG) { continue; } + if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { + continue; + } final ParseResult result; if (parser.getName().equals("intent-filter")) { diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java index 05728eee174f..da48b23a2b81 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java @@ -99,6 +99,9 @@ public class ParsedIntentInfoUtils { if (type != XmlPullParser.START_TAG) { continue; } + if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { + continue; + } final ParseResult result; String nodeName = parser.getName(); @@ -197,6 +200,9 @@ public class ParsedIntentInfoUtils { if (type != XmlPullParser.START_TAG) { continue; } + if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { + continue; + } final ParseResult result; String nodeName = parser.getName(); diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java index 12aff1c6669f..6af2a29822a2 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java @@ -36,6 +36,7 @@ import android.util.Slog; import com.android.internal.R; import com.android.internal.pm.pkg.parsing.ParsingPackage; +import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; import com.android.internal.pm.pkg.parsing.ParsingUtils; import org.xmlpull.v1.XmlPullParser; @@ -173,6 +174,9 @@ public class ParsedProviderUtils { if (type != XmlPullParser.START_TAG) { continue; } + if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { + continue; + } String name = parser.getName(); final ParseResult result; diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java index 4ac542f84226..c68ea2dc8516 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java @@ -34,6 +34,7 @@ import android.os.Build; import com.android.internal.R; import com.android.internal.pm.pkg.parsing.ParsingPackage; +import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; import com.android.internal.pm.pkg.parsing.ParsingUtils; import org.xmlpull.v1.XmlPullParser; @@ -137,6 +138,9 @@ public class ParsedServiceUtils { if (type != XmlPullParser.START_TAG) { continue; } + if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { + continue; + } final ParseResult parseResult; switch (parser.getName()) { diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java index 1dcd893eb143..44fedb11b043 100644 --- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java +++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java @@ -90,6 +90,7 @@ import com.android.internal.R; import com.android.internal.os.ClassLoaderFactory; import com.android.internal.pm.parsing.pkg.ParsedPackage; import com.android.internal.pm.permission.CompatibilityPermissionInfo; +import com.android.internal.pm.pkg.component.AconfigFlags; import com.android.internal.pm.pkg.component.ComponentMutateUtils; import com.android.internal.pm.pkg.component.ComponentParseUtils; import com.android.internal.pm.pkg.component.InstallConstraintsTagParser; @@ -292,6 +293,7 @@ public class ParsingPackageUtils { @NonNull private final List<PermissionManager.SplitPermissionInfo> mSplitPermissionInfos; private final Callback mCallback; + private static final AconfigFlags sAconfigFlags = new AconfigFlags(); public ParsingPackageUtils(String[] separateProcesses, DisplayMetrics displayMetrics, @NonNull List<PermissionManager.SplitPermissionInfo> splitPermissions, @@ -761,6 +763,9 @@ public class ParsingPackageUtils { if (outerDepth + 1 < parser.getDepth() || type != XmlPullParser.START_TAG) { continue; } + if (sAconfigFlags.skipCurrentElement(parser)) { + continue; + } final ParseResult result; String tagName = parser.getName(); @@ -837,6 +842,9 @@ public class ParsingPackageUtils { if (type != XmlPullParser.START_TAG) { continue; } + if (sAconfigFlags.skipCurrentElement(parser)) { + continue; + } ParsedMainComponent mainComponent = null; @@ -980,6 +988,9 @@ public class ParsingPackageUtils { if (type != XmlPullParser.START_TAG) { continue; } + if (sAconfigFlags.skipCurrentElement(parser)) { + continue; + } String tagName = parser.getName(); final ParseResult result; @@ -1599,6 +1610,9 @@ public class ParsingPackageUtils { if (type != XmlPullParser.START_TAG) { continue; } + if (sAconfigFlags.skipCurrentElement(parser)) { + continue; + } final String innerTagName = parser.getName(); if (innerTagName.equals("uses-feature")) { @@ -1839,6 +1853,9 @@ public class ParsingPackageUtils { if (type != XmlPullParser.START_TAG) { continue; } + if (sAconfigFlags.skipCurrentElement(parser)) { + continue; + } if (parser.getName().equals("intent")) { ParseResult<ParsedIntentInfoImpl> result = ParsedIntentInfoUtils.parseIntentInfo( null /*className*/, pkg, res, parser, true /*allowGlobs*/, @@ -2185,6 +2202,9 @@ public class ParsingPackageUtils { if (type != XmlPullParser.START_TAG) { continue; } + if (sAconfigFlags.skipCurrentElement(parser)) { + continue; + } final ParseResult result; String tagName = parser.getName(); @@ -2773,6 +2793,9 @@ public class ParsingPackageUtils { if (type != XmlPullParser.START_TAG) { continue; } + if (sAconfigFlags.skipCurrentElement(parser)) { + continue; + } final String nodeName = parser.getName(); if (nodeName.equals("additional-certificate")) { @@ -3458,4 +3481,11 @@ public class ParsingPackageUtils { @NonNull Set<String> getInstallConstraintsAllowlist(); } + + /** + * Getter for the flags object + */ + public static AconfigFlags getAconfigFlags() { + return sAconfigFlags; + } } diff --git a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java index 1340156321b2..4a3dfbe9c0d3 100644 --- a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java +++ b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java @@ -49,7 +49,7 @@ public class RavenwoodEnvironment { return false; } - public boolean isRunningOnRavenwood$ravenwood() { + private boolean isRunningOnRavenwood$ravenwood() { return true; } } diff --git a/core/jni/Android.bp b/core/jni/Android.bp index d9c40c3ea783..61eaa526116c 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -439,6 +439,7 @@ cc_library_shared_for_libandroid_runtime { "android_database_SQLiteConnection.cpp", "android_database_SQLiteGlobal.cpp", "android_database_SQLiteDebug.cpp", + "android_database_SQLiteRawStatement.cpp", "android_hardware_input_InputApplicationHandle.cpp", "android_os_MessageQueue.cpp", "android_os_Parcel.cpp", diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp index acef609448ad..59d18b8535f5 100644 --- a/core/jni/platform/host/HostRuntime.cpp +++ b/core/jni/platform/host/HostRuntime.cpp @@ -89,6 +89,7 @@ extern int register_android_database_CursorWindow(JNIEnv* env); extern int register_android_database_SQLiteConnection(JNIEnv* env); extern int register_android_database_SQLiteGlobal(JNIEnv* env); extern int register_android_database_SQLiteDebug(JNIEnv* env); +extern int register_android_database_SQLiteRawStatement(JNIEnv* env); extern int register_android_os_FileObserver(JNIEnv* env); extern int register_android_os_MessageQueue(JNIEnv* env); extern int register_android_os_Parcel(JNIEnv* env); @@ -128,6 +129,8 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { REG_JNI(register_android_database_SQLiteConnection)}, {"android.database.sqlite.SQLiteGlobal", REG_JNI(register_android_database_SQLiteGlobal)}, {"android.database.sqlite.SQLiteDebug", REG_JNI(register_android_database_SQLiteDebug)}, + {"android.database.sqlite.SQLiteRawStatement", + REG_JNI(register_android_database_SQLiteRawStatement)}, #endif {"android.content.res.StringBlock", REG_JNI(register_android_content_StringBlock)}, {"android.content.res.XmlBlock", REG_JNI(register_android_content_XmlBlock)}, diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 5fa13bab7b0c..405324bf76af 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -2589,6 +2589,8 @@ <li>The framework will set {@link android.R.attr#statusBarColor}, {@link android.R.attr#navigationBarColor}, and {@link android.R.attr#navigationBarDividerColor} to transparent. + <li>The frameworks will send Configuration no longer considering system insets. + The Configuration will be stable regardless of the system insets change. </ul> <p>If this is true, the edge-to-edge enforcement won't be applied. However, this diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml index 4d7c00991798..67cceb5d5343 100644 --- a/core/res/res/xml/sms_short_codes.xml +++ b/core/res/res/xml/sms_short_codes.xml @@ -60,6 +60,9 @@ <!-- Belgium: 4 digits, plus EU: http://www.mobileweb.be/en/mobileweb/sms-numberplan.asp --> <shortcode country="be" premium="\\d{4}" free="8\\d{3}|116\\d{3}" /> + <!-- Burkina Faso: 1-4 digits (standard system default, not country specific) --> + <shortcode country="bf" pattern="\\d{1,4}" free="3558" /> + <!-- Bulgaria: 4-5 digits, plus EU --> <shortcode country="bg" pattern="\\d{4,5}" premium="18(?:16|423)|19(?:1[56]|35)" free="116\\d{3}|1988|1490" /> @@ -175,8 +178,8 @@ <!-- Israel: 1-5 digits, known premium codes listed --> <shortcode country="il" pattern="\\d{1,5}" premium="4422|4545" free="37477|6681" /> - <!-- Iran: 4-6 digits, known premium codes listed --> - <shortcode country="ir" pattern="\\d{4,6}" free="700791|700792" /> + <!-- Iran: 4-8 digits, known premium codes listed --> + <shortcode country="ir" pattern="\\d{4,8}" free="700791|700792|100016|30008360" /> <!-- Italy: 5 digits (premium=41xxx,42xxx), plus EU: https://www.itu.int/dms_pub/itu-t/oth/02/02/T020200006B0001PDFE.pdf --> @@ -352,7 +355,7 @@ <shortcode country="za" pattern="\\d{1,5}" free="44136|30791|36056|33009" /> <!-- Yemen --> - <shortcode country="ye" pattern="\\d{1,4}" free="5081" /> + <shortcode country="ye" pattern="\\d{1,4}" free="5079" /> <!-- Zimbabwe --> <shortcode country="zw" pattern="\\d{1,5}" free="33679" /> diff --git a/graphics/java/android/graphics/fonts/FontFamily.java b/graphics/java/android/graphics/fonts/FontFamily.java index bd1327657fb0..5a7b0bbca399 100644 --- a/graphics/java/android/graphics/fonts/FontFamily.java +++ b/graphics/java/android/graphics/fonts/FontFamily.java @@ -25,6 +25,7 @@ import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.text.FontConfig; import android.util.SparseIntArray; @@ -151,6 +152,7 @@ public final class FontFamily { * @return A variable font family. null if a variable font cannot be built from the given * fonts. */ + @SuppressLint("BuilderSetStyle") @FlaggedApi(FLAG_NEW_FONTS_FALLBACK_XML) public @Nullable FontFamily buildVariableFamily() { int variableFamilyType = analyzeAndResolveVariableType(mFonts); diff --git a/graphics/java/android/graphics/text/LineBreakConfig.java b/graphics/java/android/graphics/text/LineBreakConfig.java index 7d55928aa656..5a1086cef407 100644 --- a/graphics/java/android/graphics/text/LineBreakConfig.java +++ b/graphics/java/android/graphics/text/LineBreakConfig.java @@ -23,6 +23,7 @@ import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.app.ActivityThread; import android.os.Build; import android.os.LocaleList; @@ -314,6 +315,7 @@ public final class LineBreakConfig implements Parcelable { * @param config an override line break config * @return This {@code Builder}. */ + @SuppressLint("BuilderSetStyle") @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN) public @NonNull Builder merge(@NonNull LineBreakConfig config) { if (config.mLineBreakStyle != LINE_BREAK_STYLE_UNSPECIFIED) { diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java index 785e30d879d2..6ca6517abbb0 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java @@ -322,7 +322,7 @@ public class TransitionUtil { null, new Rect(change.getStartAbsBounds()), taskInfo, - change.getAllowEnterPip(), + change.isAllowEnterPip(), INVALID_WINDOW_TYPE ); target.setWillShowImeOnTarget( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 01364d1de279..696831747865 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -46,6 +46,7 @@ import com.android.wm.shell.pip2.phone.PipTouchHandler; import com.android.wm.shell.pip2.phone.PipTransition; import com.android.wm.shell.pip2.phone.PipTransitionState; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -82,6 +83,7 @@ public abstract class Pip2Module { @Provides static Optional<PipController> providePipController(Context context, ShellInit shellInit, + ShellCommandHandler shellCommandHandler, ShellController shellController, DisplayController displayController, DisplayInsetsController displayInsetsController, @@ -97,9 +99,10 @@ public abstract class Pip2Module { return Optional.empty(); } else { return Optional.ofNullable(PipController.create( - context, shellInit, shellController, displayController, displayInsetsController, - pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler, - taskStackListener, shellTaskOrganizer, pipTransitionState, mainExecutor)); + context, shellInit, shellCommandHandler, shellController, displayController, + displayInsetsController, pipBoundsState, pipBoundsAlgorithm, + pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer, + pipTransitionState, mainExecutor)); } } @@ -129,6 +132,7 @@ public abstract class Pip2Module { @Provides static PipTouchHandler providePipTouchHandler(Context context, ShellInit shellInit, + ShellCommandHandler shellCommandHandler, PhonePipMenuController menuPhoneController, PipBoundsAlgorithm pipBoundsAlgorithm, @NonNull PipBoundsState pipBoundsState, @@ -140,10 +144,10 @@ public abstract class Pip2Module { PipUiEventLogger pipUiEventLogger, @ShellMainThread ShellExecutor mainExecutor, Optional<PipPerfHintController> pipPerfHintControllerOptional) { - return new PipTouchHandler(context, shellInit, menuPhoneController, pipBoundsAlgorithm, - pipBoundsState, pipTransitionState, pipScheduler, sizeSpecSource, pipMotionHelper, - floatingContentCoordinator, pipUiEventLogger, mainExecutor, - pipPerfHintControllerOptional); + return new PipTouchHandler(context, shellInit, shellCommandHandler, menuPhoneController, + pipBoundsAlgorithm, pipBoundsState, pipTransitionState, pipScheduler, + sizeSpecSource, pipMotionHelper, floatingContentCoordinator, pipUiEventLogger, + mainExecutor, pipPerfHintControllerOptional); } @WMSingleton @@ -163,7 +167,7 @@ public abstract class Pip2Module { @WMSingleton @Provides - static PipTransitionState providePipStackListenerController() { - return new PipTransitionState(); + static PipTransitionState providePipTransitionState(@ShellMainThread Handler handler) { + return new PipTransitionState(handler); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java index 6a7d297e83e5..a42ca1905ee7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -16,10 +16,9 @@ package com.android.wm.shell.draganddrop; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED; -import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.content.ClipDescription.EXTRA_ACTIVITY_OPTIONS; @@ -47,7 +46,6 @@ import android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.app.PendingIntent; import android.content.ActivityNotFoundException; -import android.content.ClipData; import android.content.ClipDescription; import android.content.Context; import android.content.Intent; @@ -265,13 +263,14 @@ public class DragAndDropPolicy { final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT); final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic(); baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true); + // Put BAL flags to avoid activity start aborted. + baseActivityOpts.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + baseActivityOpts.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); final Bundle opts = baseActivityOpts.toBundle(); if (session.appData.hasExtra(EXTRA_ACTIVITY_OPTIONS)) { opts.putAll(session.appData.getBundleExtra(EXTRA_ACTIVITY_OPTIONS)); } - // Put BAL flags to avoid activity start aborted. - opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); - opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true); final UserHandle user = session.appData.getParcelableExtra(EXTRA_USER); if (isTask) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index a12882f56eb7..f5afeea3eaef 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -58,9 +58,12 @@ import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ConfigurationChangeListener; +import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import java.io.PrintWriter; + /** * Manages the picture-in-picture (PIP) UI and states for Phones. */ @@ -72,6 +75,7 @@ public class PipController implements ConfigurationChangeListener, private static final String SWIPE_TO_PIP_OVERLAY = "swipe_to_pip_overlay"; private final Context mContext; + private final ShellCommandHandler mShellCommandHandler; private final ShellController mShellController; private final DisplayController mDisplayController; private final DisplayInsetsController mDisplayInsetsController; @@ -111,6 +115,7 @@ public class PipController implements ConfigurationChangeListener, private PipController(Context context, ShellInit shellInit, + ShellCommandHandler shellCommandHandler, ShellController shellController, DisplayController displayController, DisplayInsetsController displayInsetsController, @@ -123,6 +128,7 @@ public class PipController implements ConfigurationChangeListener, PipTransitionState pipTransitionState, ShellExecutor mainExecutor) { mContext = context; + mShellCommandHandler = shellCommandHandler; mShellController = shellController; mDisplayController = displayController; mDisplayInsetsController = displayInsetsController; @@ -146,6 +152,7 @@ public class PipController implements ConfigurationChangeListener, */ public static PipController create(Context context, ShellInit shellInit, + ShellCommandHandler shellCommandHandler, ShellController shellController, DisplayController displayController, DisplayInsetsController displayInsetsController, @@ -162,13 +169,14 @@ public class PipController implements ConfigurationChangeListener, "%s: Device doesn't support Pip feature", TAG); return null; } - return new PipController(context, shellInit, shellController, displayController, - displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, - pipScheduler, taskStackListener, shellTaskOrganizer, pipTransitionState, - mainExecutor); + return new PipController(context, shellInit, shellCommandHandler, shellController, + displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm, + pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer, + pipTransitionState, mainExecutor); } private void onInit() { + mShellCommandHandler.addDumpCallback(this::dump, this); // Ensure that we have the display info in case we get calls to update the bounds before the // listener calls back mPipDisplayLayoutState.setDisplayId(mContext.getDisplayId()); @@ -338,6 +346,14 @@ public class PipController implements ConfigurationChangeListener, } } + private void dump(PrintWriter pw, String prefix) { + final String innerPrefix = " "; + pw.println(TAG); + mPipBoundsAlgorithm.dump(pw, innerPrefix); + mPipBoundsState.dump(pw, innerPrefix); + mPipDisplayLayoutState.dump(pw, innerPrefix); + } + /** * The interface for calls from outside the host process. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java index be10151ca5aa..aed493f2bc8f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java @@ -696,6 +696,19 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, case PipTransitionState.SCHEDULED_BOUNDS_CHANGE: if (!extra.getBoolean(FLING_BOUNDS_CHANGE)) break; + if (mPipBoundsState.getBounds().equals( + mPipBoundsState.getMotionBoundsState().getBoundsInMotion())) { + // Avoid scheduling transitions for bounds that don't change, such transition is + // a no-op and would be aborted. + settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */); + cleanUpHighPerfSessionMaybe(); + // SCHEDULED_BOUNDS_CHANGE can have multiple active listeners making + // actual changes (e.g. PipTouchHandler). So post state update onto handler, + // to run after synchronous dispatch is complete. + mPipTransitionState.postState(PipTransitionState.CHANGED_PIP_BOUNDS); + break; + } + // If touch is turned off and we are in a fling animation, schedule a transition. mWaitingForBoundsChangeTransition = true; mPipScheduler.scheduleAnimateResizePip( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java index b55a41d8808f..7dffe543ec9c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java @@ -514,6 +514,20 @@ public class PipResizeGestureHandler implements switch (newState) { case PipTransitionState.SCHEDULED_BOUNDS_CHANGE: if (!extra.getBoolean(RESIZE_BOUNDS_CHANGE)) break; + + if (mPipBoundsState.getBounds().equals(mLastResizeBounds)) { + // If the bounds are invariant move the destination bounds by a single pixel + // to top/bottom to avoid a no-op transition. This trick helps keep the + // animation a part of the transition. + float snapFraction = mPipBoundsAlgorithm.getSnapFraction( + mPipBoundsState.getBounds()); + + // Move to the top if closer to the bottom edge and vice versa. + boolean inTopHalf = snapFraction < 1.5 || snapFraction > 3.5; + int offsetY = inTopHalf ? 1 : -1; + mLastResizeBounds.offset(0 /* dx */, offsetY); + } + mWaitingForBoundsChangeTransition = true; mPipScheduler.scheduleAnimateResizePip(mLastResizeBounds); break; @@ -527,17 +541,14 @@ public class PipResizeGestureHandler implements PipTransition.PIP_START_TX, SurfaceControl.Transaction.class); Rect destinationBounds = extra.getParcelable( PipTransition.PIP_DESTINATION_BOUNDS, Rect.class); - startTx.setPosition(mPipTransitionState.mPinnedTaskLeash, - destinationBounds.left, destinationBounds.top); startTx.apply(); // All motion operations have actually finished, so make bounds cache updates. + mUpdateResizeBoundsCallback.accept(destinationBounds); cleanUpHighPerfSessionMaybe(); // Setting state to CHANGED_PIP_BOUNDS applies finishTx and notifies Core. mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS); - - mUpdateResizeBoundsCallback.accept(destinationBounds); break; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java index 319d1999a272..56a465a4889a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -64,6 +64,7 @@ import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellInit; import java.io.PrintWriter; @@ -81,6 +82,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha // Allow PIP to resize to a slightly bigger state upon touch private boolean mEnableResize; private final Context mContext; + private final ShellCommandHandler mShellCommandHandler; private final PipBoundsAlgorithm mPipBoundsAlgorithm; @NonNull private final PipBoundsState mPipBoundsState; @NonNull private final PipTransitionState mPipTransitionState; @@ -170,6 +172,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha @SuppressLint("InflateParams") public PipTouchHandler(Context context, ShellInit shellInit, + ShellCommandHandler shellCommandHandler, PhonePipMenuController menuController, PipBoundsAlgorithm pipBoundsAlgorithm, @NonNull PipBoundsState pipBoundsState, @@ -182,6 +185,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha ShellExecutor mainExecutor, Optional<PipPerfHintController> pipPerfHintControllerOptional) { mContext = context; + mShellCommandHandler = shellCommandHandler; mMainExecutor = mainExecutor; mPipPerfHintController = pipPerfHintControllerOptional.orElse(null); mAccessibilityManager = context.getSystemService(AccessibilityManager.class); @@ -235,6 +239,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mEnableResize = res.getBoolean(R.bool.config_pipEnableResizeForMenu); reloadResources(); + mShellCommandHandler.addDumpCallback(this::dump, this); mMotionHelper.init(); mPipResizeGestureHandler.init(); mPipDismissTargetHandler.init(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java index 8204d41a9833..9d599caf13dd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java @@ -19,6 +19,7 @@ package com.android.wm.shell.pip2.phone; import android.annotation.IntDef; import android.graphics.Rect; import android.os.Bundle; +import android.os.Handler; import android.view.SurfaceControl; import android.window.WindowContainerToken; @@ -26,6 +27,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.util.Preconditions; +import com.android.wm.shell.shared.annotations.ShellMainThread; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -109,6 +111,13 @@ public class PipTransitionState { private int mState; // + // Dependencies + // + + @ShellMainThread + private final Handler mMainHandler; + + // // Swipe up to enter PiP related state // @@ -149,6 +158,10 @@ public class PipTransitionState { private final List<PipTransitionStateChangedListener> mCallbacks = new ArrayList<>(); + public PipTransitionState(@ShellMainThread Handler handler) { + mMainHandler = handler; + } + /** * @return the state of PiP in the context of transitions. */ @@ -182,6 +195,32 @@ public class PipTransitionState { } } + /** + * Posts the state update for PiP in the context of transitions onto the main handler. + * + * <p>This is done to guarantee that any callback dispatches for the present state are + * complete. This is relevant for states that have multiple listeners, such as + * <code>SCHEDULED_BOUNDS_CHANGE</code> that helps turn off touch interactions along with + * the actual transition scheduling.</p> + */ + public void postState(@TransitionState int state) { + postState(state, null /* extra */); + } + + /** + * Posts the state update for PiP in the context of transitions onto the main handler. + * + * <p>This is done to guarantee that any callback dispatches for the present state are + * complete. This is relevant for states that have multiple listeners, such as + * <code>SCHEDULED_BOUNDS_CHANGE</code> that helps turn off touch interactions along with + * the actual transition scheduling.</p> + * + * @param extra a bundle passed to the subscribed listeners to resolve/cache extra info. + */ + public void postState(@TransitionState int state, @Nullable Bundle extra) { + mMainHandler.post(() -> setState(state, extra)); + } + private void dispatchPipTransitionStateChanged(@TransitionState int oldState, @TransitionState int newState, @Nullable Bundle extra) { mCallbacks.forEach(l -> l.onPipTransitionStateChanged(oldState, newState, extra)); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 4299088a51f0..9f8cb625fe17 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -16,10 +16,8 @@ package com.android.wm.shell.splitscreen; -import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; import static android.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED; -import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; @@ -48,7 +46,6 @@ import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPos import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.shared.TransitionUtil.isClosingType; -import static com.android.wm.shell.shared.TransitionUtil.isOpeningMode; import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; @@ -1888,13 +1885,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } private void addActivityOptions(Bundle opts, @Nullable StageTaskListener launchTarget) { + ActivityOptions options = ActivityOptions.fromBundle(opts); if (launchTarget != null) { - opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, launchTarget.mRootTaskInfo.token); + options.setLaunchRootTask(launchTarget.mRootTaskInfo.token); } // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split // will be canceled. - opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); - opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true); + options.setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + opts.putAll(options.toBundle()); } void updateActivityOptions(Bundle opts, @SplitPosition int position) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 1be3b02e1465..8a49a73e0253 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -93,8 +93,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL Choreographer choreographer, SyncTransactionQueue syncQueue, ResizeHandleSizeRepository resizeHandleSizeRepository) { - super(context, displayController, taskOrganizer, taskInfo, taskSurface, - taskInfo.getConfiguration()); + super(context, displayController, taskOrganizer, taskInfo, taskSurface); mHandler = handler; mChoreographer = choreographer; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index bb89adfeac1b..9c92791f255f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -140,13 +140,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, - Configuration windowDecorConfig, Handler handler, Choreographer choreographer, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, ResizeHandleSizeRepository resizeHandleSizeRepository) { - this (context, displayController, taskOrganizer, taskInfo, taskSurface, windowDecorConfig, + this (context, displayController, taskOrganizer, taskInfo, taskSurface, handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer, resizeHandleSizeRepository, SurfaceControl.Builder::new, SurfaceControl.Transaction::new, WindowContainerTransaction::new, @@ -159,7 +158,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, - Configuration windowDecorConfig, Handler handler, Choreographer choreographer, SyncTransactionQueue syncQueue, @@ -170,7 +168,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, Supplier<SurfaceControl> surfaceControlSupplier, SurfaceControlViewHostFactory surfaceControlViewHostFactory) { - super(context, displayController, taskOrganizer, taskInfo, taskSurface, windowDecorConfig, + super(context, displayController, taskOrganizer, taskInfo, taskSurface, surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, windowContainerTransactionSupplier, surfaceControlSupplier, surfaceControlViewHostFactory); @@ -964,17 +962,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, ResizeHandleSizeRepository resizeHandleSizeRepository) { - final Configuration windowDecorConfig = - DesktopModeStatus.isDesktopDensityOverrideSet() - ? context.getResources().getConfiguration() // Use system context - : taskInfo.configuration; // Use task configuration return new DesktopModeWindowDecoration( context, displayController, taskOrganizer, taskInfo, taskSurface, - windowDecorConfig, handler, choreographer, syncQueue, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 541825437c86..2cbe47212c63 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -18,6 +18,7 @@ package com.android.wm.shell.windowdecor; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.res.Configuration.DENSITY_DPI_UNDEFINED; import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsets.Type.mandatorySystemGestures; import static android.view.WindowInsets.Type.statusBars; @@ -145,9 +146,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> DisplayController displayController, ShellTaskOrganizer taskOrganizer, RunningTaskInfo taskInfo, - SurfaceControl taskSurface, - Configuration windowDecorConfig) { - this(context, displayController, taskOrganizer, taskInfo, taskSurface, windowDecorConfig, + SurfaceControl taskSurface) { + this(context, displayController, taskOrganizer, taskInfo, taskSurface, SurfaceControl.Builder::new, SurfaceControl.Transaction::new, WindowContainerTransaction::new, SurfaceControl::new, new SurfaceControlViewHostFactory() {}); @@ -159,7 +159,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> ShellTaskOrganizer taskOrganizer, RunningTaskInfo taskInfo, @NonNull SurfaceControl taskSurface, - Configuration windowDecorConfig, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, @@ -176,8 +175,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mSurfaceControlViewHostFactory = surfaceControlViewHostFactory; mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId); - mWindowDecorConfig = windowDecorConfig; - mDecorWindowContext = mContext.createConfigurationContext(mWindowDecorConfig); } /** @@ -220,8 +217,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> outResult.mRootView = rootView; rootView = null; // Clear it just in case we use it accidentally - final int oldDensityDpi = mWindowDecorConfig.densityDpi; - final int oldNightMode = mWindowDecorConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK; + final int oldDensityDpi = mWindowDecorConfig != null + ? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED; + final int oldNightMode = mWindowDecorConfig != null + ? (mWindowDecorConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) + : Configuration.UI_MODE_NIGHT_UNDEFINED; mWindowDecorConfig = params.mWindowDecorConfig != null ? params.mWindowDecorConfig : mTaskInfo.getConfiguration(); final int newDensityDpi = mWindowDecorConfig.densityDpi; @@ -230,7 +230,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> || mDisplay == null || mDisplay.getDisplayId() != mTaskInfo.displayId || oldLayoutResId != mLayoutResId - || oldNightMode != newNightMode) { + || oldNightMode != newNightMode + || mDecorWindowContext == null) { releaseViews(wct); if (!obtainDisplayOrRegisterListener()) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java index bd8ac379b86f..f3f3c37b645d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java @@ -17,6 +17,7 @@ package com.android.wm.shell.pip2; import android.os.Bundle; +import android.os.Handler; import android.os.Parcelable; import android.testing.AndroidTestingRunner; @@ -29,6 +30,7 @@ import junit.framework.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; /** * Unit test against {@link PhoneSizeSpecSource}. @@ -42,9 +44,12 @@ public class PipTransitionStateTest extends ShellTestCase { private PipTransitionState.PipTransitionStateChangedListener mStateChangedListener; private Parcelable mEmptyParcelable; + @Mock + private Handler mMainHandler; + @Before public void setUp() { - mPipTransitionState = new PipTransitionState(); + mPipTransitionState = new PipTransitionState(mMainHandler); mPipTransitionState.setState(PipTransitionState.UNDEFINED); mEmptyParcelable = new Bundle(); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index d7c383523a6f..d18fec2f24ad 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -16,10 +16,7 @@ package com.android.wm.shell.splitscreen; -import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; import static android.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED; -import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -31,8 +28,9 @@ import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.notNull; @@ -45,6 +43,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.app.PendingIntent; import android.content.res.Configuration; import android.graphics.Rect; @@ -54,7 +53,6 @@ import android.os.Looper; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.RemoteTransition; -import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.test.annotation.UiThreadTest; @@ -343,14 +341,14 @@ public class StageCoordinatorTests extends ShellTestCase { @Test public void testAddActivityOptions_addsBackgroundActivitiesFlags() { - Bundle options = mStageCoordinator.resolveStartStage(STAGE_TYPE_MAIN, + Bundle bundle = mStageCoordinator.resolveStartStage(STAGE_TYPE_MAIN, SPLIT_POSITION_UNDEFINED, null /* options */, null /* wct */); + ActivityOptions options = ActivityOptions.fromBundle(bundle); - assertEquals(options.getParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, WindowContainerToken.class), - mMainStage.mRootTaskInfo.token); - assertTrue(options.getBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED)); - assertTrue(options.getBoolean( - KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION)); + assertThat(options.getLaunchRootTask()).isEqualTo(mMainStage.mRootTaskInfo.token); + assertThat(options.getPendingIntentBackgroundActivityStartMode()) + .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission()).isTrue(); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index cff93137ba45..e737861873d2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -346,7 +346,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private DesktopModeWindowDecoration createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo) { return new DesktopModeWindowDecoration(mContext, mMockDisplayController, - mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mConfiguration, + mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, mMockResizeHandleSizeRepository, SurfaceControl.Builder::new, mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index 8b8cd119effd..48310810e8c9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -49,7 +49,6 @@ import static org.mockito.quality.Strictness.LENIENT; import android.app.ActivityManager; import android.content.Context; -import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Point; @@ -134,7 +133,6 @@ public class WindowDecorationTests extends ShellTestCase { private SurfaceControl.Transaction mMockSurfaceControlFinishT; private SurfaceControl.Transaction mMockSurfaceControlAddWindowT; private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams(); - private Configuration mWindowConfiguration = new Configuration(); private int mCaptionMenuWidthId; @Before @@ -303,7 +301,6 @@ public class WindowDecorationTests extends ShellTestCase { taskInfo.isFocused = true; // Density is 2. Shadow radius is 10px. Caption height is 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mWindowConfiguration.densityDpi = taskInfo.configuration.densityDpi; final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); @@ -314,14 +311,16 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockWindowContainerTransaction, never()) .removeInsetsSource(eq(taskInfo.token), any(), anyInt(), anyInt()); + final SurfaceControl.Transaction t2 = mock(SurfaceControl.Transaction.class); + mMockSurfaceControlTransactions.add(t2); taskInfo.isVisible = false; windowDecor.relayout(taskInfo); - final InOrder releaseOrder = inOrder(t, mMockSurfaceControlViewHost); + final InOrder releaseOrder = inOrder(t2, mMockSurfaceControlViewHost); releaseOrder.verify(mMockSurfaceControlViewHost).release(); - releaseOrder.verify(t).remove(captionContainerSurface); - releaseOrder.verify(t).remove(decorContainerSurface); - releaseOrder.verify(t).apply(); + releaseOrder.verify(t2).remove(captionContainerSurface); + releaseOrder.verify(t2).remove(decorContainerSurface); + releaseOrder.verify(t2).apply(); // Expect to remove two insets sources, the caption insets and the mandatory gesture insets. verify(mMockWindowContainerTransaction, Mockito.times(2)) .removeInsetsSource(eq(taskInfo.token), any(), anyInt(), anyInt()); @@ -836,7 +835,7 @@ public class WindowDecorationTests extends ShellTestCase { private TestWindowDecoration createWindowDecoration(ActivityManager.RunningTaskInfo taskInfo) { return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer, - taskInfo, mMockTaskSurface, mWindowConfiguration, + taskInfo, mMockTaskSurface, new MockObjectSupplier<>(mMockSurfaceControlBuilders, () -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))), new MockObjectSupplier<>(mMockSurfaceControlTransactions, @@ -877,16 +876,15 @@ public class WindowDecorationTests extends ShellTestCase { TestWindowDecoration(Context context, DisplayController displayController, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, - Configuration windowConfiguration, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, Supplier<SurfaceControl> surfaceControlSupplier, SurfaceControlViewHostFactory surfaceControlViewHostFactory) { super(context, displayController, taskOrganizer, taskInfo, taskSurface, - windowConfiguration, surfaceControlBuilderSupplier, - surfaceControlTransactionSupplier, windowContainerTransactionSupplier, - surfaceControlSupplier, surfaceControlViewHostFactory); + surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, + windowContainerTransactionSupplier, surfaceControlSupplier, + surfaceControlViewHostFactory); } @Override diff --git a/libs/hwui/jni/BitmapFactory.cpp b/libs/hwui/jni/BitmapFactory.cpp index 3d0a53440bfb..785aef312072 100644 --- a/libs/hwui/jni/BitmapFactory.cpp +++ b/libs/hwui/jni/BitmapFactory.cpp @@ -688,8 +688,8 @@ static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteA static jobject nativeDecodeFileDescriptor(JNIEnv* env, jobject clazz, jobject fileDescriptor, jobject padding, jobject bitmapFactoryOptions, jlong inBitmapHandle, jlong colorSpaceHandle) { -#ifndef __ANDROID__ // LayoutLib for Windows does not support F_DUPFD_CLOEXEC - return nullObjectReturn("Not supported on Windows"); +#ifdef _WIN32 // LayoutLib for Windows does not support F_DUPFD_CLOEXEC + return nullObjectReturn("Not supported on Windows"); #else NPE_CHECK_RETURN_ZERO(env, fileDescriptor); diff --git a/media/java/android/media/LoudnessCodecDispatcher.java b/media/java/android/media/LoudnessCodecDispatcher.java index fa08658a214f..bdd3c7396a5a 100644 --- a/media/java/android/media/LoudnessCodecDispatcher.java +++ b/media/java/android/media/LoudnessCodecDispatcher.java @@ -16,6 +16,9 @@ package android.media; +import static android.media.MediaFormat.KEY_AAC_DRC_ALBUM_MODE; +import static android.media.MediaFormat.KEY_AAC_DRC_ATTENUATION_FACTOR; +import static android.media.MediaFormat.KEY_AAC_DRC_BOOST_FACTOR; import static android.media.MediaFormat.KEY_AAC_DRC_EFFECT_TYPE; import static android.media.MediaFormat.KEY_AAC_DRC_HEAVY_COMPRESSION; import static android.media.MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL; @@ -142,6 +145,18 @@ public class LoudnessCodecDispatcher implements CallbackUtil.DispatcherStub { filteredBundle.putInt(KEY_AAC_DRC_EFFECT_TYPE, bundle.getInt(KEY_AAC_DRC_EFFECT_TYPE)); } + if (bundle.containsKey(KEY_AAC_DRC_BOOST_FACTOR)) { + filteredBundle.putInt(KEY_AAC_DRC_BOOST_FACTOR, + bundle.getInt(KEY_AAC_DRC_BOOST_FACTOR)); + } + if (bundle.containsKey(KEY_AAC_DRC_ATTENUATION_FACTOR)) { + filteredBundle.putInt(KEY_AAC_DRC_ATTENUATION_FACTOR, + bundle.getInt(KEY_AAC_DRC_ATTENUATION_FACTOR)); + } + if (bundle.containsKey(KEY_AAC_DRC_ALBUM_MODE)) { + filteredBundle.putInt(KEY_AAC_DRC_ALBUM_MODE, + bundle.getInt(KEY_AAC_DRC_ALBUM_MODE)); + } return filteredBundle; } diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index 223b432c12af..40592915a3a8 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -109,7 +109,7 @@ public final class MediaProjection { try { final Callback c = Objects.requireNonNull(callback); if (handler == null) { - handler = new Handler(); + handler = new Handler(mContext.getMainLooper()); } mCallbacks.put(c, new CallbackRecord(c, handler)); } catch (NullPointerException e) { diff --git a/media/java/android/media/session/MediaController.java b/media/java/android/media/session/MediaController.java index a4887567b075..70462effaa54 100644 --- a/media/java/android/media/session/MediaController.java +++ b/media/java/android/media/session/MediaController.java @@ -614,12 +614,11 @@ public final class MediaController { } /** - * Override to handle changes to the audio info. + * Signals a change in the session's {@link PlaybackInfo PlaybackInfo}. * - * @param info The current audio info for this session. + * @param playbackInfo The latest known state of the session's playback info. */ - public void onAudioInfoChanged(PlaybackInfo info) { - } + public void onAudioInfoChanged(@NonNull PlaybackInfo playbackInfo) {} } /** @@ -1182,7 +1181,7 @@ public final class MediaController { } @Override - public void onVolumeInfoChanged(PlaybackInfo info) { + public void onVolumeInfoChanged(@NonNull PlaybackInfo info) { MediaController controller = mController.get(); if (controller != null) { controller.postMessage(MSG_UPDATE_VOLUME, info, null); diff --git a/nfc/java/android/nfc/INfcAdapter.aidl b/nfc/java/android/nfc/INfcAdapter.aidl index 7cd7e7ab49a9..7150b54cf7f1 100644 --- a/nfc/java/android/nfc/INfcAdapter.aidl +++ b/nfc/java/android/nfc/INfcAdapter.aidl @@ -91,7 +91,7 @@ interface INfcAdapter boolean enableReaderOption(boolean enable); boolean isObserveModeSupported(); boolean isObserveModeEnabled(); - boolean setObserveMode(boolean enabled); + boolean setObserveMode(boolean enabled, String pkg); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)") boolean setWlcEnabled(boolean enable); diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java index 06098deb8aff..698df28129be 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -1268,8 +1268,12 @@ public final class NfcAdapter { @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) public boolean setObserveModeEnabled(boolean enabled) { + if (mContext == null) { + throw new UnsupportedOperationException("You need a context on NfcAdapter to use the " + + " observe mode APIs"); + } try { - return sService.setObserveMode(enabled); + return sService.setObserveMode(enabled, mContext.getPackageName()); } catch (RemoteException e) { attemptDeadServiceRecovery(e); return false; diff --git a/packages/InputDevices/res/raw/keyboard_layout_french_ca.kcm b/packages/InputDevices/res/raw/keyboard_layout_french_ca.kcm index 03b5c19f8184..723c187d818f 100644 --- a/packages/InputDevices/res/raw/keyboard_layout_french_ca.kcm +++ b/packages/InputDevices/res/raw/keyboard_layout_french_ca.kcm @@ -348,13 +348,13 @@ key COMMA { label: ',' base: ',' shift: '\'' - ralt: '_' + ralt: '\u00af' } key PERIOD { label: '.' base: '.' - ralt: '-' + ralt: '\u00ad' } key SLASH { diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/themes.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/themes.xml index cdd5c2500693..6052be3b52e2 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v35/themes.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v35/themes.xml @@ -20,7 +20,7 @@ <item name="android:colorAccent">@color/settingslib_materialColorPrimary</item> <item name="android:colorBackground">@color/settingslib_materialColorSurfaceContainer</item> <item name="android:textColorPrimary">@color/settingslib_materialColorOnSurface</item> - <item name="android:textColorSecondary">@color/settingslib_materialColorOnSurfaceVariant</item> + <item name="android:textColorSecondary">@color/settingslib_text_color_secondary</item> <item name="android:textColorTertiary">@color/settingslib_materialColorOutline</item> </style> diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionController.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionController.kt new file mode 100644 index 000000000000..9350f98841a4 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionController.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.settingslib.spaprivileged.model.app + +import android.app.AppOpsManager +import android.content.Context +import android.content.pm.ApplicationInfo +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach + +interface IAppOpsPermissionController { + val isAllowedFlow: Flow<Boolean> + fun setAllowed(allowed: Boolean) +} + +class AppOpsPermissionController( + context: Context, + private val app: ApplicationInfo, + appOps: AppOps, + private val permission: String, + private val packageManagers: IPackageManagers = PackageManagers, + private val appOpsController: IAppOpsController = AppOpsController(context, app, appOps), +) : IAppOpsPermissionController { + override val isAllowedFlow: Flow<Boolean> = appOpsController.modeFlow.map { mode -> + when (mode) { + AppOpsManager.MODE_ALLOWED -> true + + AppOpsManager.MODE_DEFAULT -> { + with(packageManagers) { app.hasGrantPermission(permission) } + } + + else -> false + } + }.conflate().onEach { Log.d(TAG, "isAllowed: $it") }.flowOn(Dispatchers.Default) + + override fun setAllowed(allowed: Boolean) { + appOpsController.setAllowed(allowed) + } + + private companion object { + private const val TAG = "AppOpsPermissionControl" + } +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt index 37b1d731d548..120b75ecb666 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt @@ -20,13 +20,14 @@ import android.app.AppOpsManager import android.content.Context import android.content.pm.ApplicationInfo import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settingslib.spa.framework.util.asyncMapItem import com.android.settingslib.spa.framework.util.filterItem import com.android.settingslib.spaprivileged.model.app.AppOps -import com.android.settingslib.spaprivileged.model.app.AppOpsController +import com.android.settingslib.spaprivileged.model.app.AppOpsPermissionController import com.android.settingslib.spaprivileged.model.app.AppRecord -import com.android.settingslib.spaprivileged.model.app.IAppOpsController +import com.android.settingslib.spaprivileged.model.app.IAppOpsPermissionController import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.model.app.PackageManagers import kotlinx.coroutines.flow.Flow @@ -37,7 +38,7 @@ data class AppOpPermissionRecord( override val app: ApplicationInfo, val hasRequestBroaderPermission: Boolean, val hasRequestPermission: Boolean, - var appOpsController: IAppOpsController, + var appOpsPermissionController: IAppOpsPermissionController, ) : AppRecord abstract class AppOpPermissionListModel( @@ -70,8 +71,8 @@ abstract class AppOpPermissionListModel( private val notChangeablePackages = setOf("android", "com.android.systemui", context.packageName) - private fun createAppOpsController(app: ApplicationInfo) = - AppOpsController(context, app, appOps) + private fun createAppOpsPermissionController(app: ApplicationInfo) = + AppOpsPermissionController(context, app, appOps, permission) private fun createRecord( app: ApplicationInfo, @@ -84,7 +85,7 @@ abstract class AppOpPermissionListModel( app.hasRequestPermission(it) } ?: false, hasRequestPermission = hasRequestPermission, - appOpsController = createAppOpsController(app), + appOpsPermissionController = createAppOpsPermissionController(app), ) } @@ -117,14 +118,20 @@ abstract class AppOpPermissionListModel( override fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<AppOpPermissionRecord>>) = recordListFlow.filterItem(::isChangeable) + /** + * Defining the default behavior as permissible as long as the package requested this permission + * (This means pre-M gets approval during install time; M apps gets approval during runtime). + */ @Composable - override fun isAllowed(record: AppOpPermissionRecord): () -> Boolean? = - isAllowed( - record = record, - appOpsController = record.appOpsController, - permission = permission, - packageManagers = packageManagers, - ) + override fun isAllowed(record: AppOpPermissionRecord): () -> Boolean? { + if (record.hasRequestBroaderPermission) { + // Broader permission trumps the specific permission. + return { true } + } + val isAllowed by record.appOpsPermissionController.isAllowedFlow + .collectAsStateWithLifecycle(initialValue = null) + return { isAllowed } + } override fun isChangeable(record: AppOpPermissionRecord) = record.hasRequestPermission && @@ -132,36 +139,6 @@ abstract class AppOpPermissionListModel( record.app.packageName !in notChangeablePackages override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) { - record.appOpsController.setAllowed(newAllowed) - } -} - -/** - * Defining the default behavior as permissible as long as the package requested this permission - * (This means pre-M gets approval during install time; M apps gets approval during runtime). - */ -@Composable -internal fun isAllowed( - record: AppOpPermissionRecord, - appOpsController: IAppOpsController, - permission: String, - packageManagers: IPackageManagers = PackageManagers, -): () -> Boolean? { - if (record.hasRequestBroaderPermission) { - // Broader permission trumps the specific permission. - return { true } - } - - val mode = appOpsController.modeFlow.collectAsStateWithLifecycle(initialValue = null) - return { - when (mode.value) { - null -> null - AppOpsManager.MODE_ALLOWED -> true - AppOpsManager.MODE_DEFAULT -> { - with(packageManagers) { record.app.hasGrantPermission(permission) } - } - - else -> false - } + record.appOpsPermissionController.setAllowed(newAllowed) } } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionControllerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionControllerTest.kt new file mode 100644 index 000000000000..9f80b92548d2 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionControllerTest.kt @@ -0,0 +1,167 @@ +/* + * 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.app.AppOpsManager +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.spaprivileged.framework.common.appOpsManager +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify + +@RunWith(AndroidJUnit4::class) +class AppOpsPermissionControllerTest { + + private val appOpsManager = mock<AppOpsManager>() + private val packageManager = mock<PackageManager>() + private val packageManagers = mock<IPackageManagers>() + private val appOpsController = mock<IAppOpsController>() + + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { + on { appOpsManager } doReturn appOpsManager + on { packageManager } doReturn packageManager + } + + @Test + fun isAllowedFlow_appOpsAllowed_returnTrue() = runBlocking { + appOpsController.stub { + on { modeFlow } doReturn flowOf(AppOpsManager.MODE_ALLOWED) + } + val controller = AppOpsPermissionController( + context = context, + app = APP, + appOps = AppOps(op = OP), + permission = PERMISSION, + appOpsController = appOpsController, + ) + + val isAllowed = controller.isAllowedFlow.firstWithTimeoutOrNull() + + assertThat(isAllowed).isTrue() + } + + @Test + fun isAllowedFlow_appOpsDefaultAndPermissionGranted_returnTrue() = runBlocking { + appOpsController.stub { + on { modeFlow } doReturn flowOf(AppOpsManager.MODE_DEFAULT) + } + packageManagers.stub { + on { APP.hasGrantPermission(PERMISSION) } doReturn true + } + val controller = AppOpsPermissionController( + context = context, + app = APP, + appOps = AppOps(op = OP), + permission = PERMISSION, + packageManagers = packageManagers, + appOpsController = appOpsController, + ) + + val isAllowed = controller.isAllowedFlow.firstWithTimeoutOrNull() + + assertThat(isAllowed).isTrue() + } + + @Test + fun isAllowedFlow_appOpsDefaultAndPermissionNotGranted_returnFalse() = runBlocking { + appOpsController.stub { + on { modeFlow } doReturn flowOf(AppOpsManager.MODE_DEFAULT) + } + packageManagers.stub { + on { APP.hasGrantPermission(PERMISSION) } doReturn false + } + val controller = AppOpsPermissionController( + context = context, + app = APP, + appOps = AppOps(op = OP), + permission = PERMISSION, + packageManagers = packageManagers, + appOpsController = appOpsController, + ) + + val isAllowed = controller.isAllowedFlow.firstWithTimeoutOrNull() + + assertThat(isAllowed).isFalse() + } + + @Test + fun isAllowedFlow_appOpsError_returnFalse() = runBlocking { + appOpsController.stub { + on { modeFlow } doReturn flowOf(AppOpsManager.MODE_ERRORED) + } + val controller = AppOpsPermissionController( + context = context, + app = APP, + appOps = AppOps(op = OP), + permission = PERMISSION, + appOpsController = appOpsController, + ) + + val isAllowed = controller.isAllowedFlow.firstWithTimeoutOrNull() + + assertThat(isAllowed).isFalse() + } + + @Test + fun setAllowed_notSetModeByUid() { + val controller = AppOpsPermissionController( + context = context, + app = APP, + appOps = AppOps(op = OP, setModeByUid = false), + permission = PERMISSION, + ) + + controller.setAllowed(true) + + verify(appOpsManager).setMode(OP, APP.uid, APP.packageName, AppOpsManager.MODE_ALLOWED) + } + + @Test + fun setAllowed_setModeByUid() { + val controller = AppOpsPermissionController( + context = context, + app = APP, + appOps = AppOps(op = OP, setModeByUid = true), + permission = PERMISSION, + ) + + controller.setAllowed(true) + + verify(appOpsManager).setUidMode(OP, APP.uid, AppOpsManager.MODE_ALLOWED) + } + + private companion object { + const val OP = 1 + const val PERMISSION = "Permission" + val APP = ApplicationInfo().apply { + packageName = "package.name" + uid = 123 + } + } +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt index 07ccdd5a3695..9d12fc7611cb 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt @@ -26,7 +26,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull import com.android.settingslib.spaprivileged.framework.common.appOpsManager import com.android.settingslib.spaprivileged.model.app.AppOps -import com.android.settingslib.spaprivileged.model.app.IAppOpsController +import com.android.settingslib.spaprivileged.model.app.IAppOpsPermissionController import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.test.R import com.google.common.truth.Truth.assertThat @@ -119,7 +119,7 @@ class AppOpPermissionAppListTest { app = APP, hasRequestBroaderPermission = false, hasRequestPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val recordListFlow = listModel.filter(flowOf(USER_ID), flowOf(listOf(record))) @@ -135,7 +135,7 @@ class AppOpPermissionAppListTest { app = APP, hasRequestBroaderPermission = false, hasRequestPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ALLOWED), + appOpsPermissionController = FakeAppOpsPermissionController(true), ) val isAllowed = getIsAllowed(record) @@ -144,38 +144,6 @@ class AppOpPermissionAppListTest { } @Test - fun isAllowed_defaultAndHasGrantPermission() { - with(packageManagers) { whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(true) } - val record = - AppOpPermissionRecord( - app = APP, - hasRequestBroaderPermission = false, - hasRequestPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), - ) - - val isAllowed = getIsAllowed(record) - - assertThat(isAllowed).isTrue() - } - - @Test - fun isAllowed_defaultAndNotGrantPermission() { - with(packageManagers) { whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(false) } - val record = - AppOpPermissionRecord( - app = APP, - hasRequestBroaderPermission = false, - hasRequestPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), - ) - - val isAllowed = getIsAllowed(record) - - assertThat(isAllowed).isFalse() - } - - @Test fun isAllowed_broaderPermissionTrumps() { listModel.broaderPermission = BROADER_PERMISSION with(packageManagers) { @@ -187,7 +155,7 @@ class AppOpPermissionAppListTest { app = APP, hasRequestBroaderPermission = true, hasRequestPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ERRORED), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isAllowed = getIsAllowed(record) @@ -202,7 +170,7 @@ class AppOpPermissionAppListTest { app = APP, hasRequestBroaderPermission = false, hasRequestPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ERRORED), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isAllowed = getIsAllowed(record) @@ -217,7 +185,7 @@ class AppOpPermissionAppListTest { app = APP, hasRequestBroaderPermission = false, hasRequestPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isChangeable = listModel.isChangeable(record) @@ -232,7 +200,7 @@ class AppOpPermissionAppListTest { app = NOT_CHANGEABLE_APP, hasRequestBroaderPermission = false, hasRequestPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isChangeable = listModel.isChangeable(record) @@ -247,7 +215,7 @@ class AppOpPermissionAppListTest { app = APP, hasRequestBroaderPermission = false, hasRequestPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isChangeable = listModel.isChangeable(record) @@ -263,7 +231,7 @@ class AppOpPermissionAppListTest { app = APP, hasRequestBroaderPermission = true, hasRequestPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isChangeable = listModel.isChangeable(record) @@ -273,18 +241,18 @@ class AppOpPermissionAppListTest { @Test fun setAllowed() { - val appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val appOpsPermissionController = FakeAppOpsPermissionController(false) val record = AppOpPermissionRecord( app = APP, hasRequestBroaderPermission = false, hasRequestPermission = true, - appOpsController = appOpsController, + appOpsPermissionController = appOpsPermissionController, ) listModel.setAllowed(record = record, newAllowed = true) - assertThat(appOpsController.setAllowedCalledWith).isTrue() + assertThat(appOpsPermissionController.setAllowedCalledWith).isTrue() } private fun getIsAllowed(record: AppOpPermissionRecord): Boolean? { @@ -314,14 +282,12 @@ class AppOpPermissionAppListTest { } } -private class FakeAppOpsController(private val fakeMode: Int) : IAppOpsController { +private class FakeAppOpsPermissionController(allowed: Boolean) : IAppOpsPermissionController { var setAllowedCalledWith: Boolean? = null - override val modeFlow = flowOf(fakeMode) + override val isAllowedFlow = flowOf(allowed) override fun setAllowed(allowed: Boolean) { setAllowedCalledWith = allowed } - - override fun getMode() = fakeMode } diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig index 9c0d29df420f..32557b979bf5 100644 --- a/packages/SettingsLib/aconfig/settingslib.aconfig +++ b/packages/SettingsLib/aconfig/settingslib.aconfig @@ -69,3 +69,13 @@ flag { description: "Allow all widgets on the lock screen by default." bug: "328261690" } + +flag { + name: "enable_determining_advanced_details_header_with_metadata" + namespace: "pixel_cross_device_control" + description: "Use metadata instead of device type to determine whether a bluetooth device should use advanced details header." + bug: "328556903" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig b/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig index 7aae1a6b4a59..6f614b372ea6 100644 --- a/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig +++ b/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig @@ -31,3 +31,14 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "use_playback_info_for_routing_controls" + namespace: "media_solutions" + description: "Use app-provided playback info when providing media routing information." + bug: "333564788" + metadata { + purpose: PURPOSE_BUGFIX + } +} + diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index 89174125a296..721e7b93fd32 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -276,6 +276,14 @@ public class BluetoothUtils { if (isUntetheredHeadset(bluetoothDevice)) { return true; } + if (Flags.enableDeterminingAdvancedDetailsHeaderWithMetadata()) { + // A FastPair device that use advanced details header must have METADATA_MAIN_ICON + if (getUriMetaData(bluetoothDevice, BluetoothDevice.METADATA_MAIN_ICON) != null) { + Log.d(TAG, "isAdvancedDetailsHeader is true with main icon uri"); + return true; + } + return false; + } // The metadata is for Android S String deviceType = getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE); @@ -302,12 +310,15 @@ public class BluetoothUtils { if (isUntetheredHeadset(bluetoothDevice)) { return true; } - // The metadata is for Android S - String deviceType = - getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE); - if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)) { - Log.d(TAG, "isAdvancedUntetheredDevice: is untethered device "); - return true; + if (!Flags.enableDeterminingAdvancedDetailsHeaderWithMetadata()) { + // The METADATA_IS_UNTETHERED_HEADSET of an untethered FastPair headset is always true, + // so there's no need to check the device type. + String deviceType = + getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE); + if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)) { + Log.d(TAG, "isAdvancedUntetheredDevice: is untethered device"); + return true; + } } return false; } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java index eae58adb5381..b7758de0e19c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java @@ -40,6 +40,7 @@ import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; import static android.media.MediaRoute2Info.TYPE_USB_HEADSET; import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; +import static android.media.session.MediaController.PlaybackInfo; import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED; @@ -51,6 +52,8 @@ import android.content.Context; import android.media.MediaRoute2Info; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.session.MediaController; +import android.media.session.MediaSession; import android.os.Build; import android.os.UserHandle; import android.text.TextUtils; @@ -135,19 +138,28 @@ public abstract class InfoMediaManager { @NonNull protected final UserHandle mUserHandle; private final Collection<MediaDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>(); private MediaDevice mCurrentConnectedDevice; + private MediaController mMediaController; + private PlaybackInfo mLastKnownPlaybackInfo; private final LocalBluetoothManager mBluetoothManager; private final Map<String, RouteListingPreference.Item> mPreferenceItemMap = new ConcurrentHashMap<>(); + private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback(); + /* package */ InfoMediaManager( @NonNull Context context, @NonNull String packageName, @NonNull UserHandle userHandle, - @NonNull LocalBluetoothManager localBluetoothManager) { + @NonNull LocalBluetoothManager localBluetoothManager, + @Nullable MediaController mediaController) { mContext = context; mBluetoothManager = localBluetoothManager; mPackageName = packageName; mUserHandle = userHandle; + mMediaController = mediaController; + if (mediaController != null) { + mLastKnownPlaybackInfo = mediaController.getPlaybackInfo(); + } } /** @@ -159,12 +171,19 @@ public abstract class InfoMediaManager { * speakers, as opposed to app-specific routing (for example, casting to another device). * @param userHandle The {@link UserHandle} of the user on which the app to control is running, * or null if the caller does not need app-specific routing (see {@code packageName}). + * @param token The token of the associated {@link MediaSession} for which to do media routing. */ public static InfoMediaManager createInstance( Context context, @Nullable String packageName, @Nullable UserHandle userHandle, - LocalBluetoothManager localBluetoothManager) { + LocalBluetoothManager localBluetoothManager, + @Nullable MediaSession.Token token) { + MediaController mediaController = null; + + if (Flags.usePlaybackInfoForRoutingControls() && token != null) { + mediaController = new MediaController(context, token); + } // The caller is only interested in system routes (headsets, built-in speakers, etc), and is // not interested in a specific app's routing. The media routing APIs still require a @@ -180,16 +199,16 @@ public abstract class InfoMediaManager { if (Flags.useMediaRouter2ForInfoMediaManager()) { try { return new RouterInfoMediaManager( - context, packageName, userHandle, localBluetoothManager); + context, packageName, userHandle, localBluetoothManager, mediaController); } catch (PackageNotAvailableException ex) { // TODO: b/293578081 - Propagate this exception to callers for proper handling. Log.w(TAG, "Returning a no-op InfoMediaManager for package " + packageName); return new NoOpInfoMediaManager( - context, packageName, userHandle, localBluetoothManager); + context, packageName, userHandle, localBluetoothManager, mediaController); } } else { return new ManagerInfoMediaManager( - context, packageName, userHandle, localBluetoothManager); + context, packageName, userHandle, localBluetoothManager, mediaController); } } @@ -310,6 +329,9 @@ public abstract class InfoMediaManager { if (wasEmpty) { mMediaDevices.clear(); registerRouter(); + if (mMediaController != null) { + mMediaController.registerCallback(mMediaControllerCallback); + } updateRouteListingPreference(); refreshDevices(); } @@ -323,6 +345,9 @@ public abstract class InfoMediaManager { */ public final void unregisterCallback(@NonNull MediaDeviceCallback callback) { if (mCallbacks.remove(callback) && mCallbacks.isEmpty()) { + if (mMediaController != null) { + mMediaController.unregisterCallback(mMediaControllerCallback); + } unregisterRouter(); } } @@ -389,7 +414,34 @@ public abstract class InfoMediaManager { private RoutingSessionInfo getActiveRoutingSession() { // List is never empty. final List<RoutingSessionInfo> sessions = getRoutingSessionsForPackage(); - return sessions.get(sessions.size() - 1); + RoutingSessionInfo activeSession = sessions.get(sessions.size() - 1); + + // Logic from MediaRouter2Manager#getRoutingSessionForMediaController + if (!Flags.usePlaybackInfoForRoutingControls() || mMediaController == null) { + return activeSession; + } + + PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo(); + if (playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_LOCAL) { + // Return system session. + return sessions.get(0); + } + + // For PLAYBACK_TYPE_REMOTE. + String volumeControlId = playbackInfo.getVolumeControlId(); + for (RoutingSessionInfo session : sessions) { + if (TextUtils.equals(volumeControlId, session.getId())) { + return session; + } + // Workaround for provider not being able to know the unique session ID. + if (TextUtils.equals(volumeControlId, session.getOriginalId()) + && TextUtils.equals( + mMediaController.getPackageName(), session.getOwnerPackageName())) { + return session; + } + } + + return activeSession; } boolean isRoutingSessionAvailableForVolumeControl() { @@ -808,4 +860,23 @@ public abstract class InfoMediaManager { } } } + + private final class MediaControllerCallback extends MediaController.Callback { + @Override + public void onSessionDestroyed() { + mMediaController = null; + refreshDevices(); + } + + @Override + public void onAudioInfoChanged(@NonNull PlaybackInfo info) { + if (info.getPlaybackType() != mLastKnownPlaybackInfo.getPlaybackType() + || !TextUtils.equals( + info.getVolumeControlId(), + mLastKnownPlaybackInfo.getVolumeControlId())) { + refreshDevices(); + } + mLastKnownPlaybackInfo = info; + } + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java index 473c62704b8b..cfa825bbb1c4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java @@ -149,7 +149,11 @@ public class LocalMediaManager implements BluetoothCallback { // TODO: b/321969740 - Take the userHandle as a parameter and pass it through. The // package name is not sufficient to unambiguously identify an app. InfoMediaManager.createInstance( - context, packageName, /* userHandle */ null, mLocalBluetoothManager); + context, + packageName, + /* userHandle */ null, + mLocalBluetoothManager, + /* token */ null); } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java index d621751a2c29..82b197682459 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java @@ -21,6 +21,7 @@ import android.media.MediaRoute2Info; import android.media.MediaRouter2Manager; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.session.MediaController; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; @@ -55,8 +56,9 @@ public class ManagerInfoMediaManager extends InfoMediaManager { Context context, @NonNull String packageName, @NonNull UserHandle userHandle, - LocalBluetoothManager localBluetoothManager) { - super(context, packageName, userHandle, localBluetoothManager); + LocalBluetoothManager localBluetoothManager, + @Nullable MediaController mediaController) { + super(context, packageName, userHandle, localBluetoothManager, mediaController); mRouterManager = MediaRouter2Manager.getInstance(context); } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java index d2b018cd2299..2c7ec9302117 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java @@ -20,6 +20,7 @@ import android.content.Context; import android.media.MediaRoute2Info; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.session.MediaController; import android.os.UserHandle; import androidx.annotation.NonNull; @@ -60,8 +61,9 @@ import java.util.List; Context context, @NonNull String packageName, @NonNull UserHandle userHandle, - LocalBluetoothManager localBluetoothManager) { - super(context, packageName, userHandle, localBluetoothManager); + LocalBluetoothManager localBluetoothManager, + @Nullable MediaController mediaController) { + super(context, packageName, userHandle, localBluetoothManager, mediaController); } @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java index 045c60dd1514..6571dd7ba398 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java @@ -25,6 +25,7 @@ import android.media.MediaRouter2Manager; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.session.MediaController; import android.os.UserHandle; import android.text.TextUtils; @@ -71,9 +72,10 @@ public final class RouterInfoMediaManager extends InfoMediaManager { Context context, @NonNull String packageName, @NonNull UserHandle userHandle, - LocalBluetoothManager localBluetoothManager) + LocalBluetoothManager localBluetoothManager, + @Nullable MediaController mediaController) throws PackageNotAvailableException { - super(context, packageName, userHandle, localBluetoothManager); + super(context, packageName, userHandle, localBluetoothManager, mediaController); MediaRouter2 router = null; diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.java b/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.java index 23b2cc2df794..89f3cf5e9aab 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.java +++ b/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.java @@ -278,7 +278,7 @@ public class MediaSessions { } @Override - public void onAudioInfoChanged(PlaybackInfo info) { + public void onAudioInfoChanged(@NonNull PlaybackInfo info) { if (D.BUG) { Log.d(TAG, cb("onAudioInfoChanged") + Util.playbackInfoToString(info) + " sentRemote=" + sentRemote); diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/InfoMediaManagerIntegTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/InfoMediaManagerIntegTest.java index 3bd37a2c59bf..a2ee2ec9185e 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/InfoMediaManagerIntegTest.java +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/InfoMediaManagerIntegTest.java @@ -65,7 +65,7 @@ public class InfoMediaManagerIntegTest { public void createInstance_withMR2FlagOn_returnsRouterInfoMediaManager() { InfoMediaManager manager = InfoMediaManager.createInstance( - mContext, mContext.getPackageName(), mContext.getUser(), null); + mContext, mContext.getPackageName(), mContext.getUser(), null, null); assertThat(manager).isInstanceOf(RouterInfoMediaManager.class); } @@ -73,14 +73,15 @@ public class InfoMediaManagerIntegTest { @RequiresFlagsEnabled(FLAG_USE_MEDIA_ROUTER2_FOR_INFO_MEDIA_MANAGER) public void createInstance_withMR2FlagOn_withFakePackage_returnsNoOpInfoMediaManager() { InfoMediaManager manager = - InfoMediaManager.createInstance(mContext, FAKE_PACKAGE, null, null); + InfoMediaManager.createInstance(mContext, FAKE_PACKAGE, null, null, null); assertThat(manager).isInstanceOf(NoOpInfoMediaManager.class); } @Test @RequiresFlagsEnabled(FLAG_USE_MEDIA_ROUTER2_FOR_INFO_MEDIA_MANAGER) public void createInstance_withMR2FlagOn_withNullPackage_returnsRouterInfoMediaManager() { - InfoMediaManager manager = InfoMediaManager.createInstance(mContext, null, null, null); + InfoMediaManager manager = + InfoMediaManager.createInstance(mContext, null, null, null, null); assertThat(manager).isInstanceOf(RouterInfoMediaManager.class); } @@ -89,7 +90,7 @@ public class InfoMediaManagerIntegTest { public void createInstance_withMR2FlagOff_returnsManagerInfoMediaManager() { InfoMediaManager manager = InfoMediaManager.createInstance( - mContext, mContext.getPackageName(), mContext.getUser(), null); + mContext, mContext.getPackageName(), mContext.getUser(), null, null); assertThat(manager).isInstanceOf(ManagerInfoMediaManager.class); } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java index 7a2818dbf299..a638df524740 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java @@ -15,6 +15,8 @@ */ package com.android.settingslib.bluetooth; +import static com.android.settingslib.flags.Flags.FLAG_ENABLE_DETERMINING_ADVANCED_DETAILS_HEADER_WITH_METADATA; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -33,11 +35,13 @@ import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.net.Uri; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.Pair; import com.android.settingslib.widget.AdaptiveIcon; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; @@ -83,11 +87,15 @@ public class BluetoothUtilsTest { private static final String TEST_EXCLUSIVE_MANAGER_PACKAGE = "com.test.manager"; private static final String TEST_EXCLUSIVE_MANAGER_COMPONENT = "com.test.manager/.component"; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Before public void setUp() { MockitoAnnotations.initMocks(this); mContext = spy(RuntimeEnvironment.application); + mSetFlagsRule.disableFlags(FLAG_ENABLE_DETERMINING_ADVANCED_DETAILS_HEADER_WITH_METADATA); when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager); when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant); @@ -253,6 +261,25 @@ public class BluetoothUtilsTest { } @Test + public void isAdvancedDetailsHeader_noMainIcon_returnFalse() { + mSetFlagsRule.enableFlags(FLAG_ENABLE_DETERMINING_ADVANCED_DETAILS_HEADER_WITH_METADATA); + + when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_MAIN_ICON)).thenReturn(null); + + assertThat(BluetoothUtils.isAdvancedDetailsHeader(mBluetoothDevice)).isFalse(); + } + + @Test + public void isAdvancedDetailsHeader_hasMainIcon_returnTrue() { + mSetFlagsRule.enableFlags(FLAG_ENABLE_DETERMINING_ADVANCED_DETAILS_HEADER_WITH_METADATA); + + when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_MAIN_ICON)) + .thenReturn(STRING_METADATA.getBytes()); + + assertThat(BluetoothUtils.isAdvancedDetailsHeader(mBluetoothDevice)).isTrue(); + } + + @Test public void isAdvancedUntetheredDevice_untetheredHeadset_returnTrue() { when(mBluetoothDevice.getMetadata( BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn( @@ -294,6 +321,18 @@ public class BluetoothUtilsTest { } @Test + public void isAdvancedUntetheredDevice_untetheredHeadsetMetadataIsFalse_returnFalse() { + mSetFlagsRule.enableFlags(FLAG_ENABLE_DETERMINING_ADVANCED_DETAILS_HEADER_WITH_METADATA); + + when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) + .thenReturn("false".getBytes()); + when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE)) + .thenReturn(BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET.getBytes()); + + assertThat(BluetoothUtils.isAdvancedUntetheredDevice(mBluetoothDevice)).isFalse(); + } + + @Test public void isAvailableMediaBluetoothDevice_isConnectedLeAudioDevice_returnTrue() { when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java index 69faddf48f19..ce07fe9fdf0a 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java @@ -146,7 +146,11 @@ public class InfoMediaManagerTest { Context.MEDIA_SESSION_SERVICE); mInfoMediaManager = new ManagerInfoMediaManager( - mContext, TEST_PACKAGE_NAME, mContext.getUser(), mLocalBluetoothManager); + mContext, + TEST_PACKAGE_NAME, + mContext.getUser(), + mLocalBluetoothManager, + /* mediaController */ null); mShadowRouter2Manager = ShadowRouter2Manager.getShadow(); mInfoMediaManager.mRouterManager = MediaRouter2Manager.getInstance(mContext); } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java index ddb5419509d7..12541bb51cc8 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java @@ -117,9 +117,16 @@ public class LocalMediaManagerTest { when(mLocalProfileManager.getHearingAidProfile()).thenReturn(mHapProfile); // Need to call constructor to initialize final fields. - mInfoMediaManager = mock( - InfoMediaManager.class, - withSettings().useConstructor(mContext, TEST_PACKAGE_NAME, mLocalBluetoothManager)); + mInfoMediaManager = + mock( + InfoMediaManager.class, + withSettings() + .useConstructor( + mContext, + TEST_PACKAGE_NAME, + android.os.Process.myUserHandle(), + mLocalBluetoothManager, + /* mediaController */ null)); doReturn( List.of( new RoutingSessionInfo.Builder(TEST_SESSION_ID, TEST_PACKAGE_NAME) diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/NoOpInfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/NoOpInfoMediaManagerTest.java index 908f50deea78..c566741d19fc 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/NoOpInfoMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/NoOpInfoMediaManagerTest.java @@ -47,7 +47,8 @@ public class NoOpInfoMediaManagerTest { mContext, /* packageName */ "FAKE_PACKAGE_NAME", mContext.getUser(), - /* localBluetoothManager */ null); + /* localBluetoothManager */ null, + /* mediaController */ null); } @Test diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java index 8f8445d7a40b..63e98de6826b 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java @@ -38,6 +38,8 @@ public class GlobalSettings { * NOTE: All settings which are backed up should have a corresponding validator. */ public static final String[] SETTINGS_TO_BACKUP = { + Settings.Global.CONNECTED_APPS_ALLOWED_PACKAGES, + Settings.Global.CONNECTED_APPS_DISALLOWED_PACKAGES, Settings.Global.APPLY_RAMPING_RINGER, Settings.Global.BUGREPORT_IN_POWER_MENU, // moved to secure Settings.Global.STAY_ON_WHILE_PLUGGED_IN, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java index c274534c8d36..bc0ee7fea6ef 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java @@ -51,6 +51,9 @@ public class GlobalSettingsValidators { public static final Map<String, Validator> VALIDATORS = new ArrayMap<>(); static { + VALIDATORS.put(Global.CONNECTED_APPS_ALLOWED_PACKAGES, new PackageNameListValidator((","))); + VALIDATORS.put(Global.CONNECTED_APPS_DISALLOWED_PACKAGES, + new PackageNameListValidator((","))); VALIDATORS.put(Global.APPLY_RAMPING_RINGER, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.BUGREPORT_IN_POWER_MENU, BOOLEAN_VALIDATOR); VALIDATORS.put( diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java index 4579168d2784..050a3704df1f 100644 --- a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java +++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java @@ -200,7 +200,7 @@ public class BugreportReceiverTest { mBugreportFd = ParcelFileDescriptor.dup(invocation.getArgument(2)); return null; }).when(mMockIDumpstate).startBugreport(anyInt(), any(), any(), any(), anyInt(), anyInt(), - any(), anyBoolean()); + any(), anyBoolean(), anyBoolean()); setWarningState(mContext, STATE_HIDE); @@ -543,7 +543,7 @@ public class BugreportReceiverTest { getInstrumentation().waitForIdleSync(); verify(mMockIDumpstate, times(1)).startBugreport(anyInt(), any(), any(), any(), - anyInt(), anyInt(), any(), anyBoolean()); + anyInt(), anyInt(), any(), anyBoolean(), anyBoolean()); sendBugreportFinished(); } @@ -608,7 +608,7 @@ public class BugreportReceiverTest { ArgumentCaptor<IDumpstateListener> listenerCap = ArgumentCaptor.forClass( IDumpstateListener.class); verify(mMockIDumpstate, timeout(TIMEOUT)).startBugreport(anyInt(), any(), any(), any(), - anyInt(), anyInt(), listenerCap.capture(), anyBoolean()); + anyInt(), anyInt(), listenerCap.capture(), anyBoolean(), anyBoolean()); mIDumpstateListener = listenerCap.getValue(); assertNotNull("Dumpstate listener should not be null", mIDumpstateListener); mIDumpstateListener.onProgress(0); diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 8b60ed035d07..c4929a101cce 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -766,6 +766,7 @@ android_robolectric_test { ], static_libs: [ "RoboTestLibraries", + "mockito-kotlin2", ], libs: [ "android.test.runner", diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt index 15df1be02f56..76ffc8b379ae 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt @@ -68,13 +68,17 @@ constructor( @Composable private fun Content(dialog: SystemUIDialog) { val isAvailable by viewModel.isAvailable.collectAsStateWithLifecycle(true) - if (!isAvailable) { SideEffect { dialog.dismiss() } return } val slice by viewModel.popupSlice.collectAsStateWithLifecycle() + if (!viewModel.isClickable(slice)) { + SideEffect { dialog.dismiss() } + return + } + SliceAndroidView( modifier = Modifier.fillMaxWidth(), slice = slice, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt index f61ddeb47514..68fbd1c44ad7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt @@ -20,9 +20,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.accessibility.data.repository.fakeAccessibilityRepository +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.AuthenticationResult +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository @@ -30,14 +36,16 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel.Companion.UNLOCKED_DELAY_MS import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat -import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Test import org.junit.runner.RunWith @ExperimentalCoroutinesApi @@ -65,9 +73,10 @@ class DeviceEntryIconViewModelTest : SysuiTestCase() { fun isLongPressEnabled_udfpsRunning() = testScope.runTest { val isLongPressEnabled by collectLastValue(underTest.isLongPressEnabled) - fingerprintPropertyRepository.supportsUdfps() - fingerprintAuthRepository.setIsRunning(true) - keyguardRepository.setKeyguardDismissible(false) + setUpState( + isUdfpsSupported = true, + isUdfpsRunning = true, + ) assertThat(isLongPressEnabled).isFalse() } @@ -75,10 +84,10 @@ class DeviceEntryIconViewModelTest : SysuiTestCase() { fun isLongPressEnabled_unlocked() = testScope.runTest { val isLongPressEnabled by collectLastValue(underTest.isLongPressEnabled) - fingerprintPropertyRepository.supportsUdfps() - keyguardRepository.setKeyguardDismissible(true) - advanceTimeBy(UNLOCKED_DELAY_MS * 2) // wait for unlocked delay - runCurrent() + setUpState( + isUdfpsSupported = true, + isLockscreenDismissible = true, + ) assertThat(isLongPressEnabled).isTrue() } @@ -86,10 +95,9 @@ class DeviceEntryIconViewModelTest : SysuiTestCase() { fun isLongPressEnabled_lock() = testScope.runTest { val isLongPressEnabled by collectLastValue(underTest.isLongPressEnabled) - keyguardRepository.setKeyguardDismissible(false) + setUpState(isUdfpsSupported = true) // udfps supported - fingerprintPropertyRepository.supportsUdfps() assertThat(isLongPressEnabled).isTrue() // udfps isn't supported @@ -112,42 +120,90 @@ class DeviceEntryIconViewModelTest : SysuiTestCase() { } @Test + @DisableSceneContainer fun iconType_fingerprint() = testScope.runTest { val iconType by collectLastValue(underTest.iconType) - keyguardRepository.setKeyguardDismissible(false) - fingerprintPropertyRepository.supportsUdfps() - fingerprintAuthRepository.setIsRunning(true) + setUpState( + isUdfpsSupported = true, + isUdfpsRunning = true, + ) assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.FINGERPRINT) } @Test + @DisableSceneContainer fun iconType_locked() = testScope.runTest { val iconType by collectLastValue(underTest.iconType) - keyguardRepository.setKeyguardDismissible(false) - fingerprintAuthRepository.setIsRunning(false) + setUpState() assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.LOCK) } @Test + @DisableSceneContainer fun iconType_unlocked() = testScope.runTest { val iconType by collectLastValue(underTest.iconType) - keyguardRepository.setKeyguardDismissible(true) - advanceTimeBy(UNLOCKED_DELAY_MS * 2) // wait for unlocked delay - fingerprintAuthRepository.setIsRunning(false) + setUpState(isLockscreenDismissible = true) assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.UNLOCK) } @Test + @DisableSceneContainer fun iconType_none() = testScope.runTest { val iconType by collectLastValue(underTest.iconType) - keyguardRepository.setKeyguardDismissible(true) - advanceTimeBy(UNLOCKED_DELAY_MS * 2) // wait for unlocked delay - fingerprintPropertyRepository.supportsUdfps() - fingerprintAuthRepository.setIsRunning(true) + setUpState( + isUdfpsSupported = true, + isUdfpsRunning = true, + isLockscreenDismissible = true, + ) + assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.NONE) + } + + @Test + @EnableSceneContainer + fun iconType_fingerprint_withSceneContainer() = + testScope.runTest { + val iconType by collectLastValue(underTest.iconType) + setUpState( + isUdfpsSupported = true, + isUdfpsRunning = true, + ) + assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.FINGERPRINT) + } + + @Test + @EnableSceneContainer + fun iconType_locked_withSceneContainer() = + testScope.runTest { + val iconType by collectLastValue(underTest.iconType) + setUpState() + assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.LOCK) + } + + @Test + @EnableSceneContainer + fun iconType_unlocked_withSceneContainer() = + testScope.runTest { + val iconType by collectLastValue(underTest.iconType) + setUpState( + isLockscreenDismissible = true, + ) + assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.UNLOCK) + } + + @Test + @EnableSceneContainer + fun iconType_none_withSceneContainer() = + testScope.runTest { + val iconType by collectLastValue(underTest.iconType) + setUpState( + isUdfpsSupported = true, + isUdfpsRunning = true, + isLockscreenDismissible = true, + ) assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.NONE) } @@ -166,14 +222,12 @@ class DeviceEntryIconViewModelTest : SysuiTestCase() { kosmos.fakeAccessibilityRepository.isEnabled.value = true // interactive lock icon - keyguardRepository.setKeyguardDismissible(false) - fingerprintPropertyRepository.supportsUdfps() + setUpState(isUdfpsSupported = true) assertThat(accessibilityDelegateHint) .isEqualTo(DeviceEntryIconView.AccessibilityHintType.AUTHENTICATE) // non-interactive lock icon - keyguardRepository.setKeyguardDismissible(false) fingerprintPropertyRepository.supportsRearFps() assertThat(accessibilityDelegateHint) @@ -187,10 +241,10 @@ class DeviceEntryIconViewModelTest : SysuiTestCase() { kosmos.fakeAccessibilityRepository.isEnabled.value = true // interactive unlock icon - keyguardRepository.setKeyguardDismissible(true) - fingerprintPropertyRepository.supportsUdfps() - advanceTimeBy(UNLOCKED_DELAY_MS * 2) // wait for unlocked delay - runCurrent() + setUpState( + isUdfpsSupported = true, + isLockscreenDismissible = true, + ) assertThat(accessibilityDelegateHint) .isEqualTo(DeviceEntryIconView.AccessibilityHintType.ENTER) @@ -199,4 +253,45 @@ class DeviceEntryIconViewModelTest : SysuiTestCase() { private fun deviceEntryIconTransitionAlpha(alpha: Float) { deviceEntryIconTransition.setDeviceEntryParentViewAlpha(alpha) } + + private suspend fun TestScope.setUpState( + isUdfpsSupported: Boolean = false, + isUdfpsRunning: Boolean = false, + isLockscreenDismissible: Boolean = false, + ) { + if (isUdfpsSupported) { + fingerprintPropertyRepository.supportsUdfps() + } + if (isUdfpsRunning) { + check(isUdfpsSupported) { "Cannot set UDFPS as running if it's not supported!" } + fingerprintAuthRepository.setIsRunning(true) + } else { + fingerprintAuthRepository.setIsRunning(false) + } + if (isLockscreenDismissible) { + setLockscreenDismissible() + } else { + if (!SceneContainerFlag.isEnabled) { + keyguardRepository.setKeyguardDismissible(false) + } + } + runCurrent() + } + + private suspend fun TestScope.setLockscreenDismissible() { + if (SceneContainerFlag.isEnabled) { + // Need to set up a collection for the authentication to be propagated. + val unused by collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus) + runCurrent() + assertThat( + kosmos.authenticationInteractor.authenticate( + FakeAuthenticationRepository.DEFAULT_PIN + ) + ) + .isEqualTo(AuthenticationResult.SUCCEEDED) + } else { + keyguardRepository.setKeyguardDismissible(true) + } + advanceTimeBy(UNLOCKED_DELAY_MS * 2) // wait for unlocked delay + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt index 365a7c3a296a..856c3fe19d73 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt @@ -195,6 +195,7 @@ class MediaControlInteractorTest : SysuiTestCase() { eq(PACKAGE_NAME), eq(true), eq(dialogTransitionController), + eq(null), eq(null) ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt index bba9991883f5..8b4265f552fe 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt @@ -23,8 +23,14 @@ 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.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.kosmos.testScope +import com.android.systemui.shade.shadeTestUtil import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository +import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications @@ -41,9 +47,19 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) class HeadsUpNotificationInteractorTest : SysuiTestCase() { - private val kosmos = testKosmos() + private val kosmos = + testKosmos().apply { + fakeKeyguardTransitionRepository = + FakeKeyguardTransitionRepository(initInLockscreen = false) + } private val testScope = kosmos.testScope - private val repository = kosmos.headsUpNotificationRepository + private val faceAuthRepository by lazy { kosmos.fakeDeviceEntryFaceAuthRepository } + private val headsUpRepository by lazy { kosmos.headsUpNotificationRepository } + private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } + private val keyguardViewStateRepository by lazy { + kosmos.notificationsKeyguardViewStateRepository + } + private val shadeTestUtil by lazy { kosmos.shadeTestUtil } private val underTest = kosmos.headsUpNotificationInteractor @@ -60,7 +76,7 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { testScope.runTest { val hasPinnedRows by collectLastValue(underTest.hasPinnedRows) // WHEN no pinned rows are set - repository.setNotifications( + headsUpRepository.setNotifications( fakeHeadsUpRowRepository("key 0"), fakeHeadsUpRowRepository("key 1"), fakeHeadsUpRowRepository("key 2"), @@ -76,7 +92,7 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { testScope.runTest { val hasPinnedRows by collectLastValue(underTest.hasPinnedRows) // WHEN a pinned rows is set - repository.setNotifications( + headsUpRepository.setNotifications( fakeHeadsUpRowRepository("key 0", isPinned = true), fakeHeadsUpRowRepository("key 1"), fakeHeadsUpRowRepository("key 2"), @@ -98,7 +114,7 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { fakeHeadsUpRowRepository("key 1"), fakeHeadsUpRowRepository("key 2"), ) - repository.setNotifications(rows) + headsUpRepository.setNotifications(rows) runCurrent() // WHEN a row gets pinned @@ -120,7 +136,7 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { fakeHeadsUpRowRepository("key 1"), fakeHeadsUpRowRepository("key 2"), ) - repository.setNotifications(rows) + headsUpRepository.setNotifications(rows) runCurrent() // THEN that row gets unpinned @@ -144,7 +160,7 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { testScope.runTest { val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows) // WHEN no rows are pinned - repository.setNotifications( + headsUpRepository.setNotifications( fakeHeadsUpRowRepository("key 0"), fakeHeadsUpRowRepository("key 1"), fakeHeadsUpRowRepository("key 2"), @@ -166,7 +182,7 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { fakeHeadsUpRowRepository("key 1", isPinned = true), fakeHeadsUpRowRepository("key 2"), ) - repository.setNotifications(rows) + headsUpRepository.setNotifications(rows) runCurrent() // THEN the unpinned rows are filtered @@ -184,7 +200,7 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { fakeHeadsUpRowRepository("key 1", isPinned = true), fakeHeadsUpRowRepository("key 2"), ) - repository.setNotifications(rows) + headsUpRepository.setNotifications(rows) runCurrent() // WHEN all rows gets pinned @@ -206,7 +222,7 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { fakeHeadsUpRowRepository("key 1", isPinned = true), fakeHeadsUpRowRepository("key 2", isPinned = true), ) - repository.setNotifications(rows) + headsUpRepository.setNotifications(rows) runCurrent() // THEN no rows are filtered @@ -224,7 +240,7 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { fakeHeadsUpRowRepository("key 1", isPinned = true), fakeHeadsUpRowRepository("key 2", isPinned = true), ) - repository.setNotifications(rows) + headsUpRepository.setNotifications(rows) runCurrent() // WHEN a row gets unpinned @@ -246,7 +262,7 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { fakeHeadsUpRowRepository("key 1"), fakeHeadsUpRowRepository("key 2"), ) - repository.setNotifications(rows) + headsUpRepository.setNotifications(rows) runCurrent() rows[0].isPinned.value = true @@ -262,6 +278,96 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { assertThat(pinnedHeadsUpRows).containsExactly(rows[0]) } + @Test + fun showHeadsUpStatusBar_true() = + testScope.runTest { + val showHeadsUpStatusBar by collectLastValue(underTest.showHeadsUpStatusBar) + + // WHEN a row is pinned + headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) + + assertThat(showHeadsUpStatusBar).isTrue() + } + + @Test + fun showHeadsUpStatusBar_withoutPinnedNotifications_false() = + testScope.runTest { + val showHeadsUpStatusBar by collectLastValue(underTest.showHeadsUpStatusBar) + + // WHEN no row is pinned + headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = false)) + + assertThat(showHeadsUpStatusBar).isFalse() + } + + @Test + fun showHeadsUpStatusBar_whenShadeExpanded_false() = + testScope.runTest { + val showHeadsUpStatusBar by collectLastValue(underTest.showHeadsUpStatusBar) + + // WHEN a row is pinned + headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) + // AND the shade is expanded + shadeTestUtil.setShadeExpansion(1.0f) + + assertThat(showHeadsUpStatusBar).isFalse() + } + + @Test + fun showHeadsUpStatusBar_notificationsAreHidden_false() = + testScope.runTest { + val showHeadsUpStatusBar by collectLastValue(underTest.showHeadsUpStatusBar) + + // WHEN a row is pinned + headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) + // AND the notifications are hidden + keyguardViewStateRepository.areNotificationsFullyHidden.value = true + + assertThat(showHeadsUpStatusBar).isFalse() + } + + @Test + fun showHeadsUpStatusBar_onLockScreen_false() = + testScope.runTest { + val showHeadsUpStatusBar by collectLastValue(underTest.showHeadsUpStatusBar) + + // WHEN a row is pinned + headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) + // AND the lock screen is shown + keyguardTransitionRepository.emitInitialStepsFromOff(to = KeyguardState.LOCKSCREEN) + + assertThat(showHeadsUpStatusBar).isFalse() + } + + @Test + fun showHeadsUpStatusBar_onByPassLockScreen_true() = + testScope.runTest { + val showHeadsUpStatusBar by collectLastValue(underTest.showHeadsUpStatusBar) + + // WHEN a row is pinned + headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) + // AND the lock screen is shown + keyguardTransitionRepository.emitInitialStepsFromOff(to = KeyguardState.LOCKSCREEN) + // AND bypass is enabled + faceAuthRepository.isBypassEnabled.value = true + + assertThat(showHeadsUpStatusBar).isTrue() + } + + @Test + fun showHeadsUpStatusBar_onByPassLockScreen_withoutNotifications_false() = + testScope.runTest { + val showHeadsUpStatusBar by collectLastValue(underTest.showHeadsUpStatusBar) + + // WHEN no pinned rows + // AND the lock screen is shown + keyguardTransitionRepository.emitInitialStepsFromOff(to = KeyguardState.LOCKSCREEN) + // AND bypass is enabled + faceAuthRepository.isBypassEnabled.value = true + + assertThat(showHeadsUpStatusBar).isFalse() + } + private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) = FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply { this.isPinned.value = isPinned diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt index cc5df74e5e6e..9fde116e968c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt @@ -524,9 +524,9 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas // WHEN there are no pinned rows val rows = arrayListOf( - fakeHeadsUpRowRepository(key = "0"), - fakeHeadsUpRowRepository(key = "1"), - fakeHeadsUpRowRepository(key = "2"), + FakeHeadsUpRowRepository(key = "0"), + FakeHeadsUpRowRepository(key = "1"), + FakeHeadsUpRowRepository(key = "2"), ) headsUpRepository.setNotifications( rows, @@ -565,8 +565,8 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas val hasPinnedHeadsUpRow by collectLastValue(underTest.hasPinnedHeadsUpRow) headsUpRepository.setNotifications( - fakeHeadsUpRowRepository(key = "0", isPinned = true), - fakeHeadsUpRowRepository(key = "1") + FakeHeadsUpRowRepository(key = "0", isPinned = true), + FakeHeadsUpRowRepository(key = "1") ) runCurrent() @@ -580,8 +580,8 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas val hasPinnedHeadsUpRow by collectLastValue(underTest.hasPinnedHeadsUpRow) headsUpRepository.setNotifications( - fakeHeadsUpRowRepository(key = "0"), - fakeHeadsUpRowRepository(key = "1"), + FakeHeadsUpRowRepository(key = "0"), + FakeHeadsUpRowRepository(key = "1"), ) runCurrent() @@ -607,7 +607,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled) shadeTestUtil.setQsExpansion(0.0f) - fakeKeyguardRepository.setKeyguardShowing(false) + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) runCurrent() assertThat(animationsEnabled).isTrue() @@ -620,14 +620,9 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled) shadeTestUtil.setQsExpansion(0.0f) - fakeKeyguardRepository.setKeyguardShowing(true) + fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD) runCurrent() assertThat(animationsEnabled).isFalse() } - - private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) = - FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply { - this.isPinned.value = isPinned - } } diff --git a/packages/SystemUI/res/drawable/shelf_action_chip_container_background.xml b/packages/SystemUI/res/drawable/shelf_action_chip_container_background.xml index bb8cece9203b..ad6c154692ec 100644 --- a/packages/SystemUI/res/drawable/shelf_action_chip_container_background.xml +++ b/packages/SystemUI/res/drawable/shelf_action_chip_container_background.xml @@ -14,10 +14,14 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<shape +<inset xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:shape="rectangle"> - <solid android:color="?androidprv:attr/materialColorSurfaceBright"/> - <corners android:radius="10000dp"/> <!-- fully-rounded radius --> -</shape> + android:insetLeft="@dimen/overlay_action_container_minimum_edge_spacing" + android:insetRight="@dimen/overlay_action_container_minimum_edge_spacing"> + <shape + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="rectangle"> + <solid android:color="?androidprv:attr/materialColorSurfaceBright"/> + <corners android:radius="10000dp"/> <!-- fully-rounded radius --> + </shape> +</inset>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/clipboard_overlay2.xml b/packages/SystemUI/res/layout/clipboard_overlay2.xml index 521369e56652..65005f840598 100644 --- a/packages/SystemUI/res/layout/clipboard_overlay2.xml +++ b/packages/SystemUI/res/layout/clipboard_overlay2.xml @@ -24,6 +24,16 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:contentDescription="@string/clipboard_overlay_window_name"> + <!-- Min edge spacing guideline off of which the preview and actions can be anchored (without + this we'd need to express margins as the sum of two different dimens). --> + <androidx.constraintlayout.widget.Guideline + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/min_edge_guideline" + app:layout_constraintGuide_begin="@dimen/overlay_action_container_minimum_edge_spacing" + android:orientation="vertical"/> + <!-- Negative horizontal margin because this container background must render beyond the thing + it's constrained by (the actions themselves). --> <FrameLayout android:id="@+id/actions_container_background" android:visibility="gone" @@ -31,11 +41,12 @@ android:layout_width="0dp" android:elevation="4dp" android:background="@drawable/shelf_action_chip_container_background" - android:layout_marginStart="@dimen/overlay_action_container_minimum_edge_spacing" + android:layout_marginStart="@dimen/negative_overlay_action_container_minimum_edge_spacing" + android:layout_marginEnd="@dimen/negative_overlay_action_container_minimum_edge_spacing" android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@+id/actions_container" - app:layout_constraintEnd_toEndOf="@+id/actions_container" + app:layout_constraintStart_toStartOf="@id/min_edge_guideline" + app:layout_constraintTop_toTopOf="@id/actions_container" + app:layout_constraintEnd_toEndOf="@id/actions_container" app:layout_constraintBottom_toBottomOf="parent"/> <HorizontalScrollView android:id="@+id/actions_container" @@ -76,7 +87,7 @@ android:layout_marginBottom="@dimen/overlay_preview_container_margin" android:elevation="7dp" android:background="@drawable/overlay_border" - app:layout_constraintStart_toStartOf="@id/actions_container_background" + app:layout_constraintStart_toStartOf="@id/min_edge_guideline" app:layout_constraintTop_toTopOf="@id/clipboard_preview" app:layout_constraintEnd_toEndOf="@id/clipboard_preview" app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/> diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml index 6b65e9c49ca6..796def369708 100644 --- a/packages/SystemUI/res/layout/screenshot_shelf.xml +++ b/packages/SystemUI/res/layout/screenshot_shelf.xml @@ -79,7 +79,6 @@ android:layout_width="wrap_content" android:elevation="4dp" android:background="@drawable/shelf_action_chip_container_background" - android:layout_marginHorizontal="@dimen/overlay_action_container_minimum_edge_spacing" app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toTopOf="@id/guideline" > diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index b9608134b1f8..02b74ce14088 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -449,8 +449,12 @@ <dimen name="overlay_preview_container_margin">8dp</dimen> <dimen name="overlay_action_container_margin_horizontal">8dp</dimen> <dimen name="overlay_action_container_margin_bottom">6dp</dimen> - <!-- minimum distance to the left, right or bottom edges. --> + <!-- + minimum distance to the left, right or bottom edges. Keep in sync with + negative_overlay_action_container_minimum_edge_spacing. --> <dimen name="overlay_action_container_minimum_edge_spacing">12dp</dimen> + <!-- Keep in sync with overlay_action_container_minimum_edge_spacing --> + <dimen name="negative_overlay_action_container_minimum_edge_spacing">-12dp</dimen> <dimen name="overlay_bg_protection_height">242dp</dimen> <dimen name="overlay_action_container_corner_radius">20dp</dimen> <dimen name="overlay_action_container_padding_vertical">8dp</dimen> diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java index 207f7dbb5816..f320057c0763 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java @@ -221,7 +221,8 @@ public class BroadcastDialogDelegate implements SystemUIDialog.Delegate { (view) -> { // TODO: b/321969740 - Take the userHandle as a parameter and pass it through. // The package name is not sufficient to unambiguously identify an app. - mMediaOutputDialogManager.createAndShow(mOutputPackageName, true, null, null); + mMediaOutputDialogManager.createAndShow( + mOutputPackageName, true, null, null, null); dialog.dismiss(); }); cancelBtn.setOnClickListener((view) -> { diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt index a32b2aae817a..6ca8eb9449d0 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt @@ -36,6 +36,9 @@ interface DeviceEntryFaceAuthInteractor { val authenticated: Flow<Boolean> + /** Whether bypass is enabled. If enabled, face unlock dismisses the lock screen. */ + val isBypassEnabled: Flow<Boolean> + /** Can face auth be run right now */ fun canFaceAuthRun(): Boolean diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt index 6629f6e2af31..9486798c4dc7 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt @@ -22,6 +22,7 @@ import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf /** * Implementation of the interactor that noops all face auth operations. @@ -35,6 +36,7 @@ class NoopDeviceEntryFaceAuthInteractor @Inject constructor() : DeviceEntryFaceA override val detectionStatus: Flow<FaceDetectionStatus> = emptyFlow() override val lockedOut: Flow<Boolean> = emptyFlow() override val authenticated: Flow<Boolean> = emptyFlow() + override val isBypassEnabled: Flow<Boolean> = flowOf(false) override fun canFaceAuthRun(): Boolean = false diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt index 669cd94c1f21..87f3f3cf3a24 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt @@ -286,6 +286,7 @@ constructor( override val detectionStatus = repository.detectionStatus override val lockedOut: Flow<Boolean> = repository.isLockedOut override val authenticated: Flow<Boolean> = repository.isAuthenticated + override val isBypassEnabled: Flow<Boolean> = repository.isBypassEnabled private fun runFaceAuth(uiEvent: FaceAuthUiEvent, fallbackToDetect: Boolean) { if (repository.isLockedOut.value) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt index 8d90933b2d5a..fa43ec2cbc06 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt @@ -207,19 +207,24 @@ constructor( .distinctUntilChanged() private val isUnlocked: Flow<Boolean> = - keyguardInteractor.isKeyguardDismissible.flatMapLatest { isUnlocked -> - if (!isUnlocked) { - flowOf(false) + if (SceneContainerFlag.isEnabled) { + deviceEntryInteractor.isUnlocked } else { - flow { - // delay in case device ends up transitioning away from the lock screen; - // we don't want to animate to the unlocked icon and just let the - // icon fade with the transition to GONE - delay(UNLOCKED_DELAY_MS) - emit(true) + keyguardInteractor.isKeyguardDismissible + } + .flatMapLatest { isUnlocked -> + if (!isUnlocked) { + flowOf(false) + } else { + flow { + // delay in case device ends up transitioning away from the lock screen; + // we don't want to animate to the unlocked icon and just let the + // icon fade with the transition to GONE + delay(UNLOCKED_DELAY_MS) + emit(true) + } } } - } val iconType: Flow<DeviceEntryIconView.IconType> = combine( diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt index 043fbfaa8a23..486d4d46c767 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt @@ -102,7 +102,8 @@ constructor( return } val controller = data.token?.let { controllerFactory.create(it) } - val localMediaManager = localMediaManagerFactory.create(data.packageName) + val localMediaManager = + localMediaManagerFactory.create(data.packageName, controller?.sessionToken) val muteAwaitConnectionManager = muteAwaitConnectionManagerFactory.create(localMediaManager) entry = Entry(key, oldKey, controller, localMediaManager, muteAwaitConnectionManager) @@ -224,9 +225,9 @@ constructor( } @WorkerThread - override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) { - val newPlaybackType = info?.playbackType ?: PLAYBACK_TYPE_UNKNOWN - val newPlaybackVolumeControlId = info?.volumeControlId + override fun onAudioInfoChanged(info: MediaController.PlaybackInfo) { + val newPlaybackType = info.playbackType + val newPlaybackVolumeControlId = info.volumeControlId if ( newPlaybackType == playbackType && newPlaybackVolumeControlId == playbackVolumeControlId diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt index 1a0f582fb100..3f75938a91ea 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt @@ -155,11 +155,16 @@ constructor( return false } - fun startMediaOutputDialog(expandable: Expandable, packageName: String) { + fun startMediaOutputDialog( + expandable: Expandable, + packageName: String, + token: MediaSession.Token? = null + ) { mediaOutputDialogManager.createAndShowWithController( packageName, true, - expandable.dialogController() + expandable.dialogController(), + token = token, ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java index 0bc3c43993dd..5ec4f88721ca 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java @@ -743,7 +743,8 @@ public class MediaControlPanel { mPackageName, /* aboveStatusBar */ true, mMediaViewHolder.getSeamlessButton(), - UserHandle.getUserHandleForUid(mUid)); + UserHandle.getUserHandleForUid(mUid), + mToken); } } else { mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId); @@ -775,7 +776,8 @@ public class MediaControlPanel { mPackageName, /* aboveStatusBar */ true, mMediaViewHolder.getSeamlessButton(), - UserHandle.getUserHandleForUid(mUid)); + UserHandle.getUserHandleForUid(mUid), + mToken); } } }); diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt index 1944f072e7dd..099991d7c671 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt @@ -231,12 +231,20 @@ class MediaControlViewModel( ) } else { logger.logOpenOutputSwitcher(model.uid, model.packageName, model.instanceId) - interactor.startMediaOutputDialog(expandable, model.packageName) + interactor.startMediaOutputDialog( + expandable, + model.packageName, + model.token + ) } } else { logger.logOpenOutputSwitcher(model.uid, model.packageName, model.instanceId) device?.intent?.let { interactor.startDeviceIntent(it) } - ?: interactor.startMediaOutputDialog(expandable, model.packageName) + ?: interactor.startMediaOutputDialog( + expandable, + model.packageName, + model.token + ) } } ) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/LocalMediaManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/LocalMediaManagerFactory.kt index ff8e903b6637..0a717adc5162 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/LocalMediaManagerFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/LocalMediaManagerFactory.kt @@ -17,6 +17,7 @@ package com.android.systemui.media.controls.util import android.content.Context +import android.media.session.MediaSession import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.media.InfoMediaManager import com.android.settingslib.media.LocalMediaManager @@ -30,10 +31,16 @@ constructor( private val localBluetoothManager: LocalBluetoothManager? ) { /** Creates a [LocalMediaManager] for the given package. */ - fun create(packageName: String?): LocalMediaManager { + fun create(packageName: String?, token: MediaSession.Token? = null): LocalMediaManager { // TODO: b/321969740 - Populate the userHandle parameter in InfoMediaManager. The user // handle is necessary to disambiguate the same package running on different users. - return InfoMediaManager.createInstance(context, packageName, null, localBluetoothManager) + return InfoMediaManager.createInstance( + context, + packageName, + null, + localBluetoothManager, + token + ) .run { LocalMediaManager(context, localBluetoothManager, this, packageName) } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt index 06267e243456..6ef9ea36882b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt @@ -40,7 +40,12 @@ constructor( // TODO: b/321969740 - Populate the userHandle parameter. The user handle is necessary to // disambiguate the same package running on different users. - val controller = mediaOutputControllerFactory.create(packageName, /* userHandle= */ null) + val controller = + mediaOutputControllerFactory.create( + packageName, + /* userHandle= */ null, + /* token */ null, + ) val dialog = MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller) mediaOutputBroadcastDialog = dialog diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index d6ca32079b09..c2cfdbe410b8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -78,6 +78,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.media.InfoMediaManager; import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; +import com.android.settingslib.media.flags.Flags; import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.animation.ActivityTransitionAnimator; import com.android.systemui.animation.DialogTransitionAnimator; @@ -141,6 +142,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, private final KeyguardManager mKeyGuardManager; private final NearbyMediaDevicesManager mNearbyMediaDevicesManager; private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>(); + private final MediaSession.Token mToken; @VisibleForTesting boolean mIsRefreshing = false; @@ -179,6 +181,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, Context context, @Assisted String packageName, @Assisted @Nullable UserHandle userHandle, + @Assisted @Nullable MediaSession.Token token, MediaSessionManager mediaSessionManager, @Nullable LocalBluetoothManager lbm, ActivityStarter starter, @@ -202,8 +205,9 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mKeyGuardManager = keyGuardManager; mFeatureFlags = featureFlags; mUserTracker = userTracker; + mToken = token; InfoMediaManager imm = - InfoMediaManager.createInstance(mContext, packageName, userHandle, lbm); + InfoMediaManager.createInstance(mContext, packageName, userHandle, lbm, token); mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName); mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName); mDialogTransitionAnimator = dialogTransitionAnimator; @@ -235,7 +239,8 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, @AssistedFactory public interface Factory { /** Construct a MediaOutputController */ - MediaOutputController create(String packageName, UserHandle userHandle); + MediaOutputController create( + String packageName, UserHandle userHandle, MediaSession.Token token); } protected void start(@NonNull Callback cb) { @@ -297,23 +302,28 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } private MediaController getMediaController() { - for (NotificationEntry entry : mNotifCollection.getAllNotifs()) { - final Notification notification = entry.getSbn().getNotification(); - if (notification.isMediaNotification() - && TextUtils.equals(entry.getSbn().getPackageName(), mPackageName)) { - MediaSession.Token token = notification.extras.getParcelable( - Notification.EXTRA_MEDIA_SESSION, - MediaSession.Token.class); - return new MediaController(mContext, token); + if (mToken != null && Flags.usePlaybackInfoForRoutingControls()) { + return new MediaController(mContext, mToken); + } else { + for (NotificationEntry entry : mNotifCollection.getAllNotifs()) { + final Notification notification = entry.getSbn().getNotification(); + if (notification.isMediaNotification() + && TextUtils.equals(entry.getSbn().getPackageName(), mPackageName)) { + MediaSession.Token token = + notification.extras.getParcelable( + Notification.EXTRA_MEDIA_SESSION, MediaSession.Token.class); + return new MediaController(mContext, token); + } } - } - for (MediaController controller : mMediaSessionManager.getActiveSessionsForUser(null, - mUserTracker.getUserHandle())) { - if (TextUtils.equals(controller.getPackageName(), mPackageName)) { - return controller; + for (MediaController controller : + mMediaSessionManager.getActiveSessionsForUser( + null, mUserTracker.getUserHandle())) { + if (TextUtils.equals(controller.getPackageName(), mPackageName)) { + return controller; + } } + return null; } - return null; } @Override @@ -869,10 +879,6 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mMetricLogger.logInteractionUnmute(device); } - String getPackageName() { - return mPackageName; - } - boolean hasAdjustVolumeUserRestriction() { if (RestrictedLockUtilsInternal.checkIfRestrictionEnforced( mContext, UserManager.DISALLOW_ADJUST_VOLUME, UserHandle.myUserId()) != null) { @@ -955,6 +961,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mContext, mPackageName, mUserHandle, + mToken, mMediaSessionManager, mLocalBluetoothManager, mActivityStarter, @@ -1060,7 +1067,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, boolean isBroadcastSupported() { LocalBluetoothLeBroadcast broadcast = mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); - return broadcast != null ? true : false; + return broadcast != null; } boolean isBluetoothLeBroadcastEnabled() { @@ -1194,13 +1201,6 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, assistant.unregisterServiceCallBack(callback); } - private boolean isPlayBackInfoLocal() { - return mMediaController != null - && mMediaController.getPlaybackInfo() != null - && mMediaController.getPlaybackInfo().getPlaybackType() - == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL; - } - boolean isPlaying() { if (mMediaController == null) { return false; diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt index 04d1492ff656..ee8169423de2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt @@ -17,6 +17,7 @@ package com.android.systemui.media.dialog import android.content.Context +import android.media.session.MediaSession import android.os.UserHandle import android.view.View import com.android.internal.jank.InteractionJankMonitor @@ -49,7 +50,8 @@ constructor( packageName: String, aboveStatusBar: Boolean, view: View? = null, - userHandle: UserHandle? = null + userHandle: UserHandle? = null, + token: MediaSession.Token? = null ) { createAndShowWithController( packageName, @@ -65,6 +67,7 @@ constructor( ) }, userHandle = userHandle, + token = token, ) } @@ -77,6 +80,7 @@ constructor( aboveStatusBar: Boolean, controller: DialogTransitionAnimator.Controller?, userHandle: UserHandle? = null, + token: MediaSession.Token? = null, ) { createAndShow( packageName, @@ -84,6 +88,7 @@ constructor( dialogTransitionAnimatorController = controller, includePlaybackAndAppMetadata = true, userHandle = userHandle, + token = token, ) } @@ -108,11 +113,12 @@ constructor( dialogTransitionAnimatorController: DialogTransitionAnimator.Controller?, includePlaybackAndAppMetadata: Boolean = true, userHandle: UserHandle? = null, + token: MediaSession.Token? = null, ) { // Dismiss the previous dialog, if any. mediaOutputDialog?.dismiss() - val controller = mediaOutputControllerFactory.create(packageName, userHandle) + val controller = mediaOutputControllerFactory.create(packageName, userHandle, token) val mediaOutputDialog = MediaOutputDialog( diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java index 9cc288899d45..846460edbe9e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java @@ -56,7 +56,11 @@ public class MediaOutputSwitcherDialogUI implements CoreStartable, CommandQueue. public void showMediaOutputSwitcher(String packageName, UserHandle userHandle) { if (!TextUtils.isEmpty(packageName)) { mMediaOutputDialogManager.createAndShow( - packageName, /* aboveStatusBar= */ false, /* view= */ null, userHandle); + packageName, + /* aboveStatusBar= */ false, + /* view= */ null, + userHandle, + /* token */ null); } else { Log.e(TAG, "Unable to launch media output dialog. Package name is empty."); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt index 9dc19b147e4a..daea977c9d09 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt @@ -181,11 +181,8 @@ interface ShadeViewStateProvider { /** * Returns true if heads up should be visible. - * - * TODO(b/138786270): If HeadsUpAppearanceController was injectable, we could inject it into - * [KeyguardStatusBarViewController] and remove this method. */ - @Deprecated("deprecated in Flexiglass.") fun shouldHeadsUpBeVisible(): Boolean + @Deprecated("deprecated by SceneContainerFlag.isEnabled.") fun shouldHeadsUpBeVisible(): Boolean /** Return the fraction of the shade that's expanded, when in lockscreen. */ @Deprecated("deprecated by SceneContainerFlag.isEnabled") val lockscreenShadeDragProgress: Float diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index 0de3c10329e3..18407cc8e76f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -38,6 +38,9 @@ interface ShadeInteractor : BaseShadeInteractor { /** Whether the Shade is fully expanded. */ val isShadeFullyExpanded: Flow<Boolean> + /** Whether the Shade is fully collapsed. */ + val isShadeFullyCollapsed: Flow<Boolean> + /** * Whether the user is expanding or collapsing either the shade or quick settings with user * input (i.e. dragging a pointer). This will be true even if the user's input gesture had ended diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt index 883ef97a6aad..bb4baa34dd86 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt @@ -38,6 +38,7 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor { override val anyExpansion: StateFlow<Float> = inactiveFlowFloat override val isAnyFullyExpanded: StateFlow<Boolean> = inactiveFlowBoolean override val isShadeFullyExpanded: Flow<Boolean> = inactiveFlowBoolean + override val isShadeFullyCollapsed: Flow<Boolean> = inactiveFlowBoolean override val isAnyExpanded: StateFlow<Boolean> = inactiveFlowBoolean override val isUserInteractingWithShade: Flow<Boolean> = inactiveFlowBoolean override val isUserInteractingWithQs: Flow<Boolean> = inactiveFlowBoolean diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt index 0b45c0834245..06a8d1874e5d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt @@ -74,6 +74,9 @@ constructor( override val isShadeFullyExpanded: Flow<Boolean> = baseShadeInteractor.shadeExpansion.map { it >= 1f }.distinctUntilChanged() + override val isShadeFullyCollapsed: Flow<Boolean> = + baseShadeInteractor.shadeExpansion.map { it <= 0f }.distinctUntilChanged() + override val isUserInteracting: StateFlow<Boolean> = combine(isUserInteractingWithShade, isUserInteractingWithQs) { shade, qs -> shade || qs } .distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt index 98b52edcf9cc..4a6553f724c2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt @@ -18,6 +18,10 @@ package com.android.systemui.statusbar.notification.domain.interactor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey @@ -29,13 +33,21 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -class HeadsUpNotificationInteractor @Inject constructor(private val repository: HeadsUpRepository) { +class HeadsUpNotificationInteractor +@Inject +constructor( + private val headsUpRepository: HeadsUpRepository, + private val faceAuthInteractor: DeviceEntryFaceAuthInteractor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor, + private val shadeInteractor: ShadeInteractor, +) { - val topHeadsUpRow: Flow<HeadsUpRowKey?> = repository.topHeadsUpRow + val topHeadsUpRow: Flow<HeadsUpRowKey?> = headsUpRepository.topHeadsUpRow /** Set of currently pinned top-level heads up rows to be displayed. */ val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> = - repository.activeHeadsUpRows.flatMapLatest { repositories -> + headsUpRepository.activeHeadsUpRows.flatMapLatest { repositories -> if (repositories.isNotEmpty()) { val toCombine: List<Flow<Pair<HeadsUpRowRepository, Boolean>>> = repositories.map { repo -> repo.isPinned.map { isPinned -> repo to isPinned } } @@ -50,7 +62,7 @@ class HeadsUpNotificationInteractor @Inject constructor(private val repository: /** Are there any pinned heads up rows to display? */ val hasPinnedRows: Flow<Boolean> = - repository.activeHeadsUpRows.flatMapLatest { rows -> + headsUpRepository.activeHeadsUpRows.flatMapLatest { rows -> if (rows.isNotEmpty()) { combine(rows.map { it.isPinned }) { pins -> pins.any { it } } } else { @@ -60,15 +72,38 @@ class HeadsUpNotificationInteractor @Inject constructor(private val repository: } val isHeadsUpOrAnimatingAway: Flow<Boolean> = - combine(hasPinnedRows, repository.isHeadsUpAnimatingAway) { hasPinnedRows, animatingAway -> + combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) { + hasPinnedRows, + animatingAway -> hasPinnedRows || animatingAway } + private val canShowHeadsUp: Flow<Boolean> = + combine( + faceAuthInteractor.isBypassEnabled, + shadeInteractor.isShadeFullyCollapsed, + keyguardTransitionInteractor.currentKeyguardState, + notificationsKeyguardInteractor.areNotificationsFullyHidden, + ) { isBypassEnabled, isShadeCollapsed, keyguardState, areNotificationsHidden -> + val isOnLockScreen = keyguardState == KeyguardState.LOCKSCREEN + when { + areNotificationsHidden -> false // don't show when notification are hidden + !isShadeCollapsed -> false // don't show when the shade is expanded + isOnLockScreen -> isBypassEnabled // on the lock screen only show for bypass + else -> true // show otherwise + } + } + + val showHeadsUpStatusBar: Flow<Boolean> = + combine(hasPinnedRows, canShowHeadsUp) { hasPinnedRows, canShowHeadsUp -> + hasPinnedRows && canShowHeadsUp + } + fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowInteractor = HeadsUpRowInteractor(key as HeadsUpRowRepository) fun elementKeyFor(key: HeadsUpRowKey) = (key as HeadsUpRowRepository).elementKey fun setHeadsUpAnimatingAway(animatingAway: Boolean) { - repository.setHeadsUpAnimatingAway(animatingAway) + headsUpRepository.setHeadsUpAnimatingAway(animatingAway) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt index 3a89630ebe77..b54f9c4c6d32 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor @@ -59,7 +58,6 @@ constructor( activeNotificationsInteractor: ActiveNotificationsInteractor, notificationStackInteractor: NotificationStackInteractor, private val headsUpNotificationInteractor: HeadsUpNotificationInteractor, - keyguardInteractor: KeyguardInteractor, remoteInputInteractor: RemoteInputInteractor, seenNotificationsInteractor: SeenNotificationsInteractor, shadeInteractor: ShadeInteractor, @@ -277,11 +275,12 @@ constructor( if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) { flowOf(false) } else { - combine(keyguardInteractor.isKeyguardShowing, shadeInteractor.isShadeFullyExpanded) { - (isKeyguardShowing, isShadeFullyExpanded) -> - // TODO(b/325936094) use isShadeFullyCollapsed instead - !isKeyguardShowing && !isShadeFullyExpanded - } + combine( + notificationStackInteractor.isShowingOnLockscreen, + shadeInteractor.isShadeFullyCollapsed + ) { (isKeyguardShowing, isShadeFullyCollapsed) -> + !isKeyguardShowing && isShadeFullyCollapsed + } .dumpWhileCollecting("headsUpAnimationsEnabled") } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java index 4c3c7d56df50..11feb971db6d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java @@ -354,6 +354,7 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar * since the headsUp manager might not have notified us yet of the state change. * * @return if the heads up status bar view should be shown + * @deprecated use HeadsUpNotificationInteractor.showHeadsUpStatusBar instead. */ public boolean shouldBeVisible() { boolean notificationsShown = !mWakeUpCoordinator.getNotificationsFullyHidden(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 2b26e3f12ef7..f767262dbd4e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -660,10 +660,14 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat * whether heads up is visible. */ public void updateForHeadsUp() { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { + // [KeyguardStatusBarViewBinder] handles visibility changes due to heads up states. + return; + } updateForHeadsUp(true); } - // TODO(b/328579846) bind the StatusBar visibility to heads up events + @VisibleForTesting void updateForHeadsUp(boolean animate) { boolean showingKeyguardHeadsUp = isKeyguardShowing() && mShadeViewStateProvider.shouldHeadsUpBeVisible(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index a858fb079d72..e5c86c8bc7e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -548,6 +548,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private StatusBarVisibilityModel calculateInternalModel( StatusBarVisibilityModel externalModel) { + // TODO(b/328393714) use HeadsUpNotificationInteractor.showHeadsUpStatusBar instead. boolean headsUpVisible = mStatusBarFragmentComponent.getHeadsUpAppearanceController().shouldBeVisible(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt index 5da01e23e268..3e0118007c3c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.statusbar.domain.interactor.KeyguardStatusBarInteractor +import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback import javax.inject.Inject @@ -46,6 +47,7 @@ class KeyguardStatusBarViewModel @Inject constructor( @Application scope: CoroutineScope, + headsUpNotificationInteractor: HeadsUpNotificationInteractor, keyguardInteractor: KeyguardInteractor, keyguardStatusBarInteractor: KeyguardStatusBarInteractor, batteryController: BatteryController, @@ -55,8 +57,9 @@ constructor( combine( keyguardInteractor.isDozing, keyguardInteractor.statusBarState, - ) { isDozing, statusBarState -> - !isDozing && statusBarState == StatusBarState.KEYGUARD + headsUpNotificationInteractor.showHeadsUpStatusBar, + ) { isDozing, statusBarState, showHeadsUpStatusBar -> + !isDozing && statusBarState == StatusBarState.KEYGUARD && !showHeadsUpStatusBar } .stateIn(scope, SharingStarted.WhileSubscribed(), false) diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.java b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.java deleted file mode 100644 index aeed78ad4df4..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.java +++ /dev/null @@ -1,432 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.util.settings; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.ContentResolver; -import android.database.ContentObserver; -import android.net.Uri; -import android.provider.Settings; - -/** - * Used to interact with mainly with Settings.Global, but can also be used for Settings.System - * and Settings.Secure. To use the per-user System and Secure settings, {@link UserSettingsProxy} - * must be used instead. - * <p> - * This interface can be implemented to give instance method (instead of static method) versions - * of Settings.Global. It can be injected into class constructors and then faked or mocked as needed - * in tests. - * <p> - * You can ask for {@link GlobalSettings} to be injected as needed. - * <p> - * This class also provides {@link #registerContentObserver(String, ContentObserver)} methods, - * normally found on {@link ContentResolver} instances, unifying setting related actions in one - * place. - */ -public interface SettingsProxy { - - /** - * Returns the {@link ContentResolver} this instance was constructed with. - */ - ContentResolver getContentResolver(); - - /** - * Construct the content URI for a particular name/value pair, - * useful for monitoring changes with a ContentObserver. - * @param name to look up in the table - * @return the corresponding content URI, or null if not present - */ - Uri getUriFor(String name); - - /** - * Convenience wrapper around - * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.' - * <p> - * Implicitly calls {@link #getUriFor(String)} on the passed in name. - */ - default void registerContentObserver(String name, ContentObserver settingsObserver) { - registerContentObserver(getUriFor(name), settingsObserver); - } - - /** - * Convenience wrapper around - * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.' - */ - default void registerContentObserver(Uri uri, ContentObserver settingsObserver) { - registerContentObserver(uri, false, settingsObserver); - } - - /** - * Convenience wrapper around - * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.' - * <p> - * Implicitly calls {@link #getUriFor(String)} on the passed in name. - */ - default void registerContentObserver(String name, boolean notifyForDescendants, - ContentObserver settingsObserver) { - registerContentObserver(getUriFor(name), notifyForDescendants, settingsObserver); - } - - /** - * Convenience wrapper around - * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.' - */ - default void registerContentObserver(Uri uri, boolean notifyForDescendants, - ContentObserver settingsObserver) { - getContentResolver().registerContentObserver( - uri, notifyForDescendants, settingsObserver); - } - - /** See {@link ContentResolver#unregisterContentObserver(ContentObserver)}. */ - default void unregisterContentObserver(ContentObserver settingsObserver) { - getContentResolver().unregisterContentObserver(settingsObserver); - } - - /** - * Look up a name in the database. - * @param name to look up in the table - * @return the corresponding value, or null if not present - */ - @Nullable - String getString(String name); - - /** - * Store a name/value pair into the database. - * @param name to store - * @param value to associate with the name - * @return true if the value was set, false on database errors - */ - boolean putString(String name, String value); - - /** - * Store a name/value pair into the database. - * <p> - * The method takes an optional tag to associate with the setting - * which can be used to clear only settings made by your package and - * associated with this tag by passing the tag to {@link - * #resetToDefaults(String)}. Anyone can override - * the current tag. Also if another package changes the setting - * then the tag will be set to the one specified in the set call - * which can be null. Also any of the settings setters that do not - * take a tag as an argument effectively clears the tag. - * </p><p> - * For example, if you set settings A and B with tags T1 and T2 and - * another app changes setting A (potentially to the same value), it - * can assign to it a tag T3 (note that now the package that changed - * the setting is not yours). Now if you reset your changes for T1 and - * T2 only setting B will be reset and A not (as it was changed by - * another package) but since A did not change you are in the desired - * initial state. Now if the other app changes the value of A (assuming - * you registered an observer in the beginning) you would detect that - * the setting was changed by another app and handle this appropriately - * (ignore, set back to some value, etc). - * </p><p> - * Also the method takes an argument whether to make the value the - * default for this setting. If the system already specified a default - * value, then the one passed in here will <strong>not</strong> - * be set as the default. - * </p> - * - * @param name to store. - * @param value to associate with the name. - * @param tag to associate with the setting. - * @param makeDefault whether to make the value the default one. - * @return true if the value was set, false on database errors. - * - * @see #resetToDefaults(String) - * - */ - boolean putString(@NonNull String name, @Nullable String value, @Nullable String tag, - boolean makeDefault); - - /** - * Convenience function for retrieving a single secure settings value - * as an integer. Note that internally setting values are always - * stored as strings; this function converts the string to an integer - * for you. The default value will be returned if the setting is - * not defined or not an integer. - * - * @param name The name of the setting to retrieve. - * @param def Value to return if the setting is not defined. - * - * @return The setting's current value, or 'def' if it is not defined - * or not a valid integer. - */ - default int getInt(String name, int def) { - String v = getString(name); - try { - return v != null ? Integer.parseInt(v) : def; - } catch (NumberFormatException e) { - return def; - } - } - - /** - * Convenience function for retrieving a single secure settings value - * as an integer. Note that internally setting values are always - * stored as strings; this function converts the string to an integer - * for you. - * <p> - * This version does not take a default value. If the setting has not - * been set, or the string value is not a number, - * it throws {@link Settings.SettingNotFoundException}. - * - * @param name The name of the setting to retrieve. - * - * @throws Settings.SettingNotFoundException Thrown if a setting by the given - * name can't be found or the setting value is not an integer. - * - * @return The setting's current value. - */ - default int getInt(String name) - throws Settings.SettingNotFoundException { - String v = getString(name); - try { - return Integer.parseInt(v); - } catch (NumberFormatException e) { - throw new Settings.SettingNotFoundException(name); - } - } - - /** - * Convenience function for updating a single settings value as an - * integer. This will either create a new entry in the table if the - * given name does not exist, or modify the value of the existing row - * with that name. Note that internally setting values are always - * stored as strings, so this function converts the given value to a - * string before storing it. - * - * @param name The name of the setting to modify. - * @param value The new value for the setting. - * @return true if the value was set, false on database errors - */ - default boolean putInt(String name, int value) { - return putString(name, Integer.toString(value)); - } - - /** - * Convenience function for retrieving a single secure settings value - * as a boolean. Note that internally setting values are always - * stored as strings; this function converts the string to a boolean - * for you. The default value will be returned if the setting is - * not defined or not a boolean. - * - * @param name The name of the setting to retrieve. - * @param def Value to return if the setting is not defined. - * - * @return The setting's current value, or 'def' if it is not defined - * or not a valid boolean. - */ - default boolean getBool(String name, boolean def) { - return getInt(name, def ? 1 : 0) != 0; - } - - /** - * Convenience function for retrieving a single secure settings value - * as a boolean. Note that internally setting values are always - * stored as strings; this function converts the string to a boolean - * for you. - * <p> - * This version does not take a default value. If the setting has not - * been set, or the string value is not a number, - * it throws {@link Settings.SettingNotFoundException}. - * - * @param name The name of the setting to retrieve. - * - * @throws Settings.SettingNotFoundException Thrown if a setting by the given - * name can't be found or the setting value is not a boolean. - * - * @return The setting's current value. - */ - default boolean getBool(String name) - throws Settings.SettingNotFoundException { - return getInt(name) != 0; - } - - /** - * Convenience function for updating a single settings value as a - * boolean. This will either create a new entry in the table if the - * given name does not exist, or modify the value of the existing row - * with that name. Note that internally setting values are always - * stored as strings, so this function converts the given value to a - * string before storing it. - * - * @param name The name of the setting to modify. - * @param value The new value for the setting. - * @return true if the value was set, false on database errors - */ - default boolean putBool(String name, boolean value) { - return putInt(name, value ? 1 : 0); - } - - /** - * Convenience function for retrieving a single secure settings value - * as a {@code long}. Note that internally setting values are always - * stored as strings; this function converts the string to a {@code long} - * for you. The default value will be returned if the setting is - * not defined or not a {@code long}. - * - * @param name The name of the setting to retrieve. - * @param def Value to return if the setting is not defined. - * - * @return The setting's current value, or 'def' if it is not defined - * or not a valid {@code long}. - */ - default long getLong(String name, long def) { - String valString = getString(name); - return parseLongOrUseDefault(valString, def); - } - - /** Convert a string to a long, or uses a default if the string is malformed or null */ - static long parseLongOrUseDefault(String valString, long def) { - long value; - try { - value = valString != null ? Long.parseLong(valString) : def; - } catch (NumberFormatException e) { - value = def; - } - return value; - } - - /** - * Convenience function for retrieving a single secure settings value - * as a {@code long}. Note that internally setting values are always - * stored as strings; this function converts the string to a {@code long} - * for you. - * <p> - * This version does not take a default value. If the setting has not - * been set, or the string value is not a number, - * it throws {@link Settings.SettingNotFoundException}. - * - * @param name The name of the setting to retrieve. - * - * @return The setting's current value. - * @throws Settings.SettingNotFoundException Thrown if a setting by the given - * name can't be found or the setting value is not an integer. - */ - default long getLong(String name) - throws Settings.SettingNotFoundException { - String valString = getString(name); - return parseLongOrThrow(name, valString); - } - - /** Convert a string to a long, or throws an exception if the string is malformed or null */ - static long parseLongOrThrow(String name, String valString) - throws Settings.SettingNotFoundException { - try { - return Long.parseLong(valString); - } catch (NumberFormatException e) { - throw new Settings.SettingNotFoundException(name); - } - } - - /** - * Convenience function for updating a secure settings value as a long - * integer. This will either create a new entry in the table if the - * given name does not exist, or modify the value of the existing row - * with that name. Note that internally setting values are always - * stored as strings, so this function converts the given value to a - * string before storing it. - * - * @param name The name of the setting to modify. - * @param value The new value for the setting. - * @return true if the value was set, false on database errors - */ - default boolean putLong(String name, long value) { - return putString(name, Long.toString(value)); - } - - /** - * Convenience function for retrieving a single secure settings value - * as a floating point number. Note that internally setting values are - * always stored as strings; this function converts the string to an - * float for you. The default value will be returned if the setting - * is not defined or not a valid float. - * - * @param name The name of the setting to retrieve. - * @param def Value to return if the setting is not defined. - * - * @return The setting's current value, or 'def' if it is not defined - * or not a valid float. - */ - default float getFloat(String name, float def) { - String v = getString(name); - return parseFloat(v, def); - } - - /** Convert a string to a float, or uses a default if the string is malformed or null */ - static float parseFloat(String v, float def) { - try { - return v != null ? Float.parseFloat(v) : def; - } catch (NumberFormatException e) { - return def; - } - } - - /** - * Convenience function for retrieving a single secure settings value - * as a float. Note that internally setting values are always - * stored as strings; this function converts the string to a float - * for you. - * <p> - * This version does not take a default value. If the setting has not - * been set, or the string value is not a number, - * it throws {@link Settings.SettingNotFoundException}. - * - * @param name The name of the setting to retrieve. - * - * @throws Settings.SettingNotFoundException Thrown if a setting by the given - * name can't be found or the setting value is not a float. - * - * @return The setting's current value. - */ - default float getFloat(String name) - throws Settings.SettingNotFoundException { - String v = getString(name); - return parseFloatOrThrow(name, v); - } - - /** Convert a string to a float, or throws an exception if the string is malformed or null */ - static float parseFloatOrThrow(String name, String v) - throws Settings.SettingNotFoundException { - if (v == null) { - throw new Settings.SettingNotFoundException(name); - } - try { - return Float.parseFloat(v); - } catch (NumberFormatException e) { - throw new Settings.SettingNotFoundException(name); - } - } - - /** - * Convenience function for updating a single settings value as a - * floating point number. This will either create a new entry in the - * table if the given name does not exist, or modify the value of the - * existing row with that name. Note that internally setting values - * are always stored as strings, so this function converts the given - * value to a string before storing it. - * - * @param name The name of the setting to modify. - * @param value The new value for the setting. - * @return true if the value was set, false on database errors - */ - default boolean putFloat(String name, float value) { - return putString(name, Float.toString(value)); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt new file mode 100644 index 000000000000..ec89610ce014 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.util.settings + +import android.content.ContentResolver +import android.database.ContentObserver +import android.net.Uri +import android.provider.Settings.SettingNotFoundException + +/** + * Used to interact with mainly with Settings.Global, but can also be used for Settings.System and + * Settings.Secure. To use the per-user System and Secure settings, [UserSettingsProxy] must be used + * instead. + * + * This interface can be implemented to give instance method (instead of static method) versions of + * Settings.Global. It can be injected into class constructors and then faked or mocked as needed in + * tests. + * + * You can ask for [GlobalSettings] to be injected as needed. + * + * This class also provides [.registerContentObserver] methods, normally found on [ContentResolver] + * instances, unifying setting related actions in one place. + */ +interface SettingsProxy { + /** Returns the [ContentResolver] this instance was constructed with. */ + fun getContentResolver(): ContentResolver + + /** + * Construct the content URI for a particular name/value pair, useful for monitoring changes + * with a ContentObserver. + * + * @param name to look up in the table + * @return the corresponding content URI, or null if not present + */ + fun getUriFor(name: String): Uri + + /** + * Convenience wrapper around [ContentResolver.registerContentObserver].' + * + * Implicitly calls [getUriFor] on the passed in name. + */ + fun registerContentObserver(name: String, settingsObserver: ContentObserver) { + registerContentObserver(getUriFor(name), settingsObserver) + } + + /** Convenience wrapper around [ContentResolver.registerContentObserver].' */ + fun registerContentObserver(uri: Uri, settingsObserver: ContentObserver) = + registerContentObserver(uri, false, settingsObserver) + + /** + * Convenience wrapper around [ContentResolver.registerContentObserver].' + * + * Implicitly calls [getUriFor] on the passed in name. + */ + fun registerContentObserver( + name: String, + notifyForDescendants: Boolean, + settingsObserver: ContentObserver + ) = registerContentObserver(getUriFor(name), notifyForDescendants, settingsObserver) + + /** Convenience wrapper around [ContentResolver.registerContentObserver].' */ + fun registerContentObserver( + uri: Uri, + notifyForDescendants: Boolean, + settingsObserver: ContentObserver + ) = getContentResolver().registerContentObserver(uri, notifyForDescendants, settingsObserver) + + /** See [ContentResolver.unregisterContentObserver]. */ + fun unregisterContentObserver(settingsObserver: ContentObserver) = + getContentResolver().unregisterContentObserver(settingsObserver) + + /** + * Look up a name in the database. + * + * @param name to look up in the table + * @return the corresponding value, or null if not present + */ + fun getString(name: String): String + + /** + * Store a name/value pair into the database. + * + * @param name to store + * @param value to associate with the name + * @return true if the value was set, false on database errors + */ + fun putString(name: String, value: String): Boolean + + /** + * Store a name/value pair into the database. + * + * The method takes an optional tag to associate with the setting which can be used to clear + * only settings made by your package and associated with this tag by passing the tag to + * [ ][.resetToDefaults]. Anyone can override the current tag. Also if another package changes + * the setting then the tag will be set to the one specified in the set call which can be null. + * Also any of the settings setters that do not take a tag as an argument effectively clears the + * tag. + * + * For example, if you set settings A and B with tags T1 and T2 and another app changes setting + * A (potentially to the same value), it can assign to it a tag T3 (note that now the package + * that changed the setting is not yours). Now if you reset your changes for T1 and T2 only + * setting B will be reset and A not (as it was changed by another package) but since A did not + * change you are in the desired initial state. Now if the other app changes the value of A + * (assuming you registered an observer in the beginning) you would detect that the setting was + * changed by another app and handle this appropriately (ignore, set back to some value, etc). + * + * Also the method takes an argument whether to make the value the default for this setting. If + * the system already specified a default value, then the one passed in here will **not** be set + * as the default. + * + * @param name to store. + * @param value to associate with the name. + * @param tag to associate with the setting. + * @param makeDefault whether to make the value the default one. + * @return true if the value was set, false on database errors. + * @see .resetToDefaults + */ + fun putString(name: String, value: String, tag: String, makeDefault: Boolean): Boolean + + /** + * Convenience function for retrieving a single secure settings value as an integer. Note that + * internally setting values are always stored as strings; this function converts the string to + * an integer for you. The default value will be returned if the setting is not defined or not + * an integer. + * + * @param name The name of the setting to retrieve. + * @param def Value to return if the setting is not defined. + * @return The setting's current value, or 'def' if it is not defined or not a valid integer. + */ + fun getInt(name: String, def: Int): Int { + val v = getString(name) + return try { + v.toInt() + } catch (e: NumberFormatException) { + def + } + } + + /** + * Convenience function for retrieving a single secure settings value as an integer. Note that + * internally setting values are always stored as strings; this function converts the string to + * an integer for you. + * + * This version does not take a default value. If the setting has not been set, or the string + * value is not a number, it throws [Settings.SettingNotFoundException]. + * + * @param name The name of the setting to retrieve. + * @return The setting's current value. + * @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be + * found or the setting value is not an integer. + */ + @Throws(SettingNotFoundException::class) + fun getInt(name: String): Int { + val v = getString(name) + return try { + v.toInt() + } catch (e: NumberFormatException) { + throw SettingNotFoundException(name) + } + } + + /** + * Convenience function for updating a single settings value as an integer. This will either + * create a new entry in the table if the given name does not exist, or modify the value of the + * existing row with that name. Note that internally setting values are always stored as + * strings, so this function converts the given value to a string before storing it. + * + * @param name The name of the setting to modify. + * @param value The new value for the setting. + * @return true if the value was set, false on database errors + */ + fun putInt(name: String, value: Int): Boolean { + return putString(name, value.toString()) + } + + /** + * Convenience function for retrieving a single secure settings value as a boolean. Note that + * internally setting values are always stored as strings; this function converts the string to + * a boolean for you. The default value will be returned if the setting is not defined or not a + * boolean. + * + * @param name The name of the setting to retrieve. + * @param def Value to return if the setting is not defined. + * @return The setting's current value, or 'def' if it is not defined or not a valid boolean. + */ + fun getBool(name: String, def: Boolean): Boolean { + return getInt(name, if (def) 1 else 0) != 0 + } + + /** + * Convenience function for retrieving a single secure settings value as a boolean. Note that + * internally setting values are always stored as strings; this function converts the string to + * a boolean for you. + * + * This version does not take a default value. If the setting has not been set, or the string + * value is not a number, it throws [Settings.SettingNotFoundException]. + * + * @param name The name of the setting to retrieve. + * @return The setting's current value. + * @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be + * found or the setting value is not a boolean. + */ + @Throws(SettingNotFoundException::class) + fun getBool(name: String): Boolean { + return getInt(name) != 0 + } + + /** + * Convenience function for updating a single settings value as a boolean. This will either + * create a new entry in the table if the given name does not exist, or modify the value of the + * existing row with that name. Note that internally setting values are always stored as + * strings, so this function converts the given value to a string before storing it. + * + * @param name The name of the setting to modify. + * @param value The new value for the setting. + * @return true if the value was set, false on database errors + */ + fun putBool(name: String, value: Boolean): Boolean { + return putInt(name, if (value) 1 else 0) + } + + /** + * Convenience function for retrieving a single secure settings value as a `long`. Note that + * internally setting values are always stored as strings; this function converts the string to + * a `long` for you. The default value will be returned if the setting is not defined or not a + * `long`. + * + * @param name The name of the setting to retrieve. + * @param def Value to return if the setting is not defined. + * @return The setting's current value, or 'def' if it is not defined or not a valid `long`. + */ + fun getLong(name: String, def: Long): Long { + val valString = getString(name) + return parseLongOrUseDefault(valString, def) + } + + /** + * Convenience function for retrieving a single secure settings value as a `long`. Note that + * internally setting values are always stored as strings; this function converts the string to + * a `long` for you. + * + * This version does not take a default value. If the setting has not been set, or the string + * value is not a number, it throws [Settings.SettingNotFoundException]. + * + * @param name The name of the setting to retrieve. + * @return The setting's current value. + * @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be + * found or the setting value is not an integer. + */ + @Throws(SettingNotFoundException::class) + fun getLong(name: String): Long { + val valString = getString(name) + return parseLongOrThrow(name, valString) + } + + /** + * Convenience function for updating a secure settings value as a long integer. This will either + * create a new entry in the table if the given name does not exist, or modify the value of the + * existing row with that name. Note that internally setting values are always stored as + * strings, so this function converts the given value to a string before storing it. + * + * @param name The name of the setting to modify. + * @param value The new value for the setting. + * @return true if the value was set, false on database errors + */ + fun putLong(name: String, value: Long): Boolean { + return putString(name, value.toString()) + } + + /** + * Convenience function for retrieving a single secure settings value as a floating point + * number. Note that internally setting values are always stored as strings; this function + * converts the string to an float for you. The default value will be returned if the setting is + * not defined or not a valid float. + * + * @param name The name of the setting to retrieve. + * @param def Value to return if the setting is not defined. + * @return The setting's current value, or 'def' if it is not defined or not a valid float. + */ + fun getFloat(name: String, def: Float): Float { + val v = getString(name) + return parseFloat(v, def) + } + + /** + * Convenience function for retrieving a single secure settings value as a float. Note that + * internally setting values are always stored as strings; this function converts the string to + * a float for you. + * + * This version does not take a default value. If the setting has not been set, or the string + * value is not a number, it throws [Settings.SettingNotFoundException]. + * + * @param name The name of the setting to retrieve. + * @return The setting's current value. + * @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be + * found or the setting value is not a float. + */ + @Throws(SettingNotFoundException::class) + fun getFloat(name: String): Float { + val v = getString(name) + return parseFloatOrThrow(name, v) + } + + /** + * Convenience function for updating a single settings value as a floating point number. This + * will either create a new entry in the table if the given name does not exist, or modify the + * value of the existing row with that name. Note that internally setting values are always + * stored as strings, so this function converts the given value to a string before storing it. + * + * @param name The name of the setting to modify. + * @param value The new value for the setting. + * @return true if the value was set, false on database errors + */ + fun putFloat(name: String, value: Float): Boolean { + return putString(name, value.toString()) + } + + companion object { + /** Convert a string to a long, or uses a default if the string is malformed or null */ + @JvmStatic + fun parseLongOrUseDefault(valString: String, def: Long): Long { + val value: Long + value = + try { + valString.toLong() + } catch (e: NumberFormatException) { + def + } + return value + } + + /** Convert a string to a long, or throws an exception if the string is malformed or null */ + @JvmStatic + @Throws(SettingNotFoundException::class) + fun parseLongOrThrow(name: String, valString: String?): Long { + if (valString == null) { + throw SettingNotFoundException(name) + } + return try { + valString.toLong() + } catch (e: NumberFormatException) { + throw SettingNotFoundException(name) + } + } + + /** Convert a string to a float, or uses a default if the string is malformed or null */ + @JvmStatic + fun parseFloat(v: String?, def: Float): Float { + return try { + v?.toFloat() ?: def + } catch (e: NumberFormatException) { + def + } + } + + /** + * Convert a string to a float, or throws an exception if the string is malformed or null + */ + @JvmStatic + @Throws(SettingNotFoundException::class) + fun parseFloatOrThrow(name: String, v: String?): Float { + if (v == null) { + throw SettingNotFoundException(name) + } + return try { + v.toFloat() + } catch (e: NumberFormatException) { + throw SettingNotFoundException(name) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java deleted file mode 100644 index 10cf08221fb3..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright (C) 2023 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.util.settings; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.UserIdInt; -import android.content.ContentResolver; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.UserHandle; -import android.provider.Settings; - -import com.android.app.tracing.TraceUtils; -import com.android.systemui.settings.UserTracker; - -import kotlin.Unit; - -/** - * Used to interact with per-user Settings.Secure and Settings.System settings (but not - * Settings.Global, since those do not vary per-user) - * <p> - * This interface can be implemented to give instance method (instead of static method) versions - * of Settings.Secure and Settings.System. It can be injected into class constructors and then - * faked or mocked as needed in tests. - * <p> - * You can ask for {@link SecureSettings} or {@link SystemSettings} to be injected as needed. - * <p> - * This class also provides {@link #registerContentObserver(String, ContentObserver)} methods, - * normally found on {@link ContentResolver} instances, unifying setting related actions in one - * place. - */ -public interface UserSettingsProxy extends SettingsProxy { - - /** - * Returns that {@link UserTracker} this instance was constructed with. - */ - UserTracker getUserTracker(); - - /** - * Returns the user id for the associated {@link ContentResolver}. - */ - default int getUserId() { - return getContentResolver().getUserId(); - } - - /** - * Returns the actual current user handle when querying with the current user. Otherwise, - * returns the passed in user id. - */ - default int getRealUserHandle(int userHandle) { - if (userHandle != UserHandle.USER_CURRENT) { - return userHandle; - } - return getUserTracker().getUserId(); - } - - @Override - default void registerContentObserver(Uri uri, ContentObserver settingsObserver) { - registerContentObserverForUser(uri, settingsObserver, getUserId()); - } - - /** - * Convenience wrapper around - * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.' - */ - @Override - default void registerContentObserver(Uri uri, boolean notifyForDescendants, - ContentObserver settingsObserver) { - registerContentObserverForUser(uri, notifyForDescendants, settingsObserver, getUserId()); - } - - /** - * Convenience wrapper around - * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)} - * - * Implicitly calls {@link #getUriFor(String)} on the passed in name. - */ - default void registerContentObserverForUser( - String name, ContentObserver settingsObserver, int userHandle) { - registerContentObserverForUser( - getUriFor(name), settingsObserver, userHandle); - } - - /** - * Convenience wrapper around - * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)} - */ - default void registerContentObserverForUser( - Uri uri, ContentObserver settingsObserver, int userHandle) { - registerContentObserverForUser( - uri, false, settingsObserver, userHandle); - } - - /** - * Convenience wrapper around - * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)} - * - * Implicitly calls {@link #getUriFor(String)} on the passed in name. - */ - default void registerContentObserverForUser( - String name, boolean notifyForDescendants, ContentObserver settingsObserver, - int userHandle) { - registerContentObserverForUser( - getUriFor(name), notifyForDescendants, settingsObserver, userHandle); - } - - /** - * Convenience wrapper around - * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)} - */ - default void registerContentObserverForUser( - Uri uri, boolean notifyForDescendants, ContentObserver settingsObserver, - int userHandle) { - TraceUtils.trace( - () -> { - // The limit for trace tags length is 127 chars, which leaves us 90 for Uri. - return "USP#registerObserver#[" + uri.toString() + "]"; - }, () -> { - getContentResolver().registerContentObserver( - uri, notifyForDescendants, settingsObserver, - getRealUserHandle(userHandle)); - return Unit.INSTANCE; - }); - } - - /** - * Look up a name in the database. - * @param name to look up in the table - * @return the corresponding value, or null if not present - */ - @Override - default String getString(String name) { - return getStringForUser(name, getUserId()); - } - - /**See {@link #getString(String)}. */ - String getStringForUser(String name, int userHandle); - - /** - * Store a name/value pair into the database. Values written by this method will be - * overridden if a restore happens in the future. - * - * @param name to store - * @param value to associate with the name - * @return true if the value was set, false on database errors - */ - boolean putString(String name, String value, boolean overrideableByRestore); - - @Override - default boolean putString(String name, String value) { - return putStringForUser(name, value, getUserId()); - } - - /** See {@link #putString(String, String)}. */ - boolean putStringForUser(String name, String value, int userHandle); - - /** See {@link #putString(String, String)}. */ - boolean putStringForUser(@NonNull String name, @Nullable String value, @Nullable String tag, - boolean makeDefault, @UserIdInt int userHandle, boolean overrideableByRestore); - - @Override - default int getInt(String name, int def) { - return getIntForUser(name, def, getUserId()); - } - - /** See {@link #getInt(String, int)}. */ - default int getIntForUser(String name, int def, int userHandle) { - String v = getStringForUser(name, userHandle); - try { - return v != null ? Integer.parseInt(v) : def; - } catch (NumberFormatException e) { - return def; - } - } - - @Override - default int getInt(String name) throws Settings.SettingNotFoundException { - return getIntForUser(name, getUserId()); - } - - /** See {@link #getInt(String)}. */ - default int getIntForUser(String name, int userHandle) - throws Settings.SettingNotFoundException { - String v = getStringForUser(name, userHandle); - try { - return Integer.parseInt(v); - } catch (NumberFormatException e) { - throw new Settings.SettingNotFoundException(name); - } - } - - @Override - default boolean putInt(String name, int value) { - return putIntForUser(name, value, getUserId()); - } - - /** See {@link #putInt(String, int)}. */ - default boolean putIntForUser(String name, int value, int userHandle) { - return putStringForUser(name, Integer.toString(value), userHandle); - } - - @Override - default boolean getBool(String name, boolean def) { - return getBoolForUser(name, def, getUserId()); - } - - /** See {@link #getBool(String, boolean)}. */ - default boolean getBoolForUser(String name, boolean def, int userHandle) { - return getIntForUser(name, def ? 1 : 0, userHandle) != 0; - } - - @Override - default boolean getBool(String name) throws Settings.SettingNotFoundException { - return getBoolForUser(name, getUserId()); - } - - /** See {@link #getBool(String)}. */ - default boolean getBoolForUser(String name, int userHandle) - throws Settings.SettingNotFoundException { - return getIntForUser(name, userHandle) != 0; - } - - @Override - default boolean putBool(String name, boolean value) { - return putBoolForUser(name, value, getUserId()); - } - - /** See {@link #putBool(String, boolean)}. */ - default boolean putBoolForUser(String name, boolean value, int userHandle) { - return putIntForUser(name, value ? 1 : 0, userHandle); - } - - /** See {@link #getLong(String, long)}. */ - default long getLongForUser(String name, long def, int userHandle) { - String valString = getStringForUser(name, userHandle); - return SettingsProxy.parseLongOrUseDefault(valString, def); - } - - /** See {@link #getLong(String)}. */ - default long getLongForUser(String name, int userHandle) - throws Settings.SettingNotFoundException { - String valString = getStringForUser(name, userHandle); - return SettingsProxy.parseLongOrThrow(name, valString); - } - - /** See {@link #putLong(String, long)}. */ - default boolean putLongForUser(String name, long value, int userHandle) { - return putStringForUser(name, Long.toString(value), userHandle); - } - - /** See {@link #getFloat(String)}. */ - default float getFloatForUser(String name, float def, int userHandle) { - String v = getStringForUser(name, userHandle); - return SettingsProxy.parseFloat(v, def); - } - - /** See {@link #getFloat(String, float)}. */ - default float getFloatForUser(String name, int userHandle) - throws Settings.SettingNotFoundException { - String v = getStringForUser(name, userHandle); - return SettingsProxy.parseFloatOrThrow(name, v); - } - - /** See {@link #putFloat(String, float)} */ - default boolean putFloatForUser(String name, float value, int userHandle) { - return putStringForUser(name, Float.toString(value), userHandle); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt new file mode 100644 index 000000000000..2285270b0bc7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2023 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.util.settings + +import android.annotation.UserIdInt +import android.database.ContentObserver +import android.net.Uri +import android.os.UserHandle +import android.provider.Settings.SettingNotFoundException +import com.android.app.tracing.TraceUtils.trace +import com.android.systemui.settings.UserTracker +import com.android.systemui.util.settings.SettingsProxy.Companion.parseFloat +import com.android.systemui.util.settings.SettingsProxy.Companion.parseFloatOrThrow +import com.android.systemui.util.settings.SettingsProxy.Companion.parseLongOrThrow +import com.android.systemui.util.settings.SettingsProxy.Companion.parseLongOrUseDefault + +/** + * Used to interact with per-user Settings.Secure and Settings.System settings (but not + * Settings.Global, since those do not vary per-user) + * + * This interface can be implemented to give instance method (instead of static method) versions of + * Settings.Secure and Settings.System. It can be injected into class constructors and then faked or + * mocked as needed in tests. + * + * You can ask for [SecureSettings] or [SystemSettings] to be injected as needed. + * + * This class also provides [.registerContentObserver] methods, normally found on [ContentResolver] + * instances, unifying setting related actions in one place. + */ +interface UserSettingsProxy : SettingsProxy { + + /** Returns that [UserTracker] this instance was constructed with. */ + val userTracker: UserTracker + + /** Returns the user id for the associated [ContentResolver]. */ + var userId: Int + get() = getContentResolver().userId + set(_) { + throw UnsupportedOperationException( + "userId cannot be set in interface, use setter from an implementation instead." + ) + } + + /** + * Returns the actual current user handle when querying with the current user. Otherwise, + * returns the passed in user id. + */ + fun getRealUserHandle(userHandle: Int): Int { + return if (userHandle != UserHandle.USER_CURRENT) { + userHandle + } else userTracker.userId + } + + override fun registerContentObserver(uri: Uri, settingsObserver: ContentObserver) { + registerContentObserverForUser(uri, settingsObserver, userId) + } + + /** Convenience wrapper around [ContentResolver.registerContentObserver].' */ + override fun registerContentObserver( + uri: Uri, + notifyForDescendants: Boolean, + settingsObserver: ContentObserver + ) { + registerContentObserverForUser(uri, notifyForDescendants, settingsObserver, userId) + } + + /** + * Convenience wrapper around [ContentResolver.registerContentObserver] + * + * Implicitly calls [getUriFor] on the passed in name. + */ + fun registerContentObserverForUser( + name: String, + settingsObserver: ContentObserver, + userHandle: Int + ) { + registerContentObserverForUser(getUriFor(name), settingsObserver, userHandle) + } + + /** Convenience wrapper around [ContentResolver.registerContentObserver] */ + fun registerContentObserverForUser( + uri: Uri, + settingsObserver: ContentObserver, + userHandle: Int + ) { + registerContentObserverForUser(uri, false, settingsObserver, userHandle) + } + + /** + * Convenience wrapper around [ContentResolver.registerContentObserver] + * + * Implicitly calls [getUriFor] on the passed in name. + */ + fun registerContentObserverForUser( + name: String, + notifyForDescendants: Boolean, + settingsObserver: ContentObserver, + userHandle: Int + ) { + registerContentObserverForUser( + getUriFor(name), + notifyForDescendants, + settingsObserver, + userHandle + ) + } + + /** Convenience wrapper around [ContentResolver.registerContentObserver] */ + fun registerContentObserverForUser( + uri: Uri, + notifyForDescendants: Boolean, + settingsObserver: ContentObserver, + userHandle: Int + ) { + trace({ "USP#registerObserver#[$uri]" }) { + getContentResolver() + .registerContentObserver( + uri, + notifyForDescendants, + settingsObserver, + getRealUserHandle(userHandle) + ) + Unit + } + } + + /** + * Look up a name in the database. + * + * @param name to look up in the table + * @return the corresponding value, or null if not present + */ + override fun getString(name: String): String { + return getStringForUser(name, userId) + } + + /** See [getString]. */ + fun getStringForUser(name: String, userHandle: Int): String + + /** + * Store a name/value pair into the database. Values written by this method will be overridden + * if a restore happens in the future. + * + * @param name to store + * @param value to associate with the name + * @return true if the value was set, false on database errors + */ + fun putString(name: String, value: String, overrideableByRestore: Boolean): Boolean + override fun putString(name: String, value: String): Boolean { + return putStringForUser(name, value, userId) + } + + /** Similar implementation to [putString] for the specified [userHandle]. */ + fun putStringForUser(name: String, value: String, userHandle: Int): Boolean + + /** Similar implementation to [putString] for the specified [userHandle]. */ + fun putStringForUser( + name: String, + value: String, + tag: String?, + makeDefault: Boolean, + @UserIdInt userHandle: Int, + overrideableByRestore: Boolean + ): Boolean + + override fun getInt(name: String, def: Int): Int { + return getIntForUser(name, def, userId) + } + + /** Similar implementation to [getInt] for the specified [userHandle]. */ + fun getIntForUser(name: String, def: Int, userHandle: Int): Int { + val v = getStringForUser(name, userHandle) + return try { + v.toInt() + } catch (e: NumberFormatException) { + def + } + } + + @Throws(SettingNotFoundException::class) + override fun getInt(name: String) = getIntForUser(name, userId) + + /** Similar implementation to [getInt] for the specified [userHandle]. */ + @Throws(SettingNotFoundException::class) + fun getIntForUser(name: String, userHandle: Int): Int { + val v = getStringForUser(name, userHandle) + return try { + v.toInt() + } catch (e: NumberFormatException) { + throw SettingNotFoundException(name) + } + } + + override fun putInt(name: String, value: Int) = putIntForUser(name, value, userId) + + /** Similar implementation to [getInt] for the specified [userHandle]. */ + fun putIntForUser(name: String, value: Int, userHandle: Int) = + putStringForUser(name, value.toString(), userHandle) + + override fun getBool(name: String, def: Boolean) = getBoolForUser(name, def, userId) + + /** Similar implementation to [getBool] for the specified [userHandle]. */ + fun getBoolForUser(name: String, def: Boolean, userHandle: Int) = + getIntForUser(name, if (def) 1 else 0, userHandle) != 0 + + @Throws(SettingNotFoundException::class) + override fun getBool(name: String) = getBoolForUser(name, userId) + + /** Similar implementation to [getBool] for the specified [userHandle]. */ + @Throws(SettingNotFoundException::class) + fun getBoolForUser(name: String, userHandle: Int): Boolean { + return getIntForUser(name, userHandle) != 0 + } + + override fun putBool(name: String, value: Boolean): Boolean { + return putBoolForUser(name, value, userId) + } + + /** Similar implementation to [putBool] for the specified [userHandle]. */ + fun putBoolForUser(name: String, value: Boolean, userHandle: Int) = + putIntForUser(name, if (value) 1 else 0, userHandle) + + /** Similar implementation to [getLong] for the specified [userHandle]. */ + fun getLongForUser(name: String, def: Long, userHandle: Int): Long { + val valString = getStringForUser(name, userHandle) + return parseLongOrUseDefault(valString, def) + } + + /** Similar implementation to [getLong] for the specified [userHandle]. */ + @Throws(SettingNotFoundException::class) + fun getLongForUser(name: String, userHandle: Int): Long { + val valString = getStringForUser(name, userHandle) + return parseLongOrThrow(name, valString) + } + + /** Similar implementation to [putLong] for the specified [userHandle]. */ + fun putLongForUser(name: String, value: Long, userHandle: Int) = + putStringForUser(name, value.toString(), userHandle) + + /** Similar implementation to [getFloat] for the specified [userHandle]. */ + fun getFloatForUser(name: String, def: Float, userHandle: Int): Float { + val v = getStringForUser(name, userHandle) + return parseFloat(v, def) + } + + /** Similar implementation to [getFloat] for the specified [userHandle]. */ + @Throws(SettingNotFoundException::class) + fun getFloatForUser(name: String, userHandle: Int): Float { + val v = getStringForUser(name, userHandle) + return parseFloatOrThrow(name, v) + } + + /** Similar implementation to [putFloat] for the specified [userHandle]. */ + fun putFloatForUser(name: String, value: Float, userHandle: Int) = + putStringForUser(name, value.toString(), userHandle) +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaControllerInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaControllerInteractor.kt index 4812765d4afe..a714f8078db7 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaControllerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaControllerInteractor.kt @@ -86,7 +86,7 @@ private class MediaControllerCallbackProducer( send(MediaControllerChangeModel.ExtrasChanged(extras)) } - override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) { + override fun onAudioInfoChanged(info: MediaController.PlaybackInfo) { send(MediaControllerChangeModel.AudioInfoChanged(info)) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt index 599bd73abb69..6e1ebc820b08 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt @@ -57,7 +57,7 @@ constructor( } /** [MediaController.PlaybackInfo] changes for the [MediaDeviceSession]. */ - fun playbackInfo(session: MediaDeviceSession): Flow<MediaController.PlaybackInfo?> { + fun playbackInfo(session: MediaDeviceSession): Flow<MediaController.PlaybackInfo> { return stateChanges(session) { emit(MediaControllerChangeModel.AudioInfoChanged(it.playbackInfo)) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaControllerChangeModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaControllerChangeModel.kt index ef5a44a7a2fd..8b5116a64365 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaControllerChangeModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaControllerChangeModel.kt @@ -40,6 +40,6 @@ sealed interface MediaControllerChangeModel { data class ExtrasChanged(val extras: Bundle?) : MediaControllerChangeModel - data class AudioInfoChanged(val info: MediaController.PlaybackInfo?) : + data class AudioInfoChanged(val info: MediaController.PlaybackInfo) : MediaControllerChangeModel } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt index 0df4fbf86d98..9ba56d27fdf3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt @@ -36,12 +36,12 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 -import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.any private const val USER_ID = 8 diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt index 7856f9bce5cc..a89139b18bed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt @@ -196,7 +196,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { verify(globalSettings) .registerContentObserver( eq(Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE)), - settingsObserverCaptor.capture() + capture(settingsObserverCaptor) ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java index 9bb21f020be8..9616f6106a04 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java @@ -128,6 +128,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { mContext, TEST_PACKAGE, mContext.getUser(), + /* token */ null, mMediaSessionManager, mLocalBluetoothManager, mStarter, diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java index 2e6388ae3914..16b00c0b18b4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java @@ -129,6 +129,7 @@ public class MediaOutputBroadcastDialogTest extends SysuiTestCase { mContext, TEST_PACKAGE, mContext.getUser(), + /* token */ null, mMediaSessionManager, mLocalBluetoothManager, mStarter, diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java index 4eb00385f857..45ae50623612 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java @@ -199,6 +199,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mSpyContext, mPackageName, mContext.getUser(), + /* token */ null, mMediaSessionManager, mLocalBluetoothManager, mStarter, @@ -292,6 +293,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mSpyContext, null, mContext.getUser(), + /* token */ null, mMediaSessionManager, mLocalBluetoothManager, mStarter, @@ -333,6 +335,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mSpyContext, null, mSpyContext.getUser(), + /* token */ null, mMediaSessionManager, mLocalBluetoothManager, mStarter, @@ -588,6 +591,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mSpyContext, "", mSpyContext.getUser(), + /* token */ null, mMediaSessionManager, mLocalBluetoothManager, mStarter, @@ -621,6 +625,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mSpyContext, "", mSpyContext.getUser(), + /* token */ null, mMediaSessionManager, mLocalBluetoothManager, mStarter, @@ -667,6 +672,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mSpyContext, null, mSpyContext.getUser(), + /* token */ null, mMediaSessionManager, mLocalBluetoothManager, mStarter, @@ -693,6 +699,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mSpyContext, null, mSpyContext.getUser(), + /* token */ null, mMediaSessionManager, mLocalBluetoothManager, mStarter, @@ -972,6 +979,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mSpyContext, null, mSpyContext.getUser(), + /* token */ null, mMediaSessionManager, mLocalBluetoothManager, mStarter, @@ -1174,6 +1182,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mSpyContext, null, mSpyContext.getUser(), + /* token */ null, mMediaSessionManager, mLocalBluetoothManager, mStarter, diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java index 5dbfe475fedc..1e8fbeac05bd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java @@ -64,7 +64,7 @@ public class MediaOutputDialogReceiverTest extends SysuiTestCase { mMediaOutputDialogReceiver.onReceive(getContext(), intent); verify(mMockMediaOutputDialogManager, times(1)) - .createAndShow(eq(getContext().getPackageName()), eq(false), any(), any()); + .createAndShow(eq(getContext().getPackageName()), eq(false), any(), any(), any()); verify(mMockMediaOutputBroadcastDialogManager, never()) .createAndShow(any(), anyBoolean(), any()); } @@ -76,7 +76,7 @@ public class MediaOutputDialogReceiverTest extends SysuiTestCase { mMediaOutputDialogReceiver.onReceive(getContext(), intent); verify(mMockMediaOutputDialogManager, never()) - .createAndShow(any(), anyBoolean(), any(), any()); + .createAndShow(any(), anyBoolean(), any(), any(), any()); verify(mMockMediaOutputBroadcastDialogManager, never()) .createAndShow(any(), anyBoolean(), any()); } @@ -87,7 +87,7 @@ public class MediaOutputDialogReceiverTest extends SysuiTestCase { mMediaOutputDialogReceiver.onReceive(getContext(), intent); verify(mMockMediaOutputDialogManager, never()) - .createAndShow(any(), anyBoolean(), any(), any()); + .createAndShow(any(), anyBoolean(), any(), any(), any()); verify(mMockMediaOutputBroadcastDialogManager, never()) .createAndShow(any(), anyBoolean(), any()); } @@ -101,7 +101,7 @@ public class MediaOutputDialogReceiverTest extends SysuiTestCase { mMediaOutputDialogReceiver.onReceive(getContext(), intent); verify(mMockMediaOutputDialogManager, never()) - .createAndShow(any(), anyBoolean(), any(), any()); + .createAndShow(any(), anyBoolean(), any(), any(), any()); verify(mMockMediaOutputBroadcastDialogManager, never()) .createAndShow(any(), anyBoolean(), any()); } @@ -115,7 +115,7 @@ public class MediaOutputDialogReceiverTest extends SysuiTestCase { mMediaOutputDialogReceiver.onReceive(getContext(), intent); verify(mMockMediaOutputDialogManager, never()) - .createAndShow(any(), anyBoolean(), any(), any()); + .createAndShow(any(), anyBoolean(), any(), any(), any()); verify(mMockMediaOutputBroadcastDialogManager, times(1)) .createAndShow(eq(getContext().getPackageName()), eq(true), any()); } @@ -129,7 +129,7 @@ public class MediaOutputDialogReceiverTest extends SysuiTestCase { mMediaOutputDialogReceiver.onReceive(getContext(), intent); verify(mMockMediaOutputDialogManager, never()) - .createAndShow(any(), anyBoolean(), any(), any()); + .createAndShow(any(), anyBoolean(), any(), any(), any()); verify(mMockMediaOutputBroadcastDialogManager, never()) .createAndShow(any(), anyBoolean(), any()); } @@ -142,7 +142,7 @@ public class MediaOutputDialogReceiverTest extends SysuiTestCase { mMediaOutputDialogReceiver.onReceive(getContext(), intent); verify(mMockMediaOutputDialogManager, never()) - .createAndShow(any(), anyBoolean(), any(), any()); + .createAndShow(any(), anyBoolean(), any(), any(), any()); verify(mMockMediaOutputBroadcastDialogManager, never()) .createAndShow(any(), anyBoolean(), any()); } @@ -155,7 +155,7 @@ public class MediaOutputDialogReceiverTest extends SysuiTestCase { mMediaOutputDialogReceiver.onReceive(getContext(), intent); verify(mMockMediaOutputDialogManager, never()) - .createAndShow(any(), anyBoolean(), any(), any()); + .createAndShow(any(), anyBoolean(), any(), any(), any()); verify(mMockMediaOutputBroadcastDialogManager, never()) .createAndShow(any(), anyBoolean(), any()); } @@ -166,7 +166,7 @@ public class MediaOutputDialogReceiverTest extends SysuiTestCase { mMediaOutputDialogReceiver.onReceive(getContext(), intent); verify(mMockMediaOutputDialogManager, never()) - .createAndShow(any(), anyBoolean(), any(), any()); + .createAndShow(any(), anyBoolean(), any(), any(), any()); verify(mMockMediaOutputBroadcastDialogManager, never()) .createAndShow(any(), anyBoolean(), any()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java index cdef9644efa9..92d0a72e300c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java @@ -142,6 +142,7 @@ public class MediaOutputDialogTest extends SysuiTestCase { mContext, TEST_PACKAGE, mContext.getUser(), + /* token */ null, mMediaSessionManager, mLocalBluetoothManager, mStarter, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 4d32cc423ded..043dba13f616 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -24,6 +24,7 @@ import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; import static com.google.common.truth.Truth.assertThat; import static kotlinx.coroutines.flow.FlowKt.emptyFlow; +import static kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow; import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; import static org.mockito.ArgumentMatchers.any; @@ -204,6 +205,7 @@ import com.android.wm.shell.animation.FlingAnimationUtils; import dagger.Lazy; import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.channels.BufferOverflow; import kotlinx.coroutines.test.TestScope; import org.junit.After; @@ -351,7 +353,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock private KeyguardClockPositionAlgorithm mKeyguardClockPositionAlgorithm; @Mock private NaturalScrollingSettingObserver mNaturalScrollingSettingObserver; @Mock private LargeScreenHeaderHelper mLargeScreenHeaderHelper; - protected final int mMaxUdfpsBurnInOffsetY = 5; protected FakeFeatureFlagsClassic mFeatureFlags = new FakeFeatureFlagsClassic(); protected KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor; @@ -366,8 +367,11 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { protected PowerInteractor mPowerInteractor; protected FakeHeadsUpNotificationRepository mFakeHeadsUpNotificationRepository = new FakeHeadsUpNotificationRepository(); - protected HeadsUpNotificationInteractor mHeadsUpNotificationInteractor = - new HeadsUpNotificationInteractor(mFakeHeadsUpNotificationRepository); + protected NotificationsKeyguardViewStateRepository mNotificationsKeyguardViewStateRepository = + new NotificationsKeyguardViewStateRepository(); + protected NotificationsKeyguardInteractor mNotificationsKeyguardInteractor = + new NotificationsKeyguardInteractor(mNotificationsKeyguardViewStateRepository); + protected HeadsUpNotificationInteractor mHeadsUpNotificationInteractor; protected NotificationPanelViewController.TouchHandler mTouchHandler; protected ConfigurationController mConfigurationController; protected SysuiStatusBarStateController mStatusBarStateController; @@ -417,6 +421,9 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mPowerInteractor = keyguardInteractorDeps.getPowerInteractor(); when(mKeyguardTransitionInteractor.isInTransitionToStateWhere(any())).thenReturn( MutableStateFlow(false)); + when(mKeyguardTransitionInteractor.getCurrentKeyguardState()).thenReturn( + MutableSharedFlow(0, 0, BufferOverflow.SUSPEND)); + when(mDeviceEntryFaceAuthInteractor.isBypassEnabled()).thenReturn(MutableStateFlow(false)); DeviceEntryUdfpsInteractor deviceEntryUdfpsInteractor = mock(DeviceEntryUdfpsInteractor.class); when(deviceEntryUdfpsInteractor.isUdfpsSupported()).thenReturn(MutableStateFlow(false)); @@ -670,6 +677,11 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { when(mView.requireViewById(R.id.keyguard_long_press)) .thenReturn(mock(LongPressHandlingView.class)); + mHeadsUpNotificationInteractor = + new HeadsUpNotificationInteractor(mFakeHeadsUpNotificationRepository, + mDeviceEntryFaceAuthInteractor, mKeyguardTransitionInteractor, + mNotificationsKeyguardInteractor, mShadeInteractor); + mNotificationPanelViewController = new NotificationPanelViewController( mView, mMainHandler, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt index 6631d29da719..e1ee3585abe3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt @@ -251,7 +251,7 @@ class NotificationPanelViewControllerWithCoroutinesTest : // WHEN a pinned heads up is present mFakeHeadsUpNotificationRepository.setNotifications( - fakeHeadsUpRowRepository("key", isPinned = true) + FakeHeadsUpRowRepository("key", isPinned = true) ) } advanceUntilIdle() @@ -274,9 +274,4 @@ class NotificationPanelViewControllerWithCoroutinesTest : // THEN the panel should be visible assertThat(mNotificationPanelViewController.isExpanded).isTrue() } - - private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) = - FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply { - this.isPinned.value = isPinned - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java index 71f09a5d3f04..f3d640758cf3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java @@ -181,6 +181,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { mViewModel = new KeyguardStatusBarViewModel( mTestScope.getBackgroundScope(), + mKosmos.getHeadsUpNotificationInteractor(), mKeyguardInteractor, new KeyguardStatusBarInteractor(new FakeKeyguardStatusBarRepository()), mBatteryController); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt index ab10bc4a4acc..d88289d9132c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt @@ -21,12 +21,18 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.andSceneContainer +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository -import com.android.systemui.keyguard.data.repository.keyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.domain.interactor.keyguardStatusBarInteractor +import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository +import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository +import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications +import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.batteryController import com.android.systemui.testKosmos @@ -50,7 +56,11 @@ import platform.test.runner.parameterized.Parameters class KeyguardStatusBarViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope + private val faceAuthRepository by lazy { kosmos.fakeDeviceEntryFaceAuthRepository } + private val headsUpRepository by lazy { kosmos.headsUpNotificationRepository } + private val headsUpNotificationInteractor by lazy { kosmos.headsUpNotificationInteractor } private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } + private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } private val keyguardInteractor by lazy { kosmos.keyguardInteractor } private val keyguardStatusBarInteractor by lazy { kosmos.keyguardStatusBarInteractor } private val batteryController = kosmos.batteryController @@ -74,6 +84,7 @@ class KeyguardStatusBarViewModelTest(flags: FlagsParameterization) : SysuiTestCa underTest = KeyguardStatusBarViewModel( testScope.backgroundScope, + headsUpNotificationInteractor, keyguardInteractor, keyguardStatusBarInteractor, batteryController, @@ -112,7 +123,22 @@ class KeyguardStatusBarViewModelTest(flags: FlagsParameterization) : SysuiTestCa } @Test - fun isVisible_statusBarStateKeyguard_andNotDozing_true() = + fun isVisible_headsUpStatusBarShown_false() = + testScope.runTest { + val latest by collectLastValue(underTest.isVisible) + + // WHEN HUN displayed on the bypass lock screen + headsUpRepository.setNotifications(FakeHeadsUpRowRepository("key 0", isPinned = true)) + keyguardTransitionRepository.emitInitialStepsFromOff(KeyguardState.LOCKSCREEN) + keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD) + faceAuthRepository.isBypassEnabled.value = true + + // THEN KeyguardStatusBar is NOT visible to make space for HeadsUpStatusBar + assertThat(latest).isFalse() + } + + @Test + fun isVisible_statusBarStateKeyguard_andNotDozing_andNotShowingHeadsUpStatusBar_true() = testScope.runTest { val latest by collectLastValue(underTest.isVisible) diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt new file mode 100644 index 000000000000..ab95707046d9 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt @@ -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 com.android.systemui.util.settings + +import android.content.ContentResolver +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.provider.Settings.SettingNotFoundException +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.kotlin.eq + +/** Tests for [SettingsProxy]. */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +@TestableLooper.RunWithLooper +class SettingsProxyTest : SysuiTestCase() { + + private lateinit var mSettings: SettingsProxy + private lateinit var mContentObserver: ContentObserver + + @Before + fun setUp() { + mSettings = FakeSettingsProxy() + mContentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {} + } + + @Test + fun registerContentObserver_inputString_success() { + mSettings.registerContentObserver(TEST_SETTING, mContentObserver) + verify(mSettings.getContentResolver()) + .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver)) + } + + @Test + fun registerContentObserver_inputString_notifyForDescendants_true() { + mSettings.registerContentObserver( + TEST_SETTING, + notifyForDescendants = true, + mContentObserver + ) + verify(mSettings.getContentResolver()) + .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver)) + } + + @Test + fun registerContentObserver_inputUri_success() { + mSettings.registerContentObserver(TEST_SETTING_URI, mContentObserver) + verify(mSettings.getContentResolver()) + .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver)) + } + + @Test + fun registerContentObserver_inputUri_notifyForDescendants_true() { + mSettings.registerContentObserver( + TEST_SETTING_URI, + notifyForDescendants = true, + mContentObserver + ) + verify(mSettings.getContentResolver()) + .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver)) + } + + @Test + fun unregisterContentObserver() { + mSettings.unregisterContentObserver(mContentObserver) + verify(mSettings.getContentResolver()).unregisterContentObserver(eq(mContentObserver)) + } + + @Test + fun getString_keyPresent_returnValidValue() { + mSettings.putString(TEST_SETTING, "test") + assertThat(mSettings.getString(TEST_SETTING)).isEqualTo("test") + } + + @Test + fun getString_keyAbsent_returnEmptyValue() { + assertThat(mSettings.getString(TEST_SETTING)).isEmpty() + } + + @Test + fun getInt_keyPresent_returnValidValue() { + mSettings.putInt(TEST_SETTING, 2) + assertThat(mSettings.getInt(TEST_SETTING)).isEqualTo(2) + } + + @Test + fun getInt_keyPresent_nonIntegerValue_throwException() { + assertThrows(SettingNotFoundException::class.java) { + mSettings.putString(TEST_SETTING, "test") + mSettings.getInt(TEST_SETTING) + } + } + + @Test + fun getInt_keyAbsent_throwException() { + assertThrows(SettingNotFoundException::class.java) { mSettings.getInt(TEST_SETTING) } + } + + @Test + fun getInt_keyAbsent_returnDefaultValue() { + assertThat(mSettings.getInt(TEST_SETTING, 5)).isEqualTo(5) + } + + @Test + fun getBool_keyPresent_returnValidValue() { + mSettings.putBool(TEST_SETTING, true) + assertThat(mSettings.getBool(TEST_SETTING)).isTrue() + } + + @Test + fun getBool_keyPresent_nonBooleanValue_throwException() { + assertThrows(SettingNotFoundException::class.java) { + mSettings.putString(TEST_SETTING, "test") + mSettings.getBool(TEST_SETTING) + } + } + + @Test + fun getBool_keyAbsent_throwException() { + assertThrows(SettingNotFoundException::class.java) { mSettings.getBool(TEST_SETTING) } + } + + @Test + fun getBool_keyAbsent_returnDefaultValue() { + assertThat(mSettings.getBool(TEST_SETTING, false)).isEqualTo(false) + } + + @Test + fun getLong_keyPresent_returnValidValue() { + mSettings.putLong(TEST_SETTING, 1L) + assertThat(mSettings.getLong(TEST_SETTING)).isEqualTo(1L) + } + + @Test + fun getLong_keyPresent_nonLongValue_throwException() { + assertThrows(SettingNotFoundException::class.java) { + mSettings.putString(TEST_SETTING, "test") + mSettings.getLong(TEST_SETTING) + } + } + + @Test + fun getLong_keyAbsent_throwException() { + assertThrows(SettingNotFoundException::class.java) { mSettings.getLong(TEST_SETTING) } + } + + @Test + fun getLong_keyAbsent_returnDefaultValue() { + assertThat(mSettings.getLong(TEST_SETTING, 2L)).isEqualTo(2L) + } + + @Test + fun getFloat_keyPresent_returnValidValue() { + mSettings.putFloat(TEST_SETTING, 2.5F) + assertThat(mSettings.getFloat(TEST_SETTING)).isEqualTo(2.5F) + } + + @Test + fun getFloat_keyPresent_nonFloatValue_throwException() { + assertThrows(SettingNotFoundException::class.java) { + mSettings.putString(TEST_SETTING, "test") + mSettings.getFloat(TEST_SETTING) + } + } + + @Test + fun getFloat_keyAbsent_throwException() { + assertThrows(SettingNotFoundException::class.java) { mSettings.getFloat(TEST_SETTING) } + } + + @Test + fun getFloat_keyAbsent_returnDefaultValue() { + assertThat(mSettings.getFloat(TEST_SETTING, 2.5F)).isEqualTo(2.5F) + } + + private class FakeSettingsProxy : SettingsProxy { + + private val mContentResolver = mock(ContentResolver::class.java) + private val settingToValueMap: MutableMap<String, String> = mutableMapOf() + + override fun getContentResolver() = mContentResolver + + override fun getUriFor(name: String) = + Uri.parse(StringBuilder().append("content://settings/").append(name).toString()) + + override fun getString(name: String): String { + return settingToValueMap[name] ?: "" + } + + override fun putString(name: String, value: String): Boolean { + settingToValueMap[name] = value + return true + } + + override fun putString( + name: String, + value: String, + tag: String, + makeDefault: Boolean + ): Boolean { + settingToValueMap[name] = value + return true + } + } + + companion object { + private const val TEST_SETTING = "test_setting" + private val TEST_SETTING_URI = Uri.parse("content://settings/test_setting") + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt new file mode 100644 index 000000000000..56328b933602 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt @@ -0,0 +1,365 @@ +/* + * 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.util.settings + +import android.content.ContentResolver +import android.content.pm.UserInfo +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.settings.UserTracker +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.kotlin.eq + +/** Tests for [UserSettingsProxy]. */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +@TestableLooper.RunWithLooper +class UserSettingsProxyTest : SysuiTestCase() { + + private var mUserTracker = FakeUserTracker() + private var mSettings: UserSettingsProxy = FakeUserSettingsProxy(mUserTracker) + private var mContentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {} + + @Before + fun setUp() { + mUserTracker.set( + listOf(UserInfo(MAIN_USER_ID, "main", UserInfo.FLAG_MAIN)), + selectedUserIndex = 0 + ) + } + + @Test + fun registerContentObserverForUser_inputString_success() { + mSettings.registerContentObserverForUser( + TEST_SETTING, + mContentObserver, + mUserTracker.userId + ) + verify(mSettings.getContentResolver()) + .registerContentObserver( + eq(TEST_SETTING_URI), + eq(false), + eq(mContentObserver), + eq(MAIN_USER_ID) + ) + } + + @Test + fun registerContentObserverForUser_inputString_notifyForDescendants_true() { + mSettings.registerContentObserverForUser( + TEST_SETTING, + notifyForDescendants = true, + mContentObserver, + mUserTracker.userId + ) + verify(mSettings.getContentResolver()) + .registerContentObserver( + eq(TEST_SETTING_URI), + eq(true), + eq(mContentObserver), + eq(MAIN_USER_ID) + ) + } + + @Test + fun registerContentObserverForUser_inputUri_success() { + mSettings.registerContentObserverForUser( + TEST_SETTING_URI, + mContentObserver, + mUserTracker.userId + ) + verify(mSettings.getContentResolver()) + .registerContentObserver( + eq(TEST_SETTING_URI), + eq(false), + eq(mContentObserver), + eq(MAIN_USER_ID) + ) + } + + @Test + fun registerContentObserverForUser_inputUri_notifyForDescendants_true() { + mSettings.registerContentObserverForUser( + TEST_SETTING_URI, + notifyForDescendants = true, + mContentObserver, + mUserTracker.userId + ) + verify(mSettings.getContentResolver()) + .registerContentObserver( + eq(TEST_SETTING_URI), + eq(true), + eq(mContentObserver), + eq(MAIN_USER_ID) + ) + } + + @Test + fun registerContentObserver_inputUri_success() { + mSettings.registerContentObserver(TEST_SETTING_URI, mContentObserver) + verify(mSettings.getContentResolver()) + .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver), eq(0)) + } + + @Test + fun registerContentObserver_inputUri_notifyForDescendants_true() { + mSettings.registerContentObserver( + TEST_SETTING_URI, + notifyForDescendants = true, + mContentObserver + ) + verify(mSettings.getContentResolver()) + .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver), eq(0)) + } + + @Test + fun getString_keyPresent_returnValidValue() { + mSettings.putString(TEST_SETTING, "test") + assertThat(mSettings.getString(TEST_SETTING)).isEqualTo("test") + } + + @Test + fun getString_keyAbsent_returnEmptyValue() { + assertThat(mSettings.getString(TEST_SETTING)).isEmpty() + } + + @Test + fun getStringForUser_multipleUsers_validResult() { + mSettings.putStringForUser(TEST_SETTING, "test", MAIN_USER_ID) + mSettings.putStringForUser(TEST_SETTING, "test1", SECONDARY_USER_ID) + assertThat(mSettings.getStringForUser(TEST_SETTING, MAIN_USER_ID)).isEqualTo("test") + assertThat(mSettings.getStringForUser(TEST_SETTING, SECONDARY_USER_ID)).isEqualTo("test1") + } + + @Test + fun getInt_keyPresent_returnValidValue() { + mSettings.putInt(TEST_SETTING, 2) + assertThat(mSettings.getInt(TEST_SETTING)).isEqualTo(2) + } + + @Test + fun getInt_keyPresent_nonIntegerValue_throwException() { + assertThrows(Settings.SettingNotFoundException::class.java) { + mSettings.putString(TEST_SETTING, "test") + mSettings.getInt(TEST_SETTING) + } + } + + @Test + fun getInt_keyAbsent_throwException() { + assertThrows(Settings.SettingNotFoundException::class.java) { + mSettings.getInt(TEST_SETTING) + } + } + + @Test + fun getInt_keyAbsent_returnDefaultValue() { + assertThat(mSettings.getInt(TEST_SETTING, 5)).isEqualTo(5) + } + + @Test + fun getIntForUser_multipleUsers__validResult() { + mSettings.putIntForUser(TEST_SETTING, 1, MAIN_USER_ID) + mSettings.putIntForUser(TEST_SETTING, 2, SECONDARY_USER_ID) + assertThat(mSettings.getIntForUser(TEST_SETTING, MAIN_USER_ID)).isEqualTo(1) + assertThat(mSettings.getIntForUser(TEST_SETTING, SECONDARY_USER_ID)).isEqualTo(2) + } + + @Test + fun getBool_keyPresent_returnValidValue() { + mSettings.putBool(TEST_SETTING, true) + assertThat(mSettings.getBool(TEST_SETTING)).isTrue() + } + + @Test + fun getBool_keyPresent_nonBooleanValue_throwException() { + assertThrows(Settings.SettingNotFoundException::class.java) { + mSettings.putString(TEST_SETTING, "test") + mSettings.getBool(TEST_SETTING) + } + } + + @Test + fun getBool_keyAbsent_throwException() { + assertThrows(Settings.SettingNotFoundException::class.java) { + mSettings.getBool(TEST_SETTING) + } + } + + @Test + fun getBool_keyAbsent_returnDefaultValue() { + assertThat(mSettings.getBool(TEST_SETTING, false)).isEqualTo(false) + } + + @Test + fun getBoolForUser_multipleUsers__validResult() { + mSettings.putBoolForUser(TEST_SETTING, true, MAIN_USER_ID) + mSettings.putBoolForUser(TEST_SETTING, false, SECONDARY_USER_ID) + assertThat(mSettings.getBoolForUser(TEST_SETTING, MAIN_USER_ID)).isEqualTo(true) + assertThat(mSettings.getBoolForUser(TEST_SETTING, SECONDARY_USER_ID)).isEqualTo(false) + } + + @Test + fun getLong_keyPresent_returnValidValue() { + mSettings.putLong(TEST_SETTING, 1L) + assertThat(mSettings.getLong(TEST_SETTING)).isEqualTo(1L) + } + + @Test + fun getLong_keyPresent_nonLongValue_throwException() { + assertThrows(Settings.SettingNotFoundException::class.java) { + mSettings.putString(TEST_SETTING, "test") + mSettings.getLong(TEST_SETTING) + } + } + + @Test + fun getLong_keyAbsent_throwException() { + assertThrows(Settings.SettingNotFoundException::class.java) { + mSettings.getLong(TEST_SETTING) + } + } + + @Test + fun getLong_keyAbsent_returnDefaultValue() { + assertThat(mSettings.getLong(TEST_SETTING, 2L)).isEqualTo(2L) + } + + @Test + fun getLongForUser_multipleUsers__validResult() { + mSettings.putLongForUser(TEST_SETTING, 1L, MAIN_USER_ID) + mSettings.putLongForUser(TEST_SETTING, 2L, SECONDARY_USER_ID) + assertThat(mSettings.getLongForUser(TEST_SETTING, MAIN_USER_ID)).isEqualTo(1L) + assertThat(mSettings.getLongForUser(TEST_SETTING, SECONDARY_USER_ID)).isEqualTo(2L) + } + + @Test + fun getFloat_keyPresent_returnValidValue() { + mSettings.putFloat(TEST_SETTING, 2.5F) + assertThat(mSettings.getFloat(TEST_SETTING)).isEqualTo(2.5F) + } + + @Test + fun getFloat_keyPresent_nonFloatValue_throwException() { + assertThrows(Settings.SettingNotFoundException::class.java) { + mSettings.putString(TEST_SETTING, "test") + mSettings.getFloat(TEST_SETTING) + } + } + + @Test + fun getFloat_keyAbsent_throwException() { + assertThrows(Settings.SettingNotFoundException::class.java) { + mSettings.getFloat(TEST_SETTING) + } + } + + @Test + fun getFloat_keyAbsent_returnDefaultValue() { + assertThat(mSettings.getFloat(TEST_SETTING, 2.5F)).isEqualTo(2.5F) + } + + @Test + fun getFloatForUser_multipleUsers__validResult() { + mSettings.putFloatForUser(TEST_SETTING, 1F, MAIN_USER_ID) + mSettings.putFloatForUser(TEST_SETTING, 2F, SECONDARY_USER_ID) + assertThat(mSettings.getFloatForUser(TEST_SETTING, MAIN_USER_ID)).isEqualTo(1F) + assertThat(mSettings.getFloatForUser(TEST_SETTING, SECONDARY_USER_ID)).isEqualTo(2F) + } + + /** + * Fake implementation of [UserSettingsProxy]. + * + * This class uses a mock of [ContentResolver] to test the [ContentObserver] registration APIs. + */ + private class FakeUserSettingsProxy(override val userTracker: UserTracker) : UserSettingsProxy { + + private val mContentResolver = mock(ContentResolver::class.java) + private val userIdToSettingsValueMap: MutableMap<Int, MutableMap<String, String>> = + mutableMapOf() + + override fun getContentResolver() = mContentResolver + + override fun getUriFor(name: String) = + Uri.parse(StringBuilder().append(URI_PREFIX).append(name).toString()) + + override fun getStringForUser(name: String, userHandle: Int) = + userIdToSettingsValueMap[userHandle]?.get(name) ?: "" + + override fun putString( + name: String, + value: String, + overrideableByRestore: Boolean + ): Boolean { + userIdToSettingsValueMap[DEFAULT_USER_ID]?.put(name, value) + return true + } + + override fun putString( + name: String, + value: String, + tag: String, + makeDefault: Boolean + ): Boolean { + putStringForUser(name, value, DEFAULT_USER_ID) + return true + } + + override fun putStringForUser(name: String, value: String, userHandle: Int): Boolean { + userIdToSettingsValueMap[userHandle] = mutableMapOf(Pair(name, value)) + return true + } + + override fun putStringForUser( + name: String, + value: String, + tag: String?, + makeDefault: Boolean, + userHandle: Int, + overrideableByRestore: Boolean + ): Boolean { + userIdToSettingsValueMap[userHandle]?.set(name, value) + return true + } + + private companion object { + const val DEFAULT_USER_ID = 0 + const val URI_PREFIX = "content://settings/" + } + } + + private companion object { + const val MAIN_USER_ID = 10 + const val SECONDARY_USER_ID = 20 + const val TEST_SETTING = "test_setting" + val TEST_SETTING_URI = Uri.parse("content://settings/test_setting") + } +} 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 a81ac038b38a..0e95320bdf4c 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 @@ -55,6 +55,7 @@ import com.android.systemui.settings.brightness.domain.interactor.brightnessMirr import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.shadeController +import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository @@ -78,6 +79,7 @@ class KosmosJavaAdapter( val configurationInteractor by lazy { kosmos.configurationInteractor } val bouncerRepository by lazy { kosmos.bouncerRepository } val communalRepository by lazy { kosmos.fakeCommunalRepository } + val headsUpNotificationInteractor by lazy { kosmos.headsUpNotificationInteractor } val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } val keyguardBouncerRepository by lazy { kosmos.fakeKeyguardBouncerRepository } val keyguardInteractor by lazy { kosmos.keyguardInteractor } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeHeadsUpRowRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeHeadsUpRowRepository.kt index 2e983a820240..980d65fde6de 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeHeadsUpRowRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeHeadsUpRowRepository.kt @@ -18,7 +18,11 @@ package com.android.systemui.statusbar.notification.data.repository import kotlinx.coroutines.flow.MutableStateFlow -class FakeHeadsUpRowRepository(override val key: String, override val elementKey: Any) : +class FakeHeadsUpRowRepository(override val key: String, override val elementKey: Any = Any()) : HeadsUpRowRepository { + constructor(key: String, isPinned: Boolean) : this(key = key) { + this.isPinned.value = isPinned + } + override val isPinned: MutableStateFlow<Boolean> = MutableStateFlow(false) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HeadsUpNotificationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HeadsUpNotificationInteractorKosmos.kt index d3451075c51b..c74aec1fc59c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HeadsUpNotificationInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HeadsUpNotificationInteractorKosmos.kt @@ -16,11 +16,20 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository val Kosmos.headsUpNotificationInteractor by Fixture { - HeadsUpNotificationInteractor(headsUpNotificationRepository) + HeadsUpNotificationInteractor( + headsUpNotificationRepository, + deviceEntryFaceAuthInteractor, + keyguardTransitionInteractor, + notificationsKeyguardInteractor, + shadeInteractor, + ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt index 94f6ecd36c7c..de8b3500a88a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.systemui.dump.dumpManager -import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher @@ -42,7 +41,6 @@ val Kosmos.notificationListViewModel by Fixture { activeNotificationsInteractor, notificationStackInteractor, headsUpNotificationInteractor, - keyguardInteractor, remoteInputInteractor, seenNotificationsInteractor, shadeInteractor, diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 58855ea0384a..4c8f41608f46 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -5590,32 +5590,30 @@ public class ActivityManagerService extends IActivityManager.Stub // security checking for it above. userId = UserHandle.USER_CURRENT; } - try { - if (owningUid != 0 && owningUid != SYSTEM_UID) { - final int uid = AppGlobals.getPackageManager().getPackageUid(packageName, - MATCH_DEBUG_TRIAGED_MISSING, UserHandle.getUserId(owningUid)); - if (!UserHandle.isSameApp(owningUid, uid)) { - String msg = "Permission Denial: getIntentSender() from pid=" - + Binder.getCallingPid() - + ", uid=" + owningUid - + ", (need uid=" + uid + ")" - + " is not allowed to send as package " + packageName; - Slog.w(TAG, msg); - throw new SecurityException(msg); - } - } - if (type == ActivityManager.INTENT_SENDER_ACTIVITY_RESULT) { - return mAtmInternal.getIntentSender(type, packageName, featureId, owningUid, - userId, token, resultWho, requestCode, intents, resolvedTypes, flags, - bOptions); + if (owningUid != 0 && owningUid != SYSTEM_UID) { + if (!getPackageManagerInternal().isSameApp( + packageName, + MATCH_DEBUG_TRIAGED_MISSING, + owningUid, + UserHandle.getUserId(owningUid))) { + String msg = "Permission Denial: getIntentSender() from pid=" + + Binder.getCallingPid() + + ", uid=" + owningUid + + " is not allowed to send as package " + packageName; + Slog.w(TAG, msg); + throw new SecurityException(msg); } - return mPendingIntentController.getIntentSender(type, packageName, featureId, - owningUid, userId, token, resultWho, requestCode, intents, resolvedTypes, - flags, bOptions); - } catch (RemoteException e) { - throw new SecurityException(e); } + + if (type == ActivityManager.INTENT_SENDER_ACTIVITY_RESULT) { + return mAtmInternal.getIntentSender(type, packageName, featureId, owningUid, + userId, token, resultWho, requestCode, intents, resolvedTypes, flags, + bOptions); + } + return mPendingIntentController.getIntentSender(type, packageName, featureId, + owningUid, userId, token, resultWho, requestCode, intents, resolvedTypes, + flags, bOptions); } @Override diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java index 9600317faca1..a67af89ad39d 100644 --- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java +++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java @@ -84,6 +84,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.ToIntFunction; /** * A modern implementation of the oom adjuster. @@ -271,11 +272,31 @@ public class OomAdjusterModernImpl extends OomAdjuster { // The last node besides the tail. private final ProcessRecordNode[] mLastNode; + private final ToIntFunction<ProcessRecord> mSlotFunction; + // Cache of the most important slot with a node in it. + private int mFirstPopulatedSlot = 0; + ProcessRecordNodes(@ProcessRecordNode.NodeType int type, int size) { mType = type; + final ToIntFunction<ProcessRecord> valueFunction; + switch (mType) { + case ProcessRecordNode.NODE_TYPE_PROC_STATE: + valueFunction = (proc) -> proc.mState.getCurProcState(); + mSlotFunction = (proc) -> processStateToSlot(proc.mState.getCurProcState()); + break; + case ProcessRecordNode.NODE_TYPE_ADJ: + valueFunction = (proc) -> proc.mState.getCurRawAdj(); + mSlotFunction = (proc) -> adjToSlot(proc.mState.getCurRawAdj()); + break; + default: + valueFunction = (proc) -> 0; + mSlotFunction = (proc) -> 0; + break; + } + mProcessRecordNodes = new LinkedProcessRecordList[size]; for (int i = 0; i < size; i++) { - mProcessRecordNodes[i] = new LinkedProcessRecordList(type); + mProcessRecordNodes[i] = new LinkedProcessRecordList(valueFunction); } mLastNode = new ProcessRecordNode[size]; resetLastNodes(); @@ -294,6 +315,11 @@ public class OomAdjusterModernImpl extends OomAdjuster { } void resetLastNodes() { + if (Flags.simplifyProcessTraversal()) { + // Last nodes are no longer used. Just reset instead. + reset(); + return; + } for (int i = 0; i < mProcessRecordNodes.length; i++) { mLastNode[i] = mProcessRecordNodes[i].getLastNodeBeforeTail(); } @@ -308,6 +334,36 @@ public class OomAdjusterModernImpl extends OomAdjuster { final ProcessRecordNode tail = mProcessRecordNodes[slot].TAIL; while (node != tail) { mTmpOomAdjusterArgs.mApp = node.mApp; + if (node.mApp == null) { + // TODO(b/336178916) - Temporary logging for root causing b/336178916. + StringBuilder sb = new StringBuilder(); + sb.append("Iterating null process during OomAdjuster traversal!!!\n"); + sb.append("Type:"); + switch (mType) { + case ProcessRecordNode.NODE_TYPE_PROC_STATE -> sb.append( + "NODE_TYPE_PROC_STATE"); + case ProcessRecordNode.NODE_TYPE_ADJ -> sb.append("NODE_TYPE_ADJ"); + default -> sb.append("UNKNOWN"); + } + sb.append(", Slot:"); + sb.append(slot); + sb.append("\nLAST:"); + ProcessRecordNode last = mLastNode[slot]; + if (last.mApp == null) { + sb.append("null"); + } else { + sb.append(last); + sb.append("\nSetProcState:"); + sb.append(last.mApp.getSetProcState()); + sb.append(", CurProcState:"); + sb.append(last.mApp.mState.getCurProcState()); + sb.append(", SetAdj:"); + sb.append(last.mApp.getSetAdj()); + sb.append(", CurRawAdj:"); + sb.append(last.mApp.mState.getCurRawAdj()); + } + Slog.wtfStack(TAG, sb.toString()); + } // Save the next before calling callback, since that may change the node.mNext. final ProcessRecordNode next = node.mNext; callback.accept(mTmpOomAdjusterArgs); @@ -325,6 +381,33 @@ public class OomAdjusterModernImpl extends OomAdjuster { } } + ProcessRecord poll() { + ProcessRecordNode node = null; + final int size = mProcessRecordNodes.length; + // Find the next node. + while (node == null && mFirstPopulatedSlot < size) { + node = mProcessRecordNodes[mFirstPopulatedSlot].poll(); + if (node == null) { + // This slot is now empty, move on to the next. + mFirstPopulatedSlot++; + } + } + if (node == null) return null; + return node.mApp; + } + + void offer(ProcessRecord proc) { + ProcessRecordNode node = proc.mLinkedNodes[mType]; + // Find which slot to add the node to. + final int newSlot = mSlotFunction.applyAsInt(proc); + if (newSlot < mFirstPopulatedSlot) { + // node is being added to a more important slot. + mFirstPopulatedSlot = newSlot; + } + node.unlink(); + mProcessRecordNodes[newSlot].offer(node); + } + int getNumberOfSlots() { return mProcessRecordNodes.length; } @@ -423,12 +506,35 @@ public class OomAdjusterModernImpl extends OomAdjuster { // Sentinel head/tail, to make bookkeeping work easier. final ProcessRecordNode HEAD = new ProcessRecordNode(null); final ProcessRecordNode TAIL = new ProcessRecordNode(null); - final @ProcessRecordNode.NodeType int mNodeType; + final ToIntFunction<ProcessRecord> mValueFunction; - LinkedProcessRecordList(@ProcessRecordNode.NodeType int nodeType) { + LinkedProcessRecordList(ToIntFunction<ProcessRecord> valueFunction) { HEAD.mNext = TAIL; TAIL.mPrev = HEAD; - mNodeType = nodeType; + mValueFunction = valueFunction; + } + + ProcessRecordNode poll() { + final ProcessRecordNode next = HEAD.mNext; + if (next == TAIL) return null; + next.unlink(); + return next; + } + + void offer(@NonNull ProcessRecordNode node) { + final int newValue = mValueFunction.applyAsInt(node.mApp); + + // Find the last node with less than or equal value to the new node. + ProcessRecordNode curNode = TAIL.mPrev; + while (curNode != HEAD && mValueFunction.applyAsInt(curNode.mApp) > newValue) { + curNode = curNode.mPrev; + } + + // Insert the new node after the found node. + node.mPrev = curNode; + node.mNext = curNode.mNext; + curNode.mNext.mPrev = node; + curNode.mNext = node; } void append(@NonNull ProcessRecordNode node) { @@ -727,34 +833,50 @@ public class OomAdjusterModernImpl extends OomAdjuster { private void updateAdjSlotIfNecessary(ProcessRecord app, int prevRawAdj) { if (app.mState.getCurRawAdj() != prevRawAdj) { - final int slot = adjToSlot(app.mState.getCurRawAdj()); - final int prevSlot = adjToSlot(prevRawAdj); - if (slot != prevSlot && slot != ADJ_SLOT_INVALID) { - mProcessRecordAdjNodes.moveAppTo(app, prevSlot, slot); + if (Flags.simplifyProcessTraversal()) { + mProcessRecordAdjNodes.offer(app); + } else { + final int slot = adjToSlot(app.mState.getCurRawAdj()); + final int prevSlot = adjToSlot(prevRawAdj); + if (slot != prevSlot && slot != ADJ_SLOT_INVALID) { + mProcessRecordAdjNodes.moveAppTo(app, prevSlot, slot); + } } } } private void updateAdjSlot(ProcessRecord app, int prevRawAdj) { - final int slot = adjToSlot(app.mState.getCurRawAdj()); - final int prevSlot = adjToSlot(prevRawAdj); - mProcessRecordAdjNodes.moveAppTo(app, prevSlot, slot); + if (Flags.simplifyProcessTraversal()) { + mProcessRecordAdjNodes.offer(app); + } else { + final int slot = adjToSlot(app.mState.getCurRawAdj()); + final int prevSlot = adjToSlot(prevRawAdj); + mProcessRecordAdjNodes.moveAppTo(app, prevSlot, slot); + } } private void updateProcStateSlotIfNecessary(ProcessRecord app, int prevProcState) { if (app.mState.getCurProcState() != prevProcState) { - final int slot = processStateToSlot(app.mState.getCurProcState()); - final int prevSlot = processStateToSlot(prevProcState); - if (slot != prevSlot) { - mProcessRecordProcStateNodes.moveAppTo(app, prevSlot, slot); + if (Flags.simplifyProcessTraversal()) { + mProcessRecordProcStateNodes.offer(app); + } else { + final int slot = processStateToSlot(app.mState.getCurProcState()); + final int prevSlot = processStateToSlot(prevProcState); + if (slot != prevSlot) { + mProcessRecordProcStateNodes.moveAppTo(app, prevSlot, slot); + } } } } private void updateProcStateSlot(ProcessRecord app, int prevProcState) { - final int slot = processStateToSlot(app.mState.getCurProcState()); - final int prevSlot = processStateToSlot(prevProcState); - mProcessRecordProcStateNodes.moveAppTo(app, prevSlot, slot); + if (Flags.simplifyProcessTraversal()) { + mProcessRecordProcStateNodes.offer(app); + } else { + final int slot = processStateToSlot(app.mState.getCurProcState()); + final int prevSlot = processStateToSlot(prevProcState); + mProcessRecordProcStateNodes.moveAppTo(app, prevSlot, slot); + } } @Override @@ -832,8 +954,15 @@ public class OomAdjusterModernImpl extends OomAdjuster { // Compute initial values, the procState and adj priority queues will be populated here. computeOomAdjLSP(app, UNKNOWN_ADJ, topApp, true, now, false, false, oomAdjReason, false); - updateProcStateSlot(app, prevProcState); - updateAdjSlot(app, prevAdj); + + if (Flags.simplifyProcessTraversal()) { + // Just add to the procState priority queue. The adj priority queue should be + // empty going into the traversal step. + mProcessRecordProcStateNodes.offer(app); + } else { + updateProcStateSlot(app, prevProcState); + updateAdjSlot(app, prevAdj); + } } // Set adj last nodes now, this way a process will only be reevaluated during the adj node @@ -851,14 +980,32 @@ public class OomAdjusterModernImpl extends OomAdjuster { */ @GuardedBy({"mService", "mProcLock"}) private void computeConnectionsLSP() { - // 1st pass, scan each slot in the procstate node list. - for (int i = 0, end = mProcessRecordProcStateNodes.size() - 1; i < end; i++) { - mProcessRecordProcStateNodes.forEachNewNode(i, mComputeConnectionsConsumer); - } + if (Flags.simplifyProcessTraversal()) { + // 1st pass, iterate all nodes in order of procState importance. + ProcessRecord proc = mProcessRecordProcStateNodes.poll(); + while (proc != null) { + mTmpOomAdjusterArgs.mApp = proc; + mComputeConnectionsConsumer.accept(mTmpOomAdjusterArgs); + proc = mProcessRecordProcStateNodes.poll(); + } + + // 2st pass, iterate all nodes in order of procState importance. + proc = mProcessRecordAdjNodes.poll(); + while (proc != null) { + mTmpOomAdjusterArgs.mApp = proc; + mComputeConnectionsConsumer.accept(mTmpOomAdjusterArgs); + proc = mProcessRecordAdjNodes.poll(); + } + } else { + // 1st pass, scan each slot in the procstate node list. + for (int i = 0, end = mProcessRecordProcStateNodes.size() - 1; i < end; i++) { + mProcessRecordProcStateNodes.forEachNewNode(i, mComputeConnectionsConsumer); + } - // 2nd pass, scan each slot in the adj node list. - for (int i = 0, end = mProcessRecordAdjNodes.size() - 1; i < end; i++) { - mProcessRecordAdjNodes.forEachNewNode(i, mComputeConnectionsConsumer); + // 2nd pass, scan each slot in the adj node list. + for (int i = 0, end = mProcessRecordAdjNodes.size() - 1; i < end; i++) { + mProcessRecordAdjNodes.forEachNewNode(i, mComputeConnectionsConsumer); + } } } @@ -987,8 +1134,14 @@ public class OomAdjusterModernImpl extends OomAdjuster { args.mApp = reachable; computeOomAdjIgnoringReachablesLSP(args); - updateProcStateSlot(reachable, prevProcState); - updateAdjSlot(reachable, prevAdj); + if (Flags.simplifyProcessTraversal()) { + // Just add to the procState priority queue. The adj priority queue should be + // empty going into the traversal step. + mProcessRecordProcStateNodes.offer(reachable); + } else { + updateProcStateSlot(reachable, prevProcState); + updateAdjSlot(reachable, prevAdj); + } } } diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index da45a7727faf..8d7a1c9f8228 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -18,6 +18,10 @@ package com.android.server.am; import static android.app.ActivityManager.PROCESS_STATE_TOP; import static android.app.ActivityManager.START_SUCCESS; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -389,13 +393,20 @@ public final class PendingIntentRecord extends IIntentSender.Stub { private static BackgroundStartPrivileges getBackgroundStartPrivilegesAllowedByCaller( @Nullable Bundle options, int callingUid, @Nullable String callingPackage) { - if (options == null || !options.containsKey( - ActivityOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED)) { + if (options == null) { return getDefaultBackgroundStartPrivileges(callingUid, callingPackage); } - return options.getBoolean(ActivityOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED) - ? BackgroundStartPrivileges.ALLOW_BAL - : BackgroundStartPrivileges.NONE; + switch (options.getInt(ActivityOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, + MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED)) { + case MODE_BACKGROUND_ACTIVITY_START_DENIED: + return BackgroundStartPrivileges.NONE; + case MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED: + return getDefaultBackgroundStartPrivileges(callingUid, callingPackage); + case MODE_BACKGROUND_ACTIVITY_START_ALLOWED: + case MODE_BACKGROUND_ACTIVITY_START_COMPAT: + default: + return BackgroundStartPrivileges.ALLOW_BAL; + } } /** diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 9bf5c2174d2c..032093b91746 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -342,33 +342,29 @@ public class SettingsToPropertiesMapper { AsyncTask.THREAD_POOL_EXECUTOR, (DeviceConfig.Properties properties) -> { - HashMap<String, HashMap<String, String>> propsToStage = - getStagedFlagsWithValueChange(properties); + for (String flagName : properties.getKeyset()) { + String flagValue = properties.getString(flagName, null); + if (flagName == null || flagValue == null) { + continue; + } - // send prop stage request to sys prop - for (HashMap.Entry<String, HashMap<String, String>> entry : propsToStage.entrySet()) { - String actualNamespace = entry.getKey(); - HashMap<String, String> flagValuesToStage = entry.getValue(); + int idx = flagName.indexOf(NAMESPACE_REBOOT_STAGING_DELIMITER); + if (idx == -1 || idx == flagName.length() - 1 || idx == 0) { + logErr("invalid staged flag: " + flagName); + continue; + } - for (String flagName : flagValuesToStage.keySet()) { - String stagedValue = flagValuesToStage.get(flagName); + String actualNamespace = flagName.substring(0, idx); + String actualFlagName = flagName.substring(idx+1); String propertyName = "next_boot." + makeAconfigFlagPropertyName( - actualNamespace, flagName); + actualNamespace, actualFlagName); - if (!propertyName.matches(SYSTEM_PROPERTY_VALID_CHARACTERS_REGEX) - || propertyName.contains(SYSTEM_PROPERTY_INVALID_SUBSTRING)) { - logErr("unable to construct system property for " + actualNamespace - + "/" + flagName); - continue; - } - - setProperty(propertyName, stagedValue); - } + setProperty(propertyName, flagValue); } // send prop stage request to new storage if (enableAconfigStorageDaemon()) { - stageFlagsInNewStorage(propsToStage); + stageFlagsInNewStorage(properties); } }); @@ -607,25 +603,33 @@ public class SettingsToPropertiesMapper { * @param propsToStage */ @VisibleForTesting - static void stageFlagsInNewStorage(HashMap<String, HashMap<String, String>> propsToStage) { + static void stageFlagsInNewStorage(DeviceConfig.Properties props) { // write aconfigd requests proto to proto output stream int num_requests = 0; ProtoOutputStream requests = new ProtoOutputStream(); - for (HashMap.Entry<String, HashMap<String, String>> entry : propsToStage.entrySet()) { - String actualNamespace = entry.getKey(); - HashMap<String, String> flagValuesToStage = entry.getValue(); - for (String fullFlagName : flagValuesToStage.keySet()) { - String stagedValue = flagValuesToStage.get(fullFlagName); - int idx = fullFlagName.lastIndexOf("."); - if (idx == -1) { - logErr("invalid flag name: " + fullFlagName); - continue; - } - String packageName = fullFlagName.substring(0, idx); - String flagName = fullFlagName.substring(idx+1); - writeFlagOverrideRequest(requests, packageName, flagName, stagedValue, false); - ++num_requests; + for (String flagName : props.getKeyset()) { + String flagValue = props.getString(flagName, null); + if (flagName == null || flagValue == null) { + continue; } + + int idx = flagName.indexOf("*"); + if (idx == -1 || idx == flagName.length() - 1 || idx == 0) { + logErr("invalid local flag override: " + flagName); + continue; + } + String actualNamespace = flagName.substring(0, idx); + String fullFlagName = flagName.substring(idx+1); + + idx = fullFlagName.lastIndexOf("."); + if (idx == -1) { + logErr("invalid flag name: " + fullFlagName); + continue; + } + String packageName = fullFlagName.substring(0, idx); + String realFlagName = fullFlagName.substring(idx+1); + writeFlagOverrideRequest(requests, packageName, realFlagName, flagValue, false); + ++num_requests; } if (num_requests == 0) { @@ -637,7 +641,7 @@ public class SettingsToPropertiesMapper { // deserialize back using proto input stream try { - parseAndLogAconfigdReturn(returns); + parseAndLogAconfigdReturn(returns); } catch (IOException ioe) { logErr("failed to parse aconfigd return", ioe); } @@ -665,63 +669,6 @@ public class SettingsToPropertiesMapper { return propertyName; } - /** - * Get the flags that need to be staged in sys prop, only these with a real value - * change needs to be staged in sys prop. Otherwise, the flag stage is useless and - * create performance problem at sys prop side. - * @param properties - * @return a hash map of namespace name to actual flags to stage - */ - @VisibleForTesting - static HashMap<String, HashMap<String, String>> getStagedFlagsWithValueChange( - DeviceConfig.Properties properties) { - - // sort flags by actual namespace of the flag - HashMap<String, HashMap<String, String>> stagedProps = new HashMap<>(); - for (String flagName : properties.getKeyset()) { - int idx = flagName.indexOf(NAMESPACE_REBOOT_STAGING_DELIMITER); - if (idx == -1 || idx == flagName.length() - 1 || idx == 0) { - logErr("invalid staged flag: " + flagName); - continue; - } - String actualNamespace = flagName.substring(0, idx); - String actualFlagName = flagName.substring(idx+1); - HashMap<String, String> flagStagedValues = stagedProps.get(actualNamespace); - if (flagStagedValues == null) { - flagStagedValues = new HashMap<String, String>(); - stagedProps.put(actualNamespace, flagStagedValues); - } - flagStagedValues.put(actualFlagName, properties.getString(flagName, null)); - } - - // for each namespace, find flags with real flag value change - HashMap<String, HashMap<String, String>> propsToStage = new HashMap<>(); - for (HashMap.Entry<String, HashMap<String, String>> entry : stagedProps.entrySet()) { - String actualNamespace = entry.getKey(); - HashMap<String, String> flagStagedValues = entry.getValue(); - Map<String, String> flagCurrentValues = Settings.Config.getStrings( - actualNamespace, new ArrayList<String>(flagStagedValues.keySet())); - - HashMap<String, String> flagsToStage = new HashMap<>(); - for (String flagName : flagStagedValues.keySet()) { - String stagedValue = flagStagedValues.get(flagName); - String currentValue = flagCurrentValues.get(flagName); - if (stagedValue == null) { - continue; - } - if (currentValue == null || !stagedValue.equalsIgnoreCase(currentValue)) { - flagsToStage.put(flagName, stagedValue); - } - } - - if (!flagsToStage.isEmpty()) { - propsToStage.put(actualNamespace, flagsToStage); - } - } - - return propsToStage; - } - private void setProperty(String key, String value) { // Check if need to clear the property if (value == null) { diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index b7108dfcbac3..afde4f71a95f 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -123,3 +123,14 @@ flag { description: "Avoid OomAdjuster calculations for connections that won't change importance" bug: "323376416" } + +flag { + name: "simplify_process_traversal" + namespace: "backstage_power" + description: "Simplify the OomAdjuster's process traversal mechanism." + bug: "336178916" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 475334c69313..1dc1846fbb96 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -1439,7 +1439,6 @@ public class AudioDeviceBroker { sendMsgNoDelay(MSG_BROADCAST_AUDIO_BECOMING_NOISY, SENDMSG_REPLACE); } - @GuardedBy("mDeviceStateLock") /*package*/ void postBluetoothActiveDevice(BtDeviceInfo info, int delay) { sendLMsg(MSG_L_SET_BT_ACTIVE_DEVICE, SENDMSG_QUEUE, info, delay); } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 15c5c1077803..7deef2ffb5dd 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -7897,7 +7897,7 @@ public class AudioService extends IAudioService.Stub + previousDevice + " -> " + newDevice + ". Got: " + profile); } - sDeviceLogger.enqueue(new EventLogger.StringEvent("BlutoothActiveDeviceChanged for " + sDeviceLogger.enqueue(new EventLogger.StringEvent("BluetoothActiveDeviceChanged for " + BluetoothProfile.getProfileName(profile) + ", device update " + previousDevice + " -> " + newDevice).printLog(TAG)); AudioDeviceBroker.BtDeviceChangedData data = diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index a649d34884a7..07daecdfc9f6 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -289,6 +289,7 @@ public class BtHelper { Log.e(TAG, "Exception while getting status of " + device, e); } if (btCodecStatus == null) { + Log.e(TAG, "getCodec, null A2DP codec status for device: " + device); mA2dpCodecConfig = null; return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, changed); } @@ -316,6 +317,7 @@ public class BtHelper { Log.e(TAG, "Exception while getting status of " + device, e); } if (btLeCodecStatus == null) { + Log.e(TAG, "getCodec, null LE codec status for device: " + device); mLeAudioCodecConfig = null; return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, changed); } @@ -363,6 +365,7 @@ public class BtHelper { return new Pair<>(profile == BluetoothProfile.A2DP ? AudioSystem.AUDIO_FORMAT_SBC : AudioSystem.AUDIO_FORMAT_LC3, true); } + return codecAndChanged; } @@ -653,7 +656,7 @@ public class BtHelper { // Not a valid profile to connect Log.e(TAG, "onBtProfileConnected: Not a valid profile to connect " + BluetoothProfile.getProfileName(profile)); - break; + return; } // this part is only for A2DP, LE Audio unicast and Hearing aid @@ -664,17 +667,65 @@ public class BtHelper { return; } List<BluetoothDevice> activeDevices = adapter.getActiveDevices(profile); - BluetoothProfileConnectionInfo bpci = new BluetoothProfileConnectionInfo(profile); - for (BluetoothDevice device : activeDevices) { - if (device == null) { - continue; - } - AudioDeviceBroker.BtDeviceChangedData data = new AudioDeviceBroker.BtDeviceChangedData( - device, null, bpci, "mBluetoothProfileServiceListener"); - AudioDeviceBroker.BtDeviceInfo info = mDeviceBroker.createBtDeviceInfo( - data, device, BluetoothProfile.STATE_CONNECTED); - mDeviceBroker.postBluetoothActiveDevice(info, 0 /* delay */); + if (activeDevices.isEmpty() || activeDevices.get(0) == null) { + return; } + BluetoothDevice device = activeDevices.get(0); + switch (profile) { + case BluetoothProfile.A2DP: { + BluetoothProfileConnectionInfo bpci = + BluetoothProfileConnectionInfo.createA2dpInfo(false, -1); + postBluetoothActiveDevice(device, bpci); + } break; + case BluetoothProfile.HEARING_AID: { + BluetoothProfileConnectionInfo bpci = + BluetoothProfileConnectionInfo.createHearingAidInfo(false); + postBluetoothActiveDevice(device, bpci); + } break; + case BluetoothProfile.LE_AUDIO: { + int groupId = mLeAudio.getGroupId(device); + BluetoothLeAudioCodecStatus btLeCodecStatus = null; + try { + btLeCodecStatus = mLeAudio.getCodecStatus(groupId); + } catch (Exception e) { + Log.e(TAG, "Exception while getting status of " + device, e); + } + if (btLeCodecStatus == null) { + Log.i(TAG, "onBtProfileConnected null LE codec status for groupId: " + + groupId + ", device: " + device); + break; + } + List<BluetoothLeAudioCodecConfig> outputCodecConfigs = + btLeCodecStatus.getOutputCodecSelectableCapabilities(); + if (!outputCodecConfigs.isEmpty()) { + BluetoothProfileConnectionInfo bpci = + BluetoothProfileConnectionInfo.createLeAudioInfo( + false /*suppressNoisyIntent*/, true /*isLeOutput*/); + postBluetoothActiveDevice(device, bpci); + } + List<BluetoothLeAudioCodecConfig> inputCodecConfigs = + btLeCodecStatus.getInputCodecSelectableCapabilities(); + if (!inputCodecConfigs.isEmpty()) { + BluetoothProfileConnectionInfo bpci = + BluetoothProfileConnectionInfo.createLeAudioInfo( + false /*suppressNoisyIntent*/, false /*isLeOutput*/); + postBluetoothActiveDevice(device, bpci); + } + } break; + default: + // Not a valid profile to connect + Log.wtf(TAG, "Invalid profile! onBtProfileConnected"); + break; + } + } + + private void postBluetoothActiveDevice( + BluetoothDevice device, BluetoothProfileConnectionInfo bpci) { + AudioDeviceBroker.BtDeviceChangedData data = new AudioDeviceBroker.BtDeviceChangedData( + device, null, bpci, "mBluetoothProfileServiceListener"); + AudioDeviceBroker.BtDeviceInfo info = mDeviceBroker.createBtDeviceInfo( + data, device, BluetoothProfile.STATE_CONNECTED); + mDeviceBroker.postBluetoothActiveDevice(info, 0 /* delay */); } /*package*/ synchronized boolean isProfilePoxyConnected(int profile) { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java index d64b6c29c840..8dc560b0e0b5 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java @@ -20,6 +20,9 @@ import android.annotation.NonNull; import android.content.Context; import android.hardware.biometrics.ITestSession; import android.hardware.biometrics.ITestSessionCallback; +import android.hardware.biometrics.fingerprint.AcquiredInfoAndVendorCode; +import android.hardware.biometrics.fingerprint.EnrollmentProgressStep; +import android.hardware.biometrics.fingerprint.NextEnrollment; import android.hardware.fingerprint.Fingerprint; import android.hardware.fingerprint.FingerprintEnrollOptions; import android.hardware.fingerprint.FingerprintManager; @@ -46,6 +49,7 @@ import java.util.Set; class BiometricTestSessionImpl extends ITestSession.Stub { private static final String TAG = "fp/aidl/BiometricTestSessionImpl"; + private static final int VHAL_ENROLLMENT_ID = 9999; @NonNull private final Context mContext; private final int mSensorId; @@ -140,8 +144,8 @@ class BiometricTestSessionImpl extends ITestSession.Stub { super.setTestHalEnabled_enforcePermission(); - mProvider.setTestHalEnabled(enabled); mSensor.setTestHalEnabled(enabled); + mProvider.setTestHalEnabled(enabled); } @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) @@ -157,10 +161,31 @@ class BiometricTestSessionImpl extends ITestSession.Stub { @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) @Override - public void finishEnroll(int userId) { + public void finishEnroll(int userId) throws RemoteException { super.finishEnroll_enforcePermission(); + Slog.i(TAG, "finishEnroll(): useVhalForTesting=" + mProvider.useVhalForTesting()); + if (mProvider.useVhalForTesting()) { + final AcquiredInfoAndVendorCode[] acquiredInfoAndVendorCodes = + {new AcquiredInfoAndVendorCode()}; + final EnrollmentProgressStep[] enrollmentProgressSteps = + {new EnrollmentProgressStep(), new EnrollmentProgressStep()}; + enrollmentProgressSteps[0].durationMs = 100; + enrollmentProgressSteps[0].acquiredInfoAndVendorCodes = acquiredInfoAndVendorCodes; + enrollmentProgressSteps[1].durationMs = 200; + enrollmentProgressSteps[1].acquiredInfoAndVendorCodes = acquiredInfoAndVendorCodes; + + final NextEnrollment nextEnrollment = new NextEnrollment(); + nextEnrollment.id = VHAL_ENROLLMENT_ID; + nextEnrollment.progressSteps = enrollmentProgressSteps; + nextEnrollment.result = true; + mProvider.getVhal().setNextEnrollment(nextEnrollment); + mProvider.simulateVhalFingerDown(userId, mSensorId); + return; + } + + //TODO (b341889971): delete the following lines when b/341889971 is resolved int nextRandomId = mRandom.nextInt(); while (mEnrollmentIds.contains(nextRandomId)) { nextRandomId = mRandom.nextInt(); @@ -173,11 +198,18 @@ class BiometricTestSessionImpl extends ITestSession.Stub { @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) @Override - public void acceptAuthentication(int userId) { + public void acceptAuthentication(int userId) throws RemoteException { // Fake authentication with any of the existing fingers super.acceptAuthentication_enforcePermission(); + if (mProvider.useVhalForTesting()) { + mProvider.getVhal().setEnrollmentHit(VHAL_ENROLLMENT_ID); + mProvider.simulateVhalFingerDown(userId, mSensorId); + return; + } + + //TODO (b341889971): delete the following lines when b/341889971 is resolved List<Fingerprint> fingerprints = FingerprintUtils.getInstance(mSensorId) .getBiometricsForUser(mContext, userId); if (fingerprints.isEmpty()) { @@ -191,10 +223,17 @@ class BiometricTestSessionImpl extends ITestSession.Stub { @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) @Override - public void rejectAuthentication(int userId) { + public void rejectAuthentication(int userId) throws RemoteException { super.rejectAuthentication_enforcePermission(); + if (mProvider.useVhalForTesting()) { + mProvider.getVhal().setEnrollmentHit(VHAL_ENROLLMENT_ID + 1); + mProvider.simulateVhalFingerDown(userId, mSensorId); + return; + } + + //TODO (b341889971): delete the following lines when b/341889971 is resolved mSensor.getSessionForUser(userId).getHalSessionCallback().onAuthenticationFailed(); } @@ -220,11 +259,17 @@ class BiometricTestSessionImpl extends ITestSession.Stub { @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) @Override - public void cleanupInternalState(int userId) { + public void cleanupInternalState(int userId) throws RemoteException { super.cleanupInternalState_enforcePermission(); Slog.d(TAG, "cleanupInternalState: " + userId); + + if (mProvider.useVhalForTesting()) { + Slog.i(TAG, "cleanup virtualhal configurations"); + mProvider.getVhal().resetConfigurations(); //setEnrollments(new int[]{}); + } + mProvider.scheduleInternalCleanup(mSensorId, userId, new ClientMonitorCallback() { @Override public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) { @@ -248,4 +293,4 @@ class BiometricTestSessionImpl extends ITestSession.Stub { } }); } -} +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index c0dcd4943260..1bddb83b1441 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -890,7 +890,13 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi } void setTestHalEnabled(boolean enabled) { + final boolean changed = enabled != mTestHalEnabled; mTestHalEnabled = enabled; + Slog.i(getTag(), "setTestHalEnabled(): useVhalForTesting=" + Flags.useVhalForTesting() + + "mTestHalEnabled=" + mTestHalEnabled + " changed=" + changed); + if (changed && useVhalForTesting()) { + getHalInstance(); + } } public boolean getTestHalEnabled() { @@ -982,7 +988,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi if (mVhal == null && useVhalForTesting()) { mVhal = IVirtualHal.Stub.asInterface(mDaemon.asBinder().getExtension()); if (mVhal == null) { - Slog.e(getTag(), "Unable to get virtual hal interface"); + Slog.e(getTag(), "Unable to get fingerprint virtualhal interface"); } } diff --git a/services/core/java/com/android/server/display/DisplayControl.java b/services/core/java/com/android/server/display/DisplayControl.java index fa8299bd45fd..38eb416ffdd8 100644 --- a/services/core/java/com/android/server/display/DisplayControl.java +++ b/services/core/java/com/android/server/display/DisplayControl.java @@ -28,9 +28,9 @@ import java.util.Objects; * Calls into SurfaceFlinger for Display creation and deletion. */ public class DisplayControl { - private static native IBinder nativeCreateDisplay(String name, boolean secure, + private static native IBinder nativeCreateVirtualDisplay(String name, boolean secure, String uniqueId, float requestedRefreshRate); - private static native void nativeDestroyDisplay(IBinder displayToken); + private static native void nativeDestroyVirtualDisplay(IBinder displayToken); private static native void nativeOverrideHdrTypes(IBinder displayToken, int[] modes); private static native long[] nativeGetPhysicalDisplayIds(); private static native IBinder nativeGetPhysicalDisplayToken(long physicalDisplayId); @@ -41,21 +41,21 @@ public class DisplayControl { private static native boolean nativeGetHdrOutputConversionSupport(); /** - * Create a display in SurfaceFlinger. + * Create a virtual display in SurfaceFlinger. * - * @param name The name of the display. + * @param name The name of the virtual display. * @param secure Whether this display is secure. * @return The token reference for the display in SurfaceFlinger. */ - public static IBinder createDisplay(String name, boolean secure) { + public static IBinder createVirtualDisplay(String name, boolean secure) { Objects.requireNonNull(name, "name must not be null"); - return nativeCreateDisplay(name, secure, "", 0.0f); + return nativeCreateVirtualDisplay(name, secure, "", 0.0f); } /** - * Create a display in SurfaceFlinger. + * Create a virtual display in SurfaceFlinger. * - * @param name The name of the display. + * @param name The name of the virtual display. * @param secure Whether this display is secure. * @param uniqueId The unique ID for the display. * @param requestedRefreshRate The requested refresh rate in frames per second. @@ -65,23 +65,23 @@ public class DisplayControl { * display is refreshed at the physical display refresh rate. * @return The token reference for the display in SurfaceFlinger. */ - public static IBinder createDisplay(String name, boolean secure, + public static IBinder createVirtualDisplay(String name, boolean secure, String uniqueId, float requestedRefreshRate) { Objects.requireNonNull(name, "name must not be null"); Objects.requireNonNull(uniqueId, "uniqueId must not be null"); - return nativeCreateDisplay(name, secure, uniqueId, requestedRefreshRate); + return nativeCreateVirtualDisplay(name, secure, uniqueId, requestedRefreshRate); } /** - * Destroy a display in SurfaceFlinger. + * Destroy a virtual display in SurfaceFlinger. * - * @param displayToken The display token for the display to be destroyed. + * @param displayToken The display token for the virtual display to be destroyed. */ - public static void destroyDisplay(IBinder displayToken) { + public static void destroyVirtualDisplay(IBinder displayToken) { if (displayToken == null) { throw new IllegalArgumentException("displayToken must not be null"); } - nativeDestroyDisplay(displayToken); + nativeDestroyVirtualDisplay(displayToken); } /** diff --git a/services/core/java/com/android/server/display/OverlayDisplayAdapter.java b/services/core/java/com/android/server/display/OverlayDisplayAdapter.java index 22ff2d0eeffd..eb76dcba3b85 100644 --- a/services/core/java/com/android/server/display/OverlayDisplayAdapter.java +++ b/services/core/java/com/android/server/display/OverlayDisplayAdapter.java @@ -309,7 +309,7 @@ final class OverlayDisplayAdapter extends DisplayAdapter { mSurface.release(); mSurface = null; } - DisplayControl.destroyDisplay(getDisplayTokenLocked()); + DisplayControl.destroyVirtualDisplay(getDisplayTokenLocked()); } @Override @@ -467,7 +467,7 @@ final class OverlayDisplayAdapter extends DisplayAdapter { public void onWindowCreated(SurfaceTexture surfaceTexture, float refreshRate, long presentationDeadlineNanos, int state) { synchronized (getSyncRoot()) { - IBinder displayToken = DisplayControl.createDisplay(mName, mFlags.mSecure); + IBinder displayToken = DisplayControl.createVirtualDisplay(mName, mFlags.mSecure); mDevice = new OverlayDisplayDevice(displayToken, mName, mModes, mActiveMode, DEFAULT_MODE_INDEX, refreshRate, presentationDeadlineNanos, mFlags, state, surfaceTexture, mNumber) { diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index a29e8523952d..1a5c79fada55 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -94,12 +94,13 @@ public class VirtualDisplayAdapter extends DisplayAdapter { @Override public IBinder createDisplay(String name, boolean secure, String uniqueId, float requestedRefreshRate) { - return DisplayControl.createDisplay(name, secure, uniqueId, requestedRefreshRate); + return DisplayControl.createVirtualDisplay(name, secure, uniqueId, + requestedRefreshRate); } @Override public void destroyDisplay(IBinder displayToken) { - DisplayControl.destroyDisplay(displayToken); + DisplayControl.destroyVirtualDisplay(displayToken); } }, featureFlags); } diff --git a/services/core/java/com/android/server/display/WifiDisplayAdapter.java b/services/core/java/com/android/server/display/WifiDisplayAdapter.java index aa98cd85d38e..607c5d6a88bc 100644 --- a/services/core/java/com/android/server/display/WifiDisplayAdapter.java +++ b/services/core/java/com/android/server/display/WifiDisplayAdapter.java @@ -392,9 +392,9 @@ final class WifiDisplayAdapter extends DisplayAdapter { float refreshRate = 60.0f; // TODO: get this for real - String name = display.getFriendlyDisplayName(); - String address = display.getDeviceAddress(); - IBinder displayToken = DisplayControl.createDisplay(name, secure); + final String name = display.getFriendlyDisplayName(); + final String address = display.getDeviceAddress(); + IBinder displayToken = DisplayControl.createVirtualDisplay(name, secure); mDisplayDevice = new WifiDisplayDevice(displayToken, name, width, height, refreshRate, deviceFlags, address, surface); sendDisplayDeviceEventLocked(mDisplayDevice, DISPLAY_DEVICE_EVENT_ADDED); @@ -631,7 +631,7 @@ final class WifiDisplayAdapter extends DisplayAdapter { mSurface.release(); mSurface = null; } - DisplayControl.destroyDisplay(getDisplayTokenLocked()); + DisplayControl.destroyVirtualDisplay(getDisplayTokenLocked()); } public void setNameLocked(String name) { diff --git a/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java b/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java index ad98b4a8db13..a3b1a2df9577 100644 --- a/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java +++ b/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java @@ -102,26 +102,33 @@ final class AutofillSuggestionsController { boolean touchExplorationEnabled) { clearPendingInlineSuggestionsRequest(); mInlineSuggestionsRequestCallback = callback; - final InputMethodInfo imi = mService.queryInputMethodForCurrentUserLocked( - mService.getSelectedMethodIdLocked()); - - if (userId == mService.getCurrentImeUserIdLocked() - && imi != null && isInlineSuggestionsEnabled(imi, touchExplorationEnabled)) { - mPendingInlineSuggestionsRequest = new CreateInlineSuggestionsRequest( - requestInfo, callback, imi.getPackageName()); - if (mService.getCurMethodLocked() != null) { - // In the normal case when the IME is connected, we can make the request here. - performOnCreateInlineSuggestionsRequest(); - } else { - // Otherwise, the next time the IME connection is established, - // InputMethodBindingController.mMainConnection#onServiceConnected() will call - // into #performOnCreateInlineSuggestionsRequestLocked() to make the request. - if (DEBUG) { - Slog.d(TAG, "IME not connected. Delaying inline suggestions request."); - } - } - } else { + + if (userId != mService.getCurrentImeUserIdLocked()) { callback.onInlineSuggestionsUnsupported(); + return; + } + + // Note that current user ID is guaranteed to be userId. + final var imeId = mService.getSelectedMethodIdLocked(); + final InputMethodInfo imi = InputMethodSettingsRepository.get(userId).getMethodMap() + .get(imeId); + if (imi == null || !isInlineSuggestionsEnabled(imi, touchExplorationEnabled)) { + callback.onInlineSuggestionsUnsupported(); + return; + } + + mPendingInlineSuggestionsRequest = new CreateInlineSuggestionsRequest( + requestInfo, callback, imi.getPackageName()); + if (mService.getCurMethodLocked() != null) { + // In the normal case when the IME is connected, we can make the request here. + performOnCreateInlineSuggestionsRequest(); + } else { + // Otherwise, the next time the IME connection is established, + // InputMethodBindingController.mMainConnection#onServiceConnected() will call + // into #performOnCreateInlineSuggestionsRequestLocked() to make the request. + if (DEBUG) { + Slog.d(TAG, "IME not connected. Delaying inline suggestions request."); + } } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 7548b3679bae..f45b82aa2cd4 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -491,6 +491,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. */ boolean mSystemReady; + @GuardedBy("ImfLock.class") + @NonNull + InputMethodBindingController getInputMethodBindingController(@UserIdInt int userId) { + return mUserDataRepository.getOrCreate(userId).mBindingController; + } + /** * Id obtained with {@link InputMethodInfo#getId()} for the currently selected input method. * This is to be synchronized with the secure settings keyed with @@ -507,8 +513,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") @Nullable String getSelectedMethodIdLocked() { - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - return userData.mBindingController.getSelectedMethodId(); + return getInputMethodBindingController(mCurrentUserId).getSelectedMethodId(); } @GuardedBy("ImfLock.class") @@ -594,8 +599,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") @Nullable IBinder getCurTokenLocked() { - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - return userData.mBindingController.getCurToken(); + return getInputMethodBindingController(mCurrentUserId).getCurToken(); } /** @@ -603,8 +607,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. */ @GuardedBy("ImfLock.class") int getCurTokenDisplayIdLocked() { - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - return userData.mBindingController.getCurTokenDisplayId(); + return getInputMethodBindingController(mCurrentUserId).getCurTokenDisplayId(); } /** @@ -620,8 +623,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") @Nullable IInputMethodInvoker getCurMethodLocked() { - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - return userData.mBindingController.getCurMethod(); + return getInputMethodBindingController(mCurrentUserId).getCurMethod(); } /** @@ -1127,6 +1129,11 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) { // Called on ActivityManager thread. synchronized (ImfLock.class) { + if (mService.mExperimentalConcurrentMultiUserModeEnabled) { + // In concurrent multi-user mode, we in general do not rely on the concept of + // current user. + return; + } mService.scheduleSwitchUserTaskLocked(to.getUserIdentifier(), /* clientToBeReset= */ null); } @@ -1269,7 +1276,15 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. InputMethodSettingsRepository.initialize(mHandler, mContext); AdditionalSubtypeMapRepository.initialize(mHandler, mContext); - mCurrentUserId = mActivityManagerInternal.getCurrentUserId(); + final int currentUserId = mActivityManagerInternal.getCurrentUserId(); + + // For concurrent multi-user mode, we try to initialize mCurrentUserId with main + // user rather than the current user when possible. + mCurrentUserId = mExperimentalConcurrentMultiUserModeEnabled + ? MultiUserUtils.getFirstMainUserIdOrDefault( + mUserManagerInternal, currentUserId) + : currentUserId; + @SuppressWarnings("GuardedBy") final IntFunction<InputMethodBindingController> bindingControllerFactory = userId -> new InputMethodBindingController(userId, InputMethodManagerService.this); @@ -1422,8 +1437,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // Note that in b/197848765 we want to see if we can keep the binding alive for better // profile switching. - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(mCurrentUserId); bindingController.unbindCurrentMethod(); unbindCurrentClientLocked(UnbindReason.SWITCH_USER); @@ -1643,8 +1657,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // Check if selected IME of current user supports handwriting. if (userId == mCurrentUserId) { - final var userData = mUserDataRepository.getOrCreate(userId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(userId); return bindingController.supportsStylusHandwriting() && (!connectionless || bindingController.supportsConnectionlessStylusHandwriting()); @@ -1844,8 +1857,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // TODO(b/325515685): make binding controller user independent. Before this change, the // following dependencies also need to be user independent: mCurClient, mBoundToMethod, // getCurMethodLocked(), and mMenuController. - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(mCurrentUserId); mCurClient.mClient.onUnbindMethod(bindingController.getSequenceNumber(), unbindClientReason); mCurClient.mSessionRequested = false; @@ -1925,8 +1937,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final boolean restarting = !initial; final Binder startInputToken = new Binder(); - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(mCurrentUserId); final StartInputInfo info = new StartInputInfo(mCurrentUserId, getCurTokenLocked(), getCurTokenDisplayIdLocked(), bindingController.getCurId(), startInputReason, @@ -2032,7 +2043,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @StartInputReason int startInputReason, int unverifiedTargetSdkVersion, @NonNull ImeOnBackInvokedDispatcher imeDispatcher, - @NonNull UserDataRepository.UserData userData) { + @NonNull InputMethodBindingController bindingController) { // Compute the final shown display ID with validated cs.selfReportedDisplayId for this // session & other conditions. @@ -2073,7 +2084,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final boolean connectionWasActive = mCurInputConnection != null; // Bump up the sequence for this client and attach it. - final var bindingController = userData.mBindingController; bindingController.advanceSequenceNumber(); mCurClient = cs; @@ -2133,7 +2143,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. (startInputFlags & StartInputFlags.INITIAL_CONNECTION) != 0); } - InputBindResult bindResult = tryReuseConnectionLocked(userData, cs); + InputBindResult bindResult = tryReuseConnectionLocked(bindingController, cs); if (bindResult != null) { return bindResult; } @@ -2247,9 +2257,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") @Nullable - private InputBindResult tryReuseConnectionLocked(@NonNull UserDataRepository.UserData userData, - @NonNull ClientState cs) { - final var bindingController = userData.mBindingController; + private InputBindResult tryReuseConnectionLocked( + @NonNull InputMethodBindingController bindingController, @NonNull ClientState cs) { if (bindingController.hasMainConnection()) { if (getCurMethodLocked() != null) { // Return to client, and we will get back with it when @@ -2631,8 +2640,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // When the IME switcher dialog is shown, the IME switcher button should be hidden. if (mMenuController.getSwitchingDialogLocked() != null) return false; // When we are switching IMEs, the IME switcher button should be hidden. - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - if (!Objects.equals(userData.mBindingController.getCurId(), getSelectedMethodIdLocked())) { + final var bindingController = getInputMethodBindingController(mCurrentUserId); + if (!Objects.equals(bindingController.getCurId(), + bindingController.getSelectedMethodId())) { return false; } if (mWindowManagerInternal.isKeyguardShowingAndNotOccluded() @@ -2794,8 +2804,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } else { vis &= ~InputMethodService.IME_VISIBLE_IMPERCEPTIBLE; } - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - final var curId = userData.mBindingController.getCurId(); + final var curId = getInputMethodBindingController(mCurrentUserId).getCurId(); if (mMenuController.getSwitchingDialogLocked() != null || !Objects.equals(curId, getSelectedMethodIdLocked())) { // When the IME switcher dialog is shown, or we are switching IMEs, @@ -2856,8 +2865,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. id = imi.getId(); settings.putSelectedInputMethod(id); } - final var userData = mUserDataRepository.getOrCreate(userId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(userId); bindingController.setSelectedMethodId(id); } @@ -3033,8 +3041,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // mCurMethodId should be updated after setSelectedInputMethodAndSubtypeLocked() // because mCurMethodId is stored as a history in // setSelectedInputMethodAndSubtypeLocked(). - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - userData.mBindingController.setSelectedMethodId(id); + getInputMethodBindingController(mCurrentUserId).setSelectedMethodId(id); if (mActivityManagerInternal.isSystemReady()) { Intent intent = new Intent(Intent.ACTION_INPUT_METHOD_CHANGED); @@ -3089,8 +3096,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @Nullable String delegatorPackageName, @NonNull IConnectionlessHandwritingCallback callback) { synchronized (ImfLock.class) { - final var userData = mUserDataRepository.getOrCreate(userId); - if (!userData.mBindingController.supportsConnectionlessStylusHandwriting()) { + final var bindingController = getInputMethodBindingController(userId); + if (!bindingController.supportsConnectionlessStylusHandwriting()) { Slog.w(TAG, "Connectionless stylus handwriting mode unsupported by IME."); try { callback.onError(CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED); @@ -3173,8 +3180,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final long ident = Binder.clearCallingIdentity(); try { - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - if (!userData.mBindingController.supportsStylusHandwriting()) { + final var bindingController = getInputMethodBindingController(mCurrentUserId); + if (!bindingController.supportsStylusHandwriting()) { Slog.w(TAG, "Stylus HW unsupported by IME. Ignoring startStylusHandwriting()"); return false; @@ -3357,8 +3364,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. mVisibilityStateComputer.requestImeVisibility(windowToken, true); // Ensure binding the connection when IME is going to show. - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - userData.mBindingController.setCurrentMethodVisible(); + final var bindingController = getInputMethodBindingController(mCurrentUserId); + bindingController.setCurrentMethodVisible(); final IInputMethodInvoker curMethod = getCurMethodLocked(); ImeTracker.forLogging().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME); final boolean readyToDispatchToIme; @@ -3466,8 +3473,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } else { ImeTracker.forLogging().onCancelled(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE); } - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - userData.mBindingController.setCurrentMethodNotVisible(); + final var bindingController = getInputMethodBindingController(mCurrentUserId); + bindingController.setCurrentMethodNotVisible(); mVisibilityStateComputer.clearImeShowFlags(); // Cancel existing statsToken for show IME as we got a hide request. ImeTracker.forLogging().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME); @@ -3535,8 +3542,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. "InputMethodManagerService#startInputOrWindowGainedFocus", mDumper); final InputBindResult result; synchronized (ImfLock.class) { - final var userData = mUserDataRepository.getOrCreate(userId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(userId); // If the system is not yet ready, we shouldn't be running third party code. if (!mSystemReady) { return new InputBindResult( @@ -3553,7 +3559,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final long ident = Binder.clearCallingIdentity(); try { // Verify if IMMS is in the process of switching user. - if (mUserSwitchHandlerTask != null) { + if (!mExperimentalConcurrentMultiUserModeEnabled + && mUserSwitchHandlerTask != null) { // There is already an on-going pending user switch task. final int nextUserId = mUserSwitchHandlerTask.mToUserId; if (userId == nextUserId) { @@ -3607,7 +3614,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } // Verify if caller is a background user. - if (userId != mCurrentUserId) { + if (!mExperimentalConcurrentMultiUserModeEnabled && userId != mCurrentUserId) { if (ArrayUtils.contains( mUserManagerInternal.getProfileIds(mCurrentUserId, false), userId)) { @@ -3635,7 +3642,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. result = startInputOrWindowGainedFocusInternalLocked(startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection, - unverifiedTargetSdkVersion, userData, imeDispatcher, cs); + unverifiedTargetSdkVersion, bindingController, imeDispatcher, cs); } finally { Binder.restoreCallingIdentity(ident); } @@ -3663,7 +3670,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @SoftInputModeFlags int softInputMode, int windowFlags, EditorInfo editorInfo, IRemoteInputConnection inputContext, @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, - int unverifiedTargetSdkVersion, @NonNull UserDataRepository.UserData userData, + int unverifiedTargetSdkVersion, @NonNull InputMethodBindingController bindingController, @NonNull ImeOnBackInvokedDispatcher imeDispatcher, @NonNull ClientState cs) { if (DEBUG) { Slog.v(TAG, "startInputOrWindowGainedFocusInternalLocked: reason=" @@ -3676,7 +3683,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. + " softInputMode=" + InputMethodDebug.softInputModeToString(softInputMode) + " windowFlags=#" + Integer.toHexString(windowFlags) + " unverifiedTargetSdkVersion=" + unverifiedTargetSdkVersion - + " userData=" + userData + + " bindingController=" + bindingController + " imeDispatcher=" + imeDispatcher + " cs=" + cs); } @@ -3705,15 +3712,16 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (editorInfo != null) { return startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection, editorInfo, startInputFlags, - startInputReason, unverifiedTargetSdkVersion, imeDispatcher, userData); + startInputReason, unverifiedTargetSdkVersion, imeDispatcher, + bindingController); } return new InputBindResult( InputBindResult.ResultCode.SUCCESS_REPORT_WINDOW_FOCUS_ONLY, null, null, null, null, -1, false); } - mImeBindingState = new ImeBindingState(userData.mUserId, windowToken, softInputMode, cs, - editorInfo); + mImeBindingState = new ImeBindingState(bindingController.mUserId, windowToken, + softInputMode, cs, editorInfo); mFocusedWindowPerceptible.put(windowToken, true); // We want to start input before showing the IME, but after closing @@ -3738,7 +3746,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection, editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion, - imeDispatcher, userData); + imeDispatcher, bindingController); didStart = true; } break; @@ -3753,7 +3761,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // Note that we can trust client's display ID as long as it matches // to the display ID obtained from the window. if (cs.mSelfReportedDisplayId != getCurTokenDisplayIdLocked()) { - userData.mBindingController.unbindCurrentMethod(); + bindingController.unbindCurrentMethod(); } } } @@ -3762,7 +3770,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection, editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion, - imeDispatcher, userData); + imeDispatcher, bindingController); } else { res = InputBindResult.NULL_EDITOR_INFO; } @@ -3803,8 +3811,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (mCurrentUserId != UserHandle.getUserId(uid)) { return false; } - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - final var curIntent = userData.mBindingController.getCurIntent(); + final var curIntent = getInputMethodBindingController(mCurrentUserId).getCurIntent(); if (curIntent != null && InputMethodUtils.checkIfPackageBelongsToUid( mPackageManagerInternal, uid, curIntent.getComponent().getPackageName())) { return true; @@ -4213,8 +4220,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. mStylusIds.add(deviceId); // a new Stylus is detected. If IME supports handwriting, and we don't have // handwriting initialized, lets do it now. - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(mCurrentUserId); if (!mHwController.getCurrentRequestId().isPresent() && bindingController.supportsStylusHandwriting()) { scheduleResetStylusHandwriting(); @@ -4395,8 +4401,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private void dumpDebug(ProtoOutputStream proto, long fieldId) { synchronized (ImfLock.class) { - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(mCurrentUserId); final long token = proto.start(fieldId); proto.write(CUR_METHOD_ID, getSelectedMethodIdLocked()); proto.write(CUR_SEQ, bindingController.getSequenceNumber()); @@ -4786,8 +4791,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. case MSG_RESET_HANDWRITING: { synchronized (ImfLock.class) { - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(mCurrentUserId); if (bindingController.supportsStylusHandwriting() && getCurMethodLocked() != null && hasSupportedStylusLocked()) { Slog.d(TAG, "Initializing Handwriting Spy"); @@ -4813,8 +4817,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (curMethod == null || mImeBindingState.mFocusedWindow == null) { return true; } - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(mCurrentUserId); final HandwritingModeController.HandwritingSession session = mHwController.startHandwritingSession( msg.arg1 /*requestId*/, @@ -4870,8 +4873,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return; } // TODO(b/325515685): user data must be retrieved by a userId parameter - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(mCurrentUserId); if (mImePlatformCompatUtils.shouldUseSetInteractiveProtocol( bindingController.getCurMethodUid())) { // Handle IME visibility when interactive changed before finishing the input to @@ -5096,8 +5098,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") void sendOnNavButtonFlagsChangedLocked() { - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(mCurrentUserId); final IInputMethodInvoker curMethod = bindingController.getCurMethod(); if (curMethod == null) { // No need to send the data if the IME is not yet bound. @@ -5584,8 +5585,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. public void onSessionForAccessibilityCreated(int accessibilityConnectionId, IAccessibilityInputMethodSession session, @UserIdInt int userId) { synchronized (ImfLock.class) { - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(mCurrentUserId); // TODO(b/305829876): Implement user ID verification if (mCurClient != null) { clearClientSessionForAccessibilityLocked(mCurClient, accessibilityConnectionId); @@ -5620,8 +5620,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. public void unbindAccessibilityFromCurrentClient(int accessibilityConnectionId, @UserIdInt int userId) { synchronized (ImfLock.class) { - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(mCurrentUserId); // TODO(b/305829876): Implement user ID verification if (mCurClient != null) { if (DEBUG) { @@ -5853,8 +5852,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. p.println(" pid=" + c.mPid); }; mClientController.forAllClients(clientControllerDump); - final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(mCurrentUserId); p.println(" mCurrentUserId=" + mCurrentUserId); p.println(" mCurMethodId=" + getSelectedMethodIdLocked()); client = mCurClient; @@ -6376,8 +6374,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (userId == mCurrentUserId) { hideCurrentInputLocked(mImeBindingState.mFocusedWindow, 0 /* flags */, SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND); - final var userData = mUserDataRepository.getOrCreate(userId); - final var bindingController = userData.mBindingController; + final var bindingController = getInputMethodBindingController(userId); bindingController.unbindCurrentMethod(); // Enable default IMEs, disable others diff --git a/services/core/java/com/android/server/inputmethod/MultiUserUtils.java b/services/core/java/com/android/server/inputmethod/MultiUserUtils.java new file mode 100644 index 000000000000..8e188daa4209 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/MultiUserUtils.java @@ -0,0 +1,55 @@ +/* + * 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.inputmethod; + +import android.annotation.AnyThread; +import android.annotation.NonNull; +import android.annotation.UserIdInt; + +import com.android.server.pm.UserManagerInternal; + +final class MultiUserUtils { + /** + * Not intended to be instantiated. + */ + private MultiUserUtils() { + } + + /** + * Return the first user ID (a user has {@link android.content.pm.UserInfo#FLAG_MAIN} if + * available). Otherwise, return the given default value. + * + * @param userManagerInternal {@link UserManagerInternal} to be used to query about users + * @param defaultValue a user ID that will be returned when there is no main user + * @return The first main user ID + */ + @AnyThread + @UserIdInt + static int getFirstMainUserIdOrDefault(@NonNull UserManagerInternal userManagerInternal, + @UserIdInt int defaultValue) { + final int[] userIds = userManagerInternal.getUserIds(); + if (userIds != null) { + for (int userId : userIds) { + final var userInfo = userManagerInternal.getUserInfo(userId); + if (userInfo.isMain()) { + return userId; + } + } + } + return defaultValue; + } +} diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index dbdb155eb2e3..b14702dc6647 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -737,12 +737,13 @@ public class LockSettingsService extends ILockSettings.Stub { !mUserManager.isQuietModeEnabled(userHandle)) { // Only show notifications for managed profiles once their parent // user is unlocked. - showEncryptionNotificationForProfile(userHandle, reason); + showEncryptionNotificationForProfile(userHandle, parent.getUserHandle(), reason); } } } - private void showEncryptionNotificationForProfile(UserHandle user, String reason) { + private void showEncryptionNotificationForProfile(UserHandle user, UserHandle parent, + String reason) { CharSequence title = getEncryptionNotificationTitle(); CharSequence message = getEncryptionNotificationMessage(); CharSequence detail = getEncryptionNotificationDetail(); @@ -759,8 +760,15 @@ public class LockSettingsService extends ILockSettings.Stub { unlockIntent.setFlags( Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - PendingIntent intent = PendingIntent.getActivity(mContext, 0, unlockIntent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED); + PendingIntent intent; + if (android.app.admin.flags.Flags.hsumUnlockNotificationFix()) { + intent = PendingIntent.getActivityAsUser(mContext, 0, unlockIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED, + null, parent); + } else { + intent = PendingIntent.getActivity(mContext, 0, unlockIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED); + } Slogf.d(TAG, "Showing encryption notification for user %d; reason: %s", user.getIdentifier(), reason); diff --git a/services/core/java/com/android/server/media/MediaShellCommand.java b/services/core/java/com/android/server/media/MediaShellCommand.java index a20de3198d2c..bea71dc5cedb 100644 --- a/services/core/java/com/android/server/media/MediaShellCommand.java +++ b/services/core/java/com/android/server/media/MediaShellCommand.java @@ -16,6 +16,7 @@ package com.android.server.media; +import android.annotation.NonNull; import android.app.ActivityThread; import android.content.Context; import android.media.MediaMetadata; @@ -247,7 +248,7 @@ public class MediaShellCommand extends ShellCommand { } @Override - public void onAudioInfoChanged(MediaController.PlaybackInfo info) { + public void onAudioInfoChanged(@NonNull MediaController.PlaybackInfo info) { mWriter.println("onAudioInfoChanged " + info); } } diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java index 71a7d0d2a638..f07b7106c0d4 100644 --- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java +++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java @@ -17,6 +17,7 @@ package com.android.server.os; import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled; +import static android.app.admin.flags.Flags.onboardingConsentlessBugreports; import android.Manifest; import android.annotation.NonNull; @@ -31,6 +32,7 @@ import android.content.pm.UserInfo; import android.os.Binder; import android.os.BugreportManager.BugreportCallback; import android.os.BugreportParams; +import android.os.Build; import android.os.Environment; import android.os.IDumpstate; import android.os.IDumpstateListener; @@ -69,12 +71,14 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.OptionalInt; import java.util.Set; +import java.util.concurrent.TimeUnit; /** * Implementation of the service that provides a privileged API to capture and consume bugreports. @@ -98,6 +102,9 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { private static final String BUGREPORT_SERVICE = "bugreportd"; private static final long DEFAULT_BUGREPORT_SERVICE_TIMEOUT_MILLIS = 30 * 1000; + private static final long DEFAULT_BUGREPORT_CONSENTLESS_GRACE_PERIOD_MILLIS = + TimeUnit.MINUTES.toMillis(2); + private final Object mLock = new Object(); private final Injector mInjector; private final Context mContext; @@ -132,6 +139,10 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { private ArrayMap<Pair<Integer, String>, ArraySet<String>> mBugreportFiles = new ArrayMap<>(); + // Map of <CallerPackage, Pair<TimestampOfLastConsent, skipConsentForFullReport>> + @GuardedBy("mLock") + private Map<String, Pair<Long, Boolean>> mConsentGranted = new HashMap<>(); + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) @GuardedBy("mLock") final Set<String> mBugreportFilesToPersist = new HashSet<>(); @@ -238,6 +249,64 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { } } + /** + * Logs an entry with a timestamp of a consent being granted by the user to the calling + * {@code packageName}. + */ + void logConsentGrantedForCaller( + String packageName, boolean consentGranted, boolean isDeferredReport) { + if (!onboardingConsentlessBugreports() || !Build.IS_DEBUGGABLE) { + return; + } + synchronized (mLock) { + // Adds an entry with the timestamp of the consent being granted by the user, and + // whether the consent can be skipped for a full bugreport, because a single + // consent can be used for multiple deferred reports but only one full report. + if (consentGranted) { + mConsentGranted.put(packageName, new Pair<>( + System.currentTimeMillis(), + isDeferredReport)); + } else if (!isDeferredReport) { + if (!mConsentGranted.containsKey(packageName)) { + Slog.e(TAG, "Previous consent from package: " + packageName + " should" + + "have been logged."); + return; + } + mConsentGranted.put(packageName, new Pair<>( + mConsentGranted.get(packageName).first, + /* second = */ false + )); + } + } + } + + /** + * Returns {@code true} if user consent be skippeb because a previous consens has been + * granted to the caller within the allowed time period. + */ + boolean canSkipConsentScreen(String packageName, boolean isFullReport) { + if (!onboardingConsentlessBugreports() || !Build.IS_DEBUGGABLE) { + return false; + } + synchronized (mLock) { + if (!mConsentGranted.containsKey(packageName)) { + return false; + } + long currentTime = System.currentTimeMillis(); + long consentGrantedTime = mConsentGranted.get(packageName).first; + if (consentGrantedTime + DEFAULT_BUGREPORT_CONSENTLESS_GRACE_PERIOD_MILLIS + < currentTime) { + mConsentGranted.remove(packageName); + return false; + } + boolean skipConsentForFullReport = mConsentGranted.get(packageName).second; + if (isFullReport && !skipConsentForFullReport) { + return false; + } + return true; + } + } + private void addBugreportMapping(Pair<Integer, String> caller, String bugreportFile) { synchronized (mLock) { if (!mBugreportFiles.containsKey(caller)) { @@ -418,7 +487,7 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { public void startBugreport(int callingUidUnused, String callingPackage, FileDescriptor bugreportFd, FileDescriptor screenshotFd, int bugreportMode, int bugreportFlags, IDumpstateListener listener, - boolean isScreenshotRequested) { + boolean isScreenshotRequested, boolean skipUserConsentUnused) { Objects.requireNonNull(callingPackage); Objects.requireNonNull(bugreportFd); Objects.requireNonNull(listener); @@ -509,7 +578,8 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { @RequiresPermission(value = Manifest.permission.DUMP, conditional = true) public void retrieveBugreport(int callingUidUnused, String callingPackage, int userId, FileDescriptor bugreportFd, String bugreportFile, - boolean keepBugreportOnRetrievalUnused, IDumpstateListener listener) { + boolean keepBugreportOnRetrievalUnused, boolean skipUserConsentUnused, + IDumpstateListener listener) { int callingUid = Binder.getCallingUid(); enforcePermission(callingPackage, callingUid, false); @@ -540,9 +610,13 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { return; } + boolean skipUserConsent = mBugreportFileManager.canSkipConsentScreen( + callingPackage, /* isFullReport = */ false); + // Wrap the listener so we can intercept binder events directly. DumpstateListener myListener = new DumpstateListener(listener, ds, - new Pair<>(callingUid, callingPackage), /* reportFinishedFile= */ true); + new Pair<>(callingUid, callingPackage), /* reportFinishedFile= */ true, + !skipUserConsent, /* isDeferredReport = */ true); boolean keepBugreportOnRetrieval = false; if (onboardingBugreportV2Enabled()) { @@ -553,7 +627,7 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { setCurrentDumpstateListenerLocked(myListener); try { ds.retrieveBugreport(callingUid, callingPackage, userId, bugreportFd, - bugreportFile, keepBugreportOnRetrieval, myListener); + bugreportFile, keepBugreportOnRetrieval, skipUserConsent, myListener); } catch (RemoteException e) { Slog.e(TAG, "RemoteException in retrieveBugreport", e); } @@ -754,7 +828,7 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { } } - boolean reportFinishedFile = + boolean isDeferredConsentReport = (bugreportFlags & BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT) != 0; boolean keepBugreportOnRetrieval = @@ -766,14 +840,17 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { reportError(listener, IDumpstateListener.BUGREPORT_ERROR_RUNTIME_ERROR); return; } - + boolean skipUserConsent = mBugreportFileManager.canSkipConsentScreen( + callingPackage, !isDeferredConsentReport); DumpstateListener myListener = new DumpstateListener(listener, ds, - new Pair<>(callingUid, callingPackage), reportFinishedFile, - keepBugreportOnRetrieval); + new Pair<>(callingUid, callingPackage), + /* reportFinishedFile = */ isDeferredConsentReport, keepBugreportOnRetrieval, + !isDeferredConsentReport && !skipUserConsent, + isDeferredConsentReport); setCurrentDumpstateListenerLocked(myListener); try { ds.startBugreport(callingUid, callingPackage, bugreportFd, screenshotFd, bugreportMode, - bugreportFlags, myListener, isScreenshotRequested); + bugreportFlags, myListener, isScreenshotRequested, skipUserConsent); } catch (RemoteException e) { // dumpstate service is already started now. We need to kill it to manage the // lifecycle correctly. If we don't subsequent callers will get @@ -930,14 +1007,21 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { private boolean mDone; private boolean mKeepBugreportOnRetrieval; + private boolean mConsentGranted; + + private boolean mIsDeferredReport; + DumpstateListener(IDumpstateListener listener, IDumpstate ds, - Pair<Integer, String> caller, boolean reportFinishedFile) { - this(listener, ds, caller, reportFinishedFile, /* keepBugreportOnRetrieval= */ false); + Pair<Integer, String> caller, boolean reportFinishedFile, + boolean consentGranted, boolean isDeferredReport) { + this(listener, ds, caller, reportFinishedFile, /* keepBugreportOnRetrieval= */ false, + consentGranted, isDeferredReport); } DumpstateListener(IDumpstateListener listener, IDumpstate ds, Pair<Integer, String> caller, boolean reportFinishedFile, - boolean keepBugreportOnRetrieval) { + boolean keepBugreportOnRetrieval, boolean consentGranted, + boolean isDeferredReport) { if (DEBUG) { Slogf.d(TAG, "Starting DumpstateListener(id=%d) for caller %s", mId, caller); } @@ -946,6 +1030,8 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { mCaller = caller; mReportFinishedFile = reportFinishedFile; mKeepBugreportOnRetrieval = keepBugreportOnRetrieval; + mConsentGranted = consentGranted; + mIsDeferredReport = isDeferredReport; try { mDs.asBinder().linkToDeath(this, 0); } catch (RemoteException e) { @@ -985,6 +1071,8 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { } else if (DEBUG) { Slog.d(TAG, "Not reporting finished file"); } + mBugreportFileManager.logConsentGrantedForCaller( + mCaller.second, mConsentGranted, mIsDeferredReport); mListener.onFinished(bugreportFile); } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 472f228b689f..6cfa09f9adf1 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -511,14 +511,13 @@ final class InstallPackageHelper { // metadata file on the system image. Do not reset the path and source if this is the // case. if (pkgSetting.getAppMetadataFilePath() == null) { - File dir = new File(pkg.getPath()); + String dir = pkg.getPath(); if (pkgSetting.isSystem()) { - dir = new File(Environment.getDataDirectory(), - "app-metadata/" + pkg.getPackageName()); + dir = Environment.getDataDirectoryPath() + "/app-metadata/" + pkg.getPackageName(); } - File appMetadataFile = new File(dir, APP_METADATA_FILE_NAME); - if (appMetadataFile.exists()) { - pkgSetting.setAppMetadataFilePath(appMetadataFile.getAbsolutePath()); + String appMetadataFilePath = dir + "/" + APP_METADATA_FILE_NAME; + if (request.hasAppMetadataFile()) { + pkgSetting.setAppMetadataFilePath(appMetadataFilePath); if (Flags.aslInApkAppMetadataSource()) { pkgSetting.setAppMetadataSource(APP_METADATA_SOURCE_INSTALLER); } @@ -526,7 +525,7 @@ final class InstallPackageHelper { Map<String, PackageManager.Property> properties = pkg.getProperties(); if (properties.containsKey(PROPERTY_ANDROID_SAFETY_LABEL)) { // ASL file extraction is done in post-install - pkgSetting.setAppMetadataFilePath(appMetadataFile.getAbsolutePath()); + pkgSetting.setAppMetadataFilePath(appMetadataFilePath); pkgSetting.setAppMetadataSource(APP_METADATA_SOURCE_APK); } } diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java index 6d385174a8a1..8f51e3696108 100644 --- a/services/core/java/com/android/server/pm/InstallRequest.java +++ b/services/core/java/com/android/server/pm/InstallRequest.java @@ -167,6 +167,8 @@ final class InstallRequest { private int mInstallerUidForInstallExisting = INVALID_UID; + private final boolean mHasAppMetadataFileFromInstaller; + // New install InstallRequest(InstallingSession params) { mUserId = params.getUser().getIdentifier(); @@ -185,6 +187,7 @@ final class InstallRequest { mSessionId = params.mSessionId; mRequireUserAction = params.mRequireUserAction; mPreVerifiedDomains = params.mPreVerifiedDomains; + mHasAppMetadataFileFromInstaller = params.mHasAppMetadataFile; } // Install existing package as user @@ -203,6 +206,7 @@ final class InstallRequest { mAppId = appId; mInstallerUidForInstallExisting = installerUid; mSystem = isSystem; + mHasAppMetadataFileFromInstaller = false; } // addForInit @@ -224,6 +228,7 @@ final class InstallRequest { mSessionId = -1; mRequireUserAction = USER_ACTION_UNSPECIFIED; mDisabledPs = disabledPs; + mHasAppMetadataFileFromInstaller = false; } @Nullable @@ -371,6 +376,10 @@ final class InstallRequest { return PackageInstallerSession.isArchivedInstallation(getInstallFlags()); } + public boolean hasAppMetadataFile() { + return mHasAppMetadataFileFromInstaller; + } + @Nullable public String getRemovedPackage() { return mRemovedInfo != null ? mRemovedInfo.mRemovedPackage : null; diff --git a/services/core/java/com/android/server/pm/InstallingSession.java b/services/core/java/com/android/server/pm/InstallingSession.java index 4cbd3ad45dc7..b06c7cb4ac33 100644 --- a/services/core/java/com/android/server/pm/InstallingSession.java +++ b/services/core/java/com/android/server/pm/InstallingSession.java @@ -101,6 +101,7 @@ class InstallingSession { final boolean mApplicationEnabledSettingPersistent; @Nullable final DomainSet mPreVerifiedDomains; + final boolean mHasAppMetadataFile; // For move install InstallingSession(OriginInfo originInfo, MoveInfo moveInfo, IPackageInstallObserver2 observer, @@ -134,12 +135,14 @@ class InstallingSession { mRequireUserAction = USER_ACTION_UNSPECIFIED; mApplicationEnabledSettingPersistent = false; mPreVerifiedDomains = null; + mHasAppMetadataFile = false; } InstallingSession(int sessionId, File stagedDir, IPackageInstallObserver2 observer, PackageInstaller.SessionParams sessionParams, InstallSource installSource, UserHandle user, SigningDetails signingDetails, int installerUid, - PackageLite packageLite, DomainSet preVerifiedDomains, PackageManagerService pm) { + PackageLite packageLite, DomainSet preVerifiedDomains, PackageManagerService pm, + boolean hasAppMetadatafile) { mPm = pm; mUser = user; mOriginInfo = OriginInfo.fromStagedFile(stagedDir); @@ -168,6 +171,7 @@ class InstallingSession { mRequireUserAction = sessionParams.requireUserAction; mApplicationEnabledSettingPersistent = sessionParams.applicationEnabledSettingPersistent; mPreVerifiedDomains = preVerifiedDomains; + mHasAppMetadataFile = hasAppMetadatafile; } @Override diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 80a5f3a4c579..57f6d2789dc5 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -601,6 +601,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private String mSessionErrorMessage; + @GuardedBy("mLock") + private boolean mHasAppMetadataFile = false; + @Nullable final StagedSession mStagedSession; @@ -1814,7 +1817,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { assertCallerIsOwnerOrRoot(); synchronized (mLock) { assertPreparedAndNotCommittedOrDestroyedLocked("getAppMetadataFd"); - if (!getStagedAppMetadataFile().exists()) { + if (!mHasAppMetadataFile) { return null; } try { @@ -1827,9 +1830,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @Override public void removeAppMetadata() { - File file = getStagedAppMetadataFile(); - if (file.exists()) { - file.delete(); + synchronized (mLock) { + if (mHasAppMetadataFile) { + getStagedAppMetadataFile().delete(); + mHasAppMetadataFile = false; + } } } @@ -1850,8 +1855,12 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { assertPreparedAndNotSealedLocked("openWriteAppMetadata"); } try { - return doWriteInternal(APP_METADATA_FILE_NAME, /* offsetBytes= */ 0, + ParcelFileDescriptor fd = doWriteInternal(APP_METADATA_FILE_NAME, /* offsetBytes= */ 0, /* lengthBytes= */ -1, null); + synchronized (mLock) { + mHasAppMetadataFile = true; + } + return fd; } catch (IOException e) { throw ExceptionUtils.wrap(e); } @@ -2145,18 +2154,21 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } - File appMetadataFile = getStagedAppMetadataFile(); - if (appMetadataFile.exists()) { - long sizeLimit = getAppMetadataSizeLimit(); - if (appMetadataFile.length() > sizeLimit) { - appMetadataFile.delete(); - throw new IllegalArgumentException( - "App metadata size exceeds the maximum allowed limit of " + sizeLimit); - } - if (isIncrementalInstallation()) { - // Incremental requires stageDir to be empty so move the app metadata file to a - // temporary location and move back after commit. - appMetadataFile.renameTo(getTmpAppMetadataFile()); + synchronized (mLock) { + if (mHasAppMetadataFile) { + File appMetadataFile = getStagedAppMetadataFile(); + long sizeLimit = getAppMetadataSizeLimit(); + if (appMetadataFile.length() > sizeLimit) { + appMetadataFile.delete(); + mHasAppMetadataFile = false; + throw new IllegalArgumentException( + "App metadata size exceeds the maximum allowed limit of " + sizeLimit); + } + if (isIncrementalInstallation()) { + // Incremental requires stageDir to be empty so move the app metadata file to a + // temporary location and move back after commit. + appMetadataFile.renameTo(getTmpAppMetadataFile()); + } } } @@ -3207,7 +3219,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { synchronized (mLock) { return new InstallingSession(sessionId, stageDir, localObserver, params, mInstallSource, - user, mSigningDetails, mInstallerUid, mPackageLite, mPreVerifiedDomains, mPm); + user, mSigningDetails, mInstallerUid, mPackageLite, mPreVerifiedDomains, mPm, + mHasAppMetadataFile); } } @@ -3445,9 +3458,14 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + if (mHasAppMetadataFile && !getStagedAppMetadataFile().exists()) { + throw new PackageManagerException(INSTALL_FAILED_VERIFICATION_FAILURE, + "App metadata file expected but not found in " + stageDir.getAbsolutePath()); + } + final List<ApkLite> addedFiles = getAddedApkLitesLocked(); if (addedFiles.isEmpty() - && (removeSplitList.size() == 0 || getStagedAppMetadataFile().exists())) { + && (removeSplitList.size() == 0 || mHasAppMetadataFile)) { throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, TextUtils.formatSimple("Session: %d. No packages staged in %s", sessionId, stageDir.getAbsolutePath())); diff --git a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java index ad2c3e83b041..3579246b660f 100644 --- a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java +++ b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java @@ -225,7 +225,7 @@ final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { @NonNull TimeConfiguration requestedConfiguration, boolean bypassUserPolicyChecks) { Objects.requireNonNull(requestedConfiguration); - TimeCapabilitiesAndConfig capabilitiesAndConfig = getCurrentUserConfigurationInternal() + TimeCapabilitiesAndConfig capabilitiesAndConfig = getConfigurationInternal(userId) .createCapabilitiesAndConfig(bypassUserPolicyChecks); TimeCapabilities capabilities = capabilitiesAndConfig.getCapabilities(); TimeConfiguration oldConfiguration = capabilitiesAndConfig.getConfiguration(); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java index dd3d512e471c..80f1125a4ecf 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java @@ -150,7 +150,7 @@ public class WallpaperCropper { Rect landscapeCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl); landscapeCrop = noParallax(landscapeCrop, rotatedDisplaySize, bitmapSize, rtl); // compute the crop on portrait at the center of the landscape crop - crop = getAdjustedCrop(landscapeCrop, bitmapSize, displaySize, false, rtl, ADD); + crop = getAdjustedCrop(landscapeCrop, bitmapSize, displaySize, false, ADD); // add some parallax (until the border of the landscape crop without parallax) if (rtl) { @@ -160,7 +160,7 @@ public class WallpaperCropper { } } - return getAdjustedCrop(crop, bitmapSize, displaySize, true, rtl, ADD); + return getAdjustedCrop(crop, bitmapSize, displaySize, true, ADD); } // If any suggested crop is invalid, fallback to case 1 @@ -176,7 +176,7 @@ public class WallpaperCropper { // Case 2: if the orientation exists in the suggested crops, adjust the suggested crop Rect suggestedCrop = suggestedCrops.get(orientation); if (suggestedCrop != null) { - return getAdjustedCrop(suggestedCrop, bitmapSize, displaySize, true, rtl, ADD); + return getAdjustedCrop(suggestedCrop, bitmapSize, displaySize, true, ADD); } // Case 3: if we have the 90° rotated orientation in the suggested crops, reuse it and @@ -188,7 +188,7 @@ public class WallpaperCropper { 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); + return getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, BALANCE); } // Case 4: if the device is a foldable, if we're looking for a folded orientation and have @@ -200,13 +200,13 @@ public class WallpaperCropper { // compute the visible part (without parallax) of the unfolded screen Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); // compute the folded crop, at the center of the crop of the unfolded screen - Rect res = getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, REMOVE); + Rect res = getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, REMOVE); // if we removed some width, add it back to add a parallax effect if (res.width() < adjustedCrop.width()) { if (rtl) res.left = Math.min(res.left, adjustedCrop.left); else res.right = Math.max(res.right, adjustedCrop.right); // use getAdjustedCrop(parallax=true) to make sure we don't exceed MAX_PARALLAX - res = getAdjustedCrop(res, bitmapSize, displaySize, true, rtl, ADD); + res = getAdjustedCrop(res, bitmapSize, displaySize, true, ADD); } return res; } @@ -220,7 +220,7 @@ public class WallpaperCropper { 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); + return getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, ADD); } // Case 6: for a foldable device, try to combine case 3 + case 4 or 5: @@ -255,7 +255,7 @@ public class WallpaperCropper { @VisibleForTesting 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); + Rect adjustedCrop = getAdjustedCrop(crop, bitmapSize, displaySize, true, ADD); // only keep the visible part (without parallax) float suggestedDisplayRatio = 1f * displaySize.x / displaySize.y; int widthToRemove = (int) (adjustedCrop.width() @@ -272,7 +272,7 @@ public class WallpaperCropper { * 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 layout) if necessary. + * by removing content from both sides if necessary. * <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>Make sure the crop fills the screen, i.e. that the width/height ratio of the crop @@ -282,7 +282,7 @@ public class WallpaperCropper { */ @VisibleForTesting static Rect getAdjustedCrop(Rect crop, Point bitmapSize, Point screenSize, - boolean parallax, boolean rtl, int mode) { + boolean parallax, int mode) { Rect adjustedCrop = new Rect(crop); float cropRatio = ((float) crop.width()) / crop.height(); float screenRatio = ((float) screenSize.x) / screenSize.y; @@ -297,8 +297,7 @@ public class WallpaperCropper { 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); + Rect rect = getAdjustedCrop(rotatedCrop, rotatedBitmap, rotatedScreen, false, mode); int resultLeft = rect.top; int resultRight = resultLeft + rect.height(); int resultTop = rotatedBitmap.x - rect.right; @@ -318,9 +317,8 @@ public class WallpaperCropper { // total surface of W * H. In other words it is the width to add to get the desired // aspect ratio R, while preserving the total number of pixels W * H. int widthToAdd = mode == REMOVE ? 0 - : mode == ADD ? (int) (0.5 + crop.height() * screenRatio - crop.width()) - : (int) (0.5 - crop.width() - + Math.sqrt(crop.width() * crop.height() * screenRatio)); + : mode == ADD ? (int) (crop.height() * screenRatio - crop.width()) + : (int) (-crop.width() + Math.sqrt(crop.width() * crop.height() * screenRatio)); int availableWidth = bitmapSize.x - crop.width(); if (availableWidth >= widthToAdd) { int widthToAddLeft = widthToAdd / 2; diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index d053bbb28ad1..2f6e07c24d67 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -660,6 +660,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A private final TaskFragment.ConfigOverrideHint mResolveConfigHint; + private final boolean mOptOutEdgeToEdge; + private static ConstrainDisplayApisConfig sConstrainDisplayApisConfig; boolean pendingVoiceInteractionStart; // Waiting for activity-invoked voice session @@ -2179,9 +2181,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A || ent.array.getBoolean(R.styleable.Window_windowShowWallpaper, false); mStyleFillsParent = mOccludesParent; noDisplay = ent.array.getBoolean(R.styleable.Window_windowNoDisplay, false); + mOptOutEdgeToEdge = ent.array.getBoolean( + R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false); } else { mStyleFillsParent = mOccludesParent = true; noDisplay = false; + mOptOutEdgeToEdge = false; } if (options != null) { @@ -8710,9 +8715,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (rotation == ROTATION_UNDEFINED && !isFixedRotationTransforming()) { rotation = mDisplayContent.getRotation(); } - if (!mResolveConfigHint.mUseOverrideInsetsForConfig - || getCompatDisplayInsets() != null || shouldCreateCompatDisplayInsets() - || isFloating(parentWindowingMode) || rotation == ROTATION_UNDEFINED) { + if (!mOptOutEdgeToEdge && (!mResolveConfigHint.mUseOverrideInsetsForConfig + || getCompatDisplayInsets() != null || isFloating(parentWindowingMode) + || rotation == ROTATION_UNDEFINED)) { // If the insets configuration decoupled logic is not enabled for the app, or the app // already has a compat override, or the context doesn't contain enough info to // calculate the override, skip the override. diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 62931bb3fbcb..f7910b08b1e2 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -20,6 +20,7 @@ import static android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND; import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; import static android.app.ActivityOptions.BackgroundActivityStartMode; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; @@ -105,6 +106,7 @@ public class BackgroundActivityStartController { static final String AUTO_OPT_IN_NOT_PENDING_INTENT = "notPendingIntent"; static final String AUTO_OPT_IN_CALL_FOR_RESULT = "callForResult"; static final String AUTO_OPT_IN_SAME_UID = "sameUid"; + static final String AUTO_OPT_IN_COMPAT = "compatibility"; /** If enabled the creator will not allow BAL on its behalf by default. */ @ChangeId @@ -303,6 +305,10 @@ public class BackgroundActivityStartController { } else if (callingUid == realCallingUid && !balRequireOptInSameUid()) { mAutoOptInReason = AUTO_OPT_IN_SAME_UID; mAutoOptInCaller = false; + } else if (realCallerBackgroundActivityStartMode + == MODE_BACKGROUND_ACTIVITY_START_COMPAT) { + mAutoOptInReason = AUTO_OPT_IN_COMPAT; + mAutoOptInCaller = false; } else { mAutoOptInReason = null; mAutoOptInCaller = false; diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java index 42ca7b44287e..16fcb097ca5c 100644 --- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java +++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java @@ -348,6 +348,9 @@ class SnapshotPersistQueue { + bitmap.isMutable() + ") to (config=ARGB_8888, isMutable=false) failed."); return false; } + final int width = bitmap.getWidth(); + final int height = bitmap.getHeight(); + bitmap.recycle(); final File file = mPersistInfoProvider.getHighResolutionBitmapFile(mId, mUserId); try { @@ -365,8 +368,8 @@ class SnapshotPersistQueue { } final Bitmap lowResBitmap = Bitmap.createScaledBitmap(swBitmap, - (int) (bitmap.getWidth() * mPersistInfoProvider.lowResScaleFactor()), - (int) (bitmap.getHeight() * mPersistInfoProvider.lowResScaleFactor()), + (int) (width * mPersistInfoProvider.lowResScaleFactor()), + (int) (height * mPersistInfoProvider.lowResScaleFactor()), true /* filter */); swBitmap.recycle(); diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index ce53290da49c..2dc439da992d 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -492,6 +492,27 @@ class TransitionController { return false; } + /** Returns {@code true} if the display contains a transient-launch transition. */ + boolean hasTransientLaunch(@NonNull DisplayContent dc) { + if (mCollectingTransition != null && mCollectingTransition.hasTransientLaunch() + && mCollectingTransition.isOnDisplay(dc)) { + return true; + } + for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) { + final Transition transition = mWaitingTransitions.get(i); + if (transition.hasTransientLaunch() && transition.isOnDisplay(dc)) { + return true; + } + } + for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { + final Transition transition = mPlayingTransitions.get(i); + if (transition.hasTransientLaunch() && transition.isOnDisplay(dc)) { + return true; + } + } + return false; + } + boolean isTransientHide(@NonNull Task task) { if (mCollectingTransition != null && mCollectingTransition.isInTransientHide(task)) { return true; diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 65e17615f775..3e43f5a2da66 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -165,7 +165,7 @@ class WallpaperController { || (w.mActivityRecord != null && !w.mActivityRecord.fillsParent()); } } else if (w.hasWallpaper() && mService.mPolicy.isKeyguardHostWindow(w.mAttrs) - && w.mTransitionController.isTransitionOnDisplay(mDisplayContent)) { + && w.mTransitionController.hasTransientLaunch(mDisplayContent)) { // If we have no candidates at all, notification shade is allowed to be the target // of last resort even if it has not been made visible yet. if (DEBUG_WALLPAPER) Slog.v(TAG, "Found keyguard as wallpaper target: " + w); diff --git a/services/core/jni/com_android_server_display_DisplayControl.cpp b/services/core/jni/com_android_server_display_DisplayControl.cpp index 22c0f730ad7d..6613a250bd71 100644 --- a/services/core/jni/com_android_server_display_DisplayControl.cpp +++ b/services/core/jni/com_android_server_display_DisplayControl.cpp @@ -23,20 +23,22 @@ namespace android { -static jobject nativeCreateDisplay(JNIEnv* env, jclass clazz, jstring nameObj, jboolean secure, - jstring uniqueIdStr, jfloat requestedRefreshRate) { +static jobject nativeCreateVirtualDisplay(JNIEnv* env, jclass clazz, jstring nameObj, + jboolean secure, jstring uniqueIdStr, + jfloat requestedRefreshRate) { const ScopedUtfChars name(env, nameObj); const ScopedUtfChars uniqueId(env, uniqueIdStr); - sp<IBinder> token(SurfaceComposerClient::createDisplay(String8(name.c_str()), bool(secure), - std::string(uniqueId.c_str()), - requestedRefreshRate)); + sp<IBinder> token(SurfaceComposerClient::createVirtualDisplay(std::string(name.c_str()), + bool(secure), + std::string(uniqueId.c_str()), + requestedRefreshRate)); return javaObjectForIBinder(env, token); } -static void nativeDestroyDisplay(JNIEnv* env, jclass clazz, jobject tokenObj) { +static void nativeDestroyVirtualDisplay(JNIEnv* env, jclass clazz, jobject tokenObj) { sp<IBinder> token(ibinderForJavaObject(env, tokenObj)); if (token == NULL) return; - SurfaceComposerClient::destroyDisplay(token); + SurfaceComposerClient::destroyVirtualDisplay(token); } static void nativeOverrideHdrTypes(JNIEnv* env, jclass clazz, jobject tokenObject, @@ -180,10 +182,10 @@ static jobject nativeGetPhysicalDisplayToken(JNIEnv* env, jclass clazz, jlong ph static const JNINativeMethod sDisplayMethods[] = { // clang-format off - {"nativeCreateDisplay", "(Ljava/lang/String;ZLjava/lang/String;F)Landroid/os/IBinder;", - (void*)nativeCreateDisplay }, - {"nativeDestroyDisplay", "(Landroid/os/IBinder;)V", - (void*)nativeDestroyDisplay }, + {"nativeCreateVirtualDisplay", "(Ljava/lang/String;ZLjava/lang/String;F)Landroid/os/IBinder;", + (void*)nativeCreateVirtualDisplay }, + {"nativeDestroyVirtualDisplay", "(Landroid/os/IBinder;)V", + (void*)nativeDestroyVirtualDisplay }, {"nativeOverrideHdrTypes", "(Landroid/os/IBinder;[I)V", (void*)nativeOverrideHdrTypes }, {"nativeGetPhysicalDisplayIds", "()[J", diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 4c746a97bafa..b19de189af5a 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -601,7 +601,7 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon // Original data: [{'inputPort1': '1'}, {'inputPort2': '2'}] // Received data: ['inputPort1', '1', 'inputPort2', '2'] // So we unpack accordingly here. - outConfig->portAssociations.clear(); + outConfig->inputPortToDisplayPortAssociations.clear(); jobjectArray portAssociations = jobjectArray(env->CallObjectMethod(mServiceObj, gServiceClassInfo.getInputPortAssociations)); if (!checkAndClearExceptionFromCallback(env, "getInputPortAssociations") && portAssociations) { @@ -618,16 +618,16 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon displayPortStr.c_str()); continue; } - outConfig->portAssociations.insert({inputPort, displayPort}); + outConfig->inputPortToDisplayPortAssociations.insert({inputPort, displayPort}); } env->DeleteLocalRef(portAssociations); } - outConfig->uniqueIdAssociationsByPort = readMapFromInterleavedJavaArray< + outConfig->inputPortToDisplayUniqueIdAssociations = readMapFromInterleavedJavaArray< std::string>(gServiceClassInfo.getInputUniqueIdAssociationsByPort, "getInputUniqueIdAssociationsByPort"); - outConfig->uniqueIdAssociationsByDescriptor = readMapFromInterleavedJavaArray< + outConfig->inputDeviceDescriptorToDisplayUniqueIdAssociations = readMapFromInterleavedJavaArray< std::string>(gServiceClassInfo.getInputUniqueIdAssociationsByDescriptor, "getInputUniqueIdAssociationsByDescriptor"); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java index 3b25cb13e66c..5f395c567d1f 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java @@ -37,6 +37,7 @@ import static org.mockito.Mockito.when; import android.app.ActivityManagerInternal; import android.content.Context; import android.content.pm.PackageManagerInternal; +import android.content.pm.UserInfo; import android.content.res.Configuration; import android.hardware.input.IInputManager; import android.hardware.input.InputManagerGlobal; @@ -207,6 +208,16 @@ public class InputMethodManagerServiceTestBase { when(mMockUserManagerInternal.getProfileIds(anyInt(), anyBoolean())) .thenReturn(new int[] {0}); when(mMockUserManagerInternal.getUserIds()).thenReturn(new int[] {0}); + when(mMockUserManagerInternal.getUserInfo(anyInt())).thenAnswer(invocation -> { + final int userId = invocation.getArgument(0); + if (userId == 0) { + new UserInfo(userId, "main", + UserInfo.FLAG_PRIMARY | UserInfo.FLAG_MAIN | UserInfo.FLAG_SYSTEM); + } + // TODO(b/315348827): Update mock for multi-user scenarios. + throw new UnsupportedOperationException( + "Please mock #getUserInfo for userId=" + userId); + }); when(mMockActivityManagerInternal.isSystemReady()).thenReturn(true); when(mMockActivityManagerInternal.getCurrentUserId()).thenReturn(mCallingUserId); when(mMockPackageManagerInternal.getPackageUid(anyString(), anyLong(), anyInt())) diff --git a/services/tests/PackageManagerServiceTests/server/Android.bp b/services/tests/PackageManagerServiceTests/server/Android.bp index ea7bb8b4a1d1..a738acb299c1 100644 --- a/services/tests/PackageManagerServiceTests/server/Android.bp +++ b/services/tests/PackageManagerServiceTests/server/Android.bp @@ -105,6 +105,7 @@ android_test { ":PackageParserTestApp5", ":PackageParserTestApp6", ":PackageParserTestApp7", + ":PackageParserTestApp8", ], resource_zips: [":PackageManagerServiceServerTests_apks_as_resources"], diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java index a0e0e1ef36ee..5da202f109d4 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java @@ -101,6 +101,7 @@ import com.android.internal.pm.pkg.component.ParsedServiceImpl; import com.android.internal.pm.pkg.component.ParsedUsesPermission; import com.android.internal.pm.pkg.component.ParsedUsesPermissionImpl; import com.android.internal.pm.pkg.parsing.ParsingPackage; +import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; import com.android.internal.util.ArrayUtils; import com.android.server.pm.parsing.PackageCacher; import com.android.server.pm.parsing.PackageInfoUtils; @@ -126,6 +127,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -154,6 +156,7 @@ public class PackageParserTest { private static final String TEST_APP5_APK = "PackageParserTestApp5.apk"; private static final String TEST_APP6_APK = "PackageParserTestApp6.apk"; private static final String TEST_APP7_APK = "PackageParserTestApp7.apk"; + private static final String TEST_APP8_APK = "PackageParserTestApp8.apk"; private static final String PACKAGE_NAME = "com.android.servicestests.apps.packageparserapp"; @Before @@ -814,6 +817,39 @@ public class PackageParserTest { } } + @Test + @RequiresFlagsEnabled(android.content.res.Flags.FLAG_MANIFEST_FLAGGING) + public void testParseWithFeatureFlagAttributes() throws Exception { + final File testFile = extractFile(TEST_APP8_APK); + try (PackageParser2 parser = new TestPackageParser2()) { + Map<String, Boolean> flagValues = new HashMap<>(); + flagValues.put("my.flag1", true); + flagValues.put("my.flag2", false); + flagValues.put("my.flag3", false); + flagValues.put("my.flag4", true); + ParsingPackageUtils.getAconfigFlags().addFlagValuesForTesting(flagValues); + + // The manifest has: + // <permission android:name="PERM1" android:featureFlag="my.flag1 " /> + // <permission android:name="PERM2" android:featureFlag=" !my.flag2" /> + // <permission android:name="PERM3" android:featureFlag="my.flag3" /> + // <permission android:name="PERM4" android:featureFlag="!my.flag4" /> + // <permission android:name="PERM5" android:featureFlag="unknown.flag" /> + // Therefore with the above flag values, only PERM1 and PERM2 should be present. + + final ParsedPackage pkg = parser.parsePackage(testFile, 0, false); + List<String> permissionNames = + pkg.getPermissions().stream().map(ParsedComponent::getName).toList(); + assertThat(permissionNames).contains(PACKAGE_NAME + ".PERM1"); + assertThat(permissionNames).contains(PACKAGE_NAME + ".PERM2"); + assertThat(permissionNames).doesNotContain(PACKAGE_NAME + ".PERM3"); + assertThat(permissionNames).doesNotContain(PACKAGE_NAME + ".PERM4"); + assertThat(permissionNames).doesNotContain(PACKAGE_NAME + ".PERM5"); + } finally { + testFile.delete(); + } + } + /** * A subclass of package parser that adds a "cache_" prefix to the package name for the cached * results. This is used by tests to tell if a ParsedPackage is generated from the cache or not. diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamServiceTest.java b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamServiceTest.java index 1322545c8d7e..b98af6bc7dd0 100644 --- a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamServiceTest.java +++ b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamServiceTest.java @@ -37,6 +37,7 @@ import android.service.dreams.DreamService; import android.service.dreams.Flags; import android.service.dreams.IDreamOverlayCallback; import android.testing.TestableLooper; +import android.view.KeyEvent; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; @@ -181,4 +182,15 @@ public class DreamServiceTest { environment.advance(TestDreamEnvironment.DREAM_STATE_WOKEN); verify(environment.getDreamOverlayClient()).onWakeRequested(); } + + @Test + @EnableFlags(Flags.FLAG_DREAM_HANDLES_CONFIRM_KEYS) + public void testPartialKeyHandling() throws Exception { + TestDreamEnvironment environment = new TestDreamEnvironment.Builder(mTestableLooper) + .build(); + environment.advance(TestDreamEnvironment.DREAM_STATE_STARTED); + + // Ensure service does not crash from only receiving up event. + environment.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SPACE)); + } } diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java b/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java index ef85ba56769e..3d03bf218557 100644 --- a/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java +++ b/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java @@ -46,6 +46,7 @@ import android.service.dreams.IDreamOverlayCallback; import android.service.dreams.IDreamOverlayClient; import android.service.dreams.IDreamService; import android.testing.TestableLooper; +import android.view.KeyEvent; import android.view.View; import android.view.Window; import android.view.WindowInsetsController; @@ -390,6 +391,13 @@ public class TestDreamEnvironment { } } + /** + * Sends a key event to the dream. + */ + public void dispatchKeyEvent(KeyEvent event) { + mService.dispatchKeyEvent(event); + } + private void wakeDream() throws RemoteException { mService.wakeUp(); } diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java index c359412b6ccd..cb15d6f84403 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java @@ -3094,13 +3094,14 @@ public final class AlarmManagerServiceTest { ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class); verify(alarmPi).send(eq(mMockContext), eq(0), any(Intent.class), any(), any(Handler.class), isNull(), bundleCaptor.capture()); + Bundle bundle = bundleCaptor.getValue(); if (idleOptions != null) { - assertEquals(idleOptions, bundleCaptor.getValue()); + assertEquals(idleOptions, bundle); } else { - assertFalse("BAL flag needs to be false in alarm manager", - bundleCaptor.getValue().getBoolean( - ActivityOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, - true)); + ActivityOptions options = ActivityOptions.fromBundle(bundle); + assertEquals("BAL should not be allowed in alarm manager", + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED, + options.getPendingIntentBackgroundActivityStartMode()); } } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java b/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java index 8e1e3392eb1c..c77ab0f303d9 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java @@ -24,7 +24,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import android.content.ContentResolver; import android.os.SystemProperties; import android.provider.Settings; -import android.provider.DeviceConfig.Properties; import android.text.TextUtils; import com.android.dx.mockito.inline.extended.ExtendedMockito; @@ -43,7 +42,6 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; /** * Test SettingsToPropertiesMapper. @@ -63,7 +61,6 @@ public class SettingsToPropertiesMapperTest { private HashMap<String, String> mSystemSettingsMap; private HashMap<String, String> mGlobalSettingsMap; - private HashMap<String, String> mConfigSettingsMap; @Before public void setUp() throws Exception { @@ -74,11 +71,9 @@ public class SettingsToPropertiesMapperTest { .spyStatic(SystemProperties.class) .spyStatic(Settings.Global.class) .spyStatic(SettingsToPropertiesMapper.class) - .spyStatic(Settings.Config.class) .startMocking(); mSystemSettingsMap = new HashMap<>(); mGlobalSettingsMap = new HashMap<>(); - mConfigSettingsMap = new HashMap<>(); // Mock SystemProperties setter and various getters doAnswer((Answer<Void>) invocationOnMock -> { @@ -106,21 +101,6 @@ public class SettingsToPropertiesMapperTest { } ).when(() -> Settings.Global.getString(any(), anyString())); - // Mock Settings.Config getstrings method - doAnswer((Answer<Map<String, String>>) invocationOnMock -> { - String namespace = invocationOnMock.getArgument(0); - List<String> flags = invocationOnMock.getArgument(1); - HashMap<String, String> values = new HashMap<>(); - for (String flag : flags) { - String value = mConfigSettingsMap.get(namespace + "/" + flag); - if (value != null) { - values.put(flag, value); - } - } - return values; - } - ).when(() -> Settings.Config.getStrings(anyString(), any())); - mTestMapper = new SettingsToPropertiesMapper( mMockContentResolver, TEST_MAPPING, new String[] {}, new String[] {}); } @@ -259,39 +239,4 @@ public class SettingsToPropertiesMapperTest { Assert.assertTrue(categories.contains("category2")); Assert.assertTrue(categories.contains("category3")); } - - @Test - public void testGetStagedFlagsWithValueChange() { - // mock up what is in the setting already - mConfigSettingsMap.put("namespace_1/flag_1", "true"); - mConfigSettingsMap.put("namespace_1/flag_2", "true"); - - // mock up input - String namespace = "staged"; - Map<String, String> keyValueMap = new HashMap<>(); - // case 1: existing prop, stage the same value - keyValueMap.put("namespace_1*flag_1", "true"); - // case 2: existing prop, stage a different value - keyValueMap.put("namespace_1*flag_2", "false"); - // case 3: new prop - keyValueMap.put("namespace_2*flag_1", "true"); - Properties props = new Properties(namespace, keyValueMap); - - HashMap<String, HashMap<String, String>> toStageProps = - SettingsToPropertiesMapper.getStagedFlagsWithValueChange(props); - - HashMap<String, String> namespace_1_to_stage = toStageProps.get("namespace_1"); - HashMap<String, String> namespace_2_to_stage = toStageProps.get("namespace_2"); - Assert.assertTrue(namespace_1_to_stage != null); - Assert.assertTrue(namespace_2_to_stage != null); - - String namespace_1_flag_1 = namespace_1_to_stage.get("flag_1"); - String namespace_1_flag_2 = namespace_1_to_stage.get("flag_2"); - String namespace_2_flag_1 = namespace_2_to_stage.get("flag_1"); - Assert.assertTrue(namespace_1_flag_1 == null); - Assert.assertTrue(namespace_1_flag_2 != null); - Assert.assertTrue(namespace_2_flag_1 != null); - Assert.assertTrue(namespace_1_flag_2.equals("false")); - Assert.assertTrue(namespace_2_flag_1.equals("true")); - } } diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java index 29f3720a1828..1b0a8d2222b9 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java @@ -210,12 +210,10 @@ public class WallpaperCropperTest { new Rect(0, 0, bitmapSize.x, bitmapSize.y), new Rect(100, 200, bitmapSize.x - 100, bitmapSize.y))) { for (int mode: ALL_MODES) { - for (boolean rtl: List.of(true, false)) { - for (boolean parallax: List.of(true, false)) { - assertThat(WallpaperCropper.getAdjustedCrop( - crop, bitmapSize, displaySize, parallax, rtl, mode)) - .isEqualTo(crop); - } + for (boolean parallax: List.of(true, false)) { + assertThat(WallpaperCropper.getAdjustedCrop( + crop, bitmapSize, displaySize, parallax, mode)) + .isEqualTo(crop); } } } @@ -235,11 +233,9 @@ public class WallpaperCropperTest { int expectedWidth = (int) (displaySize.x * (1 + WallpaperCropper.MAX_PARALLAX)); Point expectedCropSize = new Point(expectedWidth, 1000); for (int mode: ALL_MODES) { - for (boolean rtl: List.of(false, true)) { - assertThat(WallpaperCropper.getAdjustedCrop( - crop, bitmapSize, displaySize, true, rtl, mode)) - .isEqualTo(centerOf(crop, expectedCropSize)); - } + assertThat(WallpaperCropper.getAdjustedCrop( + crop, bitmapSize, displaySize, true, mode)) + .isEqualTo(centerOf(crop, expectedCropSize)); } } @@ -258,11 +254,9 @@ public class WallpaperCropperTest { Point bitmapSize = new Point(acceptableWidth, 1000); Rect crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y); for (int mode : ALL_MODES) { - for (boolean rtl : List.of(false, true)) { - assertThat(WallpaperCropper.getAdjustedCrop( - crop, bitmapSize, displaySize, true, rtl, mode)) - .isEqualTo(crop); - } + assertThat(WallpaperCropper.getAdjustedCrop( + crop, bitmapSize, displaySize, true, mode)) + .isEqualTo(crop); } } } @@ -292,11 +286,9 @@ public class WallpaperCropperTest { for (int i = 0; i < crops.size(); i++) { Rect crop = crops.get(i); Rect expectedCrop = expectedAdjustedCrops.get(i); - for (boolean rtl: List.of(false, true)) { - assertThat(WallpaperCropper.getAdjustedCrop( - crop, bitmapSize, displaySize, false, rtl, WallpaperCropper.ADD)) - .isEqualTo(expectedCrop); - } + assertThat(WallpaperCropper.getAdjustedCrop( + crop, bitmapSize, displaySize, false, WallpaperCropper.ADD)) + .isEqualTo(expectedCrop); } } @@ -317,11 +309,9 @@ public class WallpaperCropperTest { Point expectedCropSize = new Point(1000, 1000); for (Rect crop: crops) { - for (boolean rtl : List.of(false, true)) { - assertThat(WallpaperCropper.getAdjustedCrop( - crop, bitmapSize, displaySize, false, rtl, WallpaperCropper.REMOVE)) - .isEqualTo(centerOf(crop, expectedCropSize)); - } + assertThat(WallpaperCropper.getAdjustedCrop( + crop, bitmapSize, displaySize, false, WallpaperCropper.REMOVE)) + .isEqualTo(centerOf(crop, expectedCropSize)); } } @@ -348,14 +338,14 @@ public class WallpaperCropperTest { Rect crop = crops.get(i); Rect expected = expectedAdjustedCrops.get(i); assertThat(WallpaperCropper.getAdjustedCrop( - crop, bitmapSize, displaySize, false, false, WallpaperCropper.BALANCE)) + crop, bitmapSize, displaySize, false, WallpaperCropper.BALANCE)) .isEqualTo(expected); Rect transposedCrop = new Rect(crop.top, crop.left, crop.bottom, crop.right); Rect expectedTransposed = new Rect( expected.top, expected.left, expected.bottom, expected.right); assertThat(WallpaperCropper.getAdjustedCrop(transposedCrop, bitmapSize, - transposedDisplaySize, false, false, WallpaperCropper.BALANCE)) + transposedDisplaySize, false, WallpaperCropper.BALANCE)) .isEqualTo(expectedTransposed); } } diff --git a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java index 9862663c37b2..1db97b9ede81 100644 --- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java +++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java @@ -186,7 +186,8 @@ public class BugreportManagerServiceImplTest { new FileDescriptor(), /* screenshotFd= */ null, BugreportParams.BUGREPORT_MODE_FULL, /* flags= */ 0, new Listener(new CountDownLatch(1)), - /* isScreenshotRequested= */ false); + /* isScreenshotRequested= */ false, + /* skipUserConsentUnused = */ false); assertThat(mInjector.isBugreportStarted()).isTrue(); } @@ -202,7 +203,8 @@ public class BugreportManagerServiceImplTest { new FileDescriptor(), /* screenshotFd= */ null, BugreportParams.BUGREPORT_MODE_FULL, /* flags= */ 0, new Listener(new CountDownLatch(1)), - /* isScreenshotRequested= */ false); + /* isScreenshotRequested= */ false, + /* skipUserConsentUnused = */ false); assertThat(mInjector.isBugreportStarted()).isTrue(); } @@ -216,7 +218,8 @@ public class BugreportManagerServiceImplTest { new FileDescriptor(), /* screenshotFd= */ null, BugreportParams.BUGREPORT_MODE_FULL, /* flags= */ 0, new Listener(new CountDownLatch(1)), - /* isScreenshotRequested= */ false)); + /* isScreenshotRequested= */ false, + /* skipUserConsentUnused = */ false)); assertThat(thrown.getMessage()).contains("not an admin user"); } @@ -232,7 +235,8 @@ public class BugreportManagerServiceImplTest { new FileDescriptor(), /* screenshotFd= */ null, BugreportParams.BUGREPORT_MODE_REMOTE, /* flags= */ 0, new Listener(new CountDownLatch(1)), - /* isScreenshotRequested= */ false)); + /* isScreenshotRequested= */ false, + /* skipUserConsentUnused = */ false)); assertThat(thrown.getMessage()).contains("not affiliated to the device owner"); } @@ -243,7 +247,7 @@ public class BugreportManagerServiceImplTest { Listener listener = new Listener(latch); mService.retrieveBugreport(Binder.getCallingUid(), mContext.getPackageName(), mContext.getUserId(), new FileDescriptor(), mBugreportFile, - /* keepOnRetrieval= */ false, listener); + /* keepOnRetrieval= */ false, /* skipUserConsent = */ false, listener); assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(listener.getErrorCode()).isEqualTo( BugreportCallback.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE); diff --git a/services/tests/servicestests/test-apps/PackageParserApp/Android.bp b/services/tests/servicestests/test-apps/PackageParserApp/Android.bp index 131b380d9215..3def48aefa00 100644 --- a/services/tests/servicestests/test-apps/PackageParserApp/Android.bp +++ b/services/tests/servicestests/test-apps/PackageParserApp/Android.bp @@ -116,3 +116,20 @@ android_test_helper_app { resource_dirs: ["res"], manifest: "AndroidManifestApp7.xml", } + +android_test_helper_app { + name: "PackageParserTestApp8", + sdk_version: "current", + srcs: ["**/*.java"], + dex_preopt: { + enabled: false, + }, + optimize: { + enabled: false, + }, + resource_dirs: ["res"], + aaptflags: [ + "--feature-flags my.flag1,my.flag2,my.flag3,my.flag4,unknown.flag", + ], + manifest: "AndroidManifestApp8.xml", +} diff --git a/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp8.xml b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp8.xml new file mode 100644 index 000000000000..d489c1bb9e07 --- /dev/null +++ b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp8.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.servicestests.apps.packageparserapp" > + + <application> + <activity android:name=".TestActivity" + android:exported="true" /> + </application> + + <permission android:name="PERM1" android:featureFlag="my.flag1 " /> + <permission android:name="PERM2" android:featureFlag=" !my.flag2" /> + <permission android:name="PERM3" android:featureFlag="my.flag3" /> + <permission android:name="PERM4" android:featureFlag="!my.flag4" /> + <permission android:name="PERM5" android:featureFlag="unknown.flag" /> +</manifest>
\ No newline at end of file diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java index 37e0818eb083..5787780cef46 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java @@ -24,6 +24,8 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; @@ -250,6 +252,7 @@ public class ActivityOptionsTest { case ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN: case ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN: case ActivityOptions.KEY_TRANSIENT_LAUNCH: + case ActivityOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED: case "android:activity.animationFinishedListener": // KEY_ANIMATION_FINISHED_LISTENER case "android:activity.animSpecs": // KEY_ANIM_SPECS @@ -319,7 +322,7 @@ public class ActivityOptionsTest { Log.e("ActivityOptionsTests", "Unknown key " + key + " is found. " + "Please review if the given bundle should be protected with permissions."); } - assertTrue(unknownKeys.isEmpty()); + assertThat(unknownKeys).isEmpty(); } public static class TrampolineActivity extends Activity { 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 5b1a18da3173..9b48cb9d328c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java @@ -314,6 +314,18 @@ public class WallpaperControllerTests extends WindowTestsBase { // Wallpaper is invisible because the lowest show-when-locked activity is opaque. assertNull(wallpaperController.getWallpaperTarget()); + // Only transient-launch transition will make notification shade as last resort target. + // This verifies that regular transition won't choose invisible keyguard as the target. + final WindowState keyguard = createWindow(null /* parent */, + WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE, "keyguard"); + keyguard.mAttrs.flags |= FLAG_SHOW_WALLPAPER; + registerTestTransitionPlayer(); + final Transition transition = wallpaperWindow.mTransitionController.createTransition( + WindowManager.TRANSIT_CHANGE); + transition.collect(keyguard); + wallpaperController.adjustWallpaperWindows(); + assertNull(wallpaperController.getWallpaperTarget()); + // A show-when-locked wallpaper is used for lockscreen. So the top wallpaper should // be the one that is not show-when-locked. final WindowState wallpaperWindow2 = createWallpaperWindow(mDisplayContent); diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt index b8d18001f37b..3f2b13aed5c0 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt @@ -290,6 +290,16 @@ fun FieldNode.getVisibility(): Visibility { return Visibility.fromAccess(this.access) } +/** Return the [access] flags without the visibility */ +fun clearVisibility(access: Int): Int { + return access and (Opcodes.ACC_PUBLIC or Opcodes.ACC_PROTECTED or Opcodes.ACC_PRIVATE).inv() +} + +/** Return the visibility part of the [access] flags */ +fun getVisibility(access: Int): Int { + return access and (Opcodes.ACC_PUBLIC or Opcodes.ACC_PROTECTED or Opcodes.ACC_PRIVATE) +} + /* diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt index 6643492a1394..c99ff0e5d990 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt @@ -195,6 +195,8 @@ abstract class BaseAdapter ( return null } + var newAccess = access + // Maybe rename the method. val newName: String val renameTo = filter.getRenameTo(currentClassName, name, descriptor) @@ -205,8 +207,9 @@ abstract class BaseAdapter ( // (the one with the @substitute/replace annotation). // `name` is the name of the method we're currently visiting, so it's usually a // "...$ravewnwood" name. - if (!checkSubstitutionMethodCompatibility( - classes, currentClassName, newName, name, descriptor, options.errors)) { + newAccess = checkSubstitutionMethodCompatibility( + classes, currentClassName, newName, name, descriptor, options.errors) + if (newAccess == NOT_COMPATIBLE) { return null } @@ -221,7 +224,7 @@ abstract class BaseAdapter ( // But note, we only use it when calling the super's method, // but not for visitMethodInner(), because when subclass wants to change access, // it can do so inside visitMethodInner(). - val newAccess = updateAccessFlags(access, name, descriptor) + newAccess = updateAccessFlags(newAccess, name, descriptor) val ret = visitMethodInner(access, newName, descriptor, signature, exceptions, policy, renameTo != null, @@ -303,4 +306,4 @@ abstract class BaseAdapter ( return ret } } -}
\ No newline at end of file +} diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/Helper.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/Helper.kt index 9d66c32e76ee..dc4f26bdda34 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/Helper.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/Helper.kt @@ -17,12 +17,19 @@ package com.android.hoststubgen.visitors import com.android.hoststubgen.HostStubGenErrors import com.android.hoststubgen.asm.ClassNodes +import com.android.hoststubgen.asm.clearVisibility import com.android.hoststubgen.asm.getVisibility import com.android.hoststubgen.asm.isStatic +const val NOT_COMPATIBLE: Int = -1 + /** * Make sure substitution from and to methods have matching definition. - * (static-ness, visibility.) + * (static-ness, etc) + * + * If the methods are compatible, return the "merged" [access] of the new method. + * + * If they are not compatible, returns [NOT_COMPATIBLE] */ fun checkSubstitutionMethodCompatibility( classes: ClassNodes, @@ -31,33 +38,31 @@ fun checkSubstitutionMethodCompatibility( toMethodName: String, // the one with either a "_host" or "$ravenwood" prefix. (typically) descriptor: String, errors: HostStubGenErrors, -): Boolean { +): Int { val from = classes.findMethod(className, fromMethodName, descriptor) if (from == null) { errors.onErrorFound( - "Substitution-from method not found: $className.$fromMethodName$descriptor") - return false + "Substitution-from method not found: $className.$fromMethodName$descriptor" + ) + return NOT_COMPATIBLE } val to = classes.findMethod(className, toMethodName, descriptor) if (to == null) { // This shouldn't happen, because the visitor visited this method... errors.onErrorFound( - "Substitution-to method not found: $className.$toMethodName$descriptor") - return false + "Substitution-to method not found: $className.$toMethodName$descriptor" + ) + return NOT_COMPATIBLE } if (from.isStatic() != to.isStatic()) { errors.onErrorFound( "Substitution method must have matching static-ness: " + - "$className.$fromMethodName$descriptor") - return false - } - if (from.getVisibility().ordinal > to.getVisibility().ordinal) { - errors.onErrorFound( - "Substitution method cannot have smaller visibility than original: " + - "$className.$fromMethodName$descriptor") - return false + "$className.$fromMethodName$descriptor" + ) + return NOT_COMPATIBLE } - return true + // Return the substitution's access flag but with the original method's visibility. + return clearVisibility (to.access) or getVisibility(from.access) } diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt index 931f0c5fa793..dd638925a5bc 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt @@ -644,9 +644,9 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkClassAnnota suffix="_host" ) - public static int nativeAddThree_host(int); + private static int nativeAddThree_host(int); descriptor: (I)I - flags: (0x0009) ACC_PUBLIC, ACC_STATIC + flags: (0x000a) ACC_PRIVATE, ACC_STATIC Code: stack=2, locals=1, args_size=1 x: iload_0 diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations.java index ab387e0938c3..6d8a48a37fea 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations.java +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations.java @@ -73,7 +73,8 @@ public class TinyFrameworkClassAnnotations { @HostSideTestSubstitute(suffix = "_host") public static native int nativeAddThree(int value); - public static int nativeAddThree_host(int value) { + // This method is private, but at runtime, it'll inherit the visibility of the original method + private static int nativeAddThree_host(int value) { return value + 3; } diff --git a/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/visitors/HelperTest.kt b/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/visitors/HelperTest.kt index 0ea90ed2fbf0..75e2536a98fa 100644 --- a/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/visitors/HelperTest.kt +++ b/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/visitors/HelperTest.kt @@ -71,7 +71,7 @@ class HelperTest { addClass(cn) } - fun check(from: MethodNode?, to: MethodNode?, expected: Boolean) { + fun check(from: MethodNode?, to: MethodNode?, expected: Int) { assertThat(checkSubstitutionMethodCompatibility( classes, cn.name, @@ -82,21 +82,21 @@ class HelperTest { )).isEqualTo(expected) } - check(staticPublic, staticPublic, true) - check(staticPrivate, staticPrivate, true) - check(nonStaticPublic, nonStaticPublic, true) - check(nonStaticPProtected, nonStaticPProtected, true) + check(staticPublic, staticPublic, Opcodes.ACC_PUBLIC or Opcodes.ACC_STATIC) + check(staticPrivate, staticPrivate, Opcodes.ACC_PRIVATE or Opcodes.ACC_STATIC) + check(nonStaticPublic, nonStaticPublic, Opcodes.ACC_PUBLIC) + check(nonStaticPProtected, nonStaticPProtected, 0) - check(staticPublic, null, false) - check(null, staticPublic, false) + check(staticPublic, null, NOT_COMPATIBLE) + check(null, staticPublic, NOT_COMPATIBLE) - check(staticPublic, nonStaticPublic, false) - check(nonStaticPublic, staticPublic, false) + check(staticPublic, nonStaticPublic, NOT_COMPATIBLE) + check(nonStaticPublic, staticPublic, NOT_COMPATIBLE) - check(staticPublic, staticPrivate, false) - check(staticPrivate, staticPublic, true) + check(staticPublic, staticPrivate, Opcodes.ACC_PUBLIC or Opcodes.ACC_STATIC) + check(staticPrivate, staticPublic, Opcodes.ACC_PRIVATE or Opcodes.ACC_STATIC) - check(nonStaticPublic, nonStaticPProtected, false) - check(nonStaticPProtected, nonStaticPublic, true) + check(nonStaticPublic, nonStaticPProtected, Opcodes.ACC_PUBLIC) + check(nonStaticPProtected, nonStaticPublic, 0) } }
\ No newline at end of file |